文章目录
1、socket函数
#include <sys/types.h>
#include <sys/socket.h>
int socket(int family, int type, int protocol)
/** * func:创建通信端点(服务端)并返回一个描述符; * param family:指定通信域;选择用于通信的协议族; AF_INET IPv4 AF_INET6 IPv6 AF_LOCAL Unix域协议 AF_ROUTE 路由套接字 AF_KEY 密钥套接字 * param type:指定通信语义; SOCK_STREAM [TPC]提供有序的、可靠的、双向的、基于连接的字节流。 可能支持带外数据传输机制; SOCK_DGRAM [UDP]支持数据报(固定最大长度的无连接、不可靠消息); SOCK_SEQPACKET [有序分组]为最大固定长度的atagram提供一个顺序的、可靠的、基于双向连接的数据传输路径; 消费 者需要在每次输入系统调用时读取整个数据包; SOCK_RAW [原始套接字]提供原始网络协议访问; SOCK_RDM 提供不保证顺序的可靠数据报层; SOCK_NONBLOCK 在新打开的文件描述中设置O_NONBLOCK文件状态标志。 使用这个标志可以节省对fcntl(2) 的额外调用来实现相同的结果; * param protocol:当只有一个协议支持指定的套接字类型时,为0;其他需指定; IPPROTO_CP TCP传输协议 IPPROTO_UDP UDP传输协议 IPPROTO_SCTP SCTP传输协议 * return:成功返回新套接字的文件描述符,出错返回-1; * */
2、connect函数
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen)
/** * func:将套接字连接到由addr指定的地址; * param sockfd:返回的文件描述符; * param addr:指向sockaddr结构的指针;若sockfd的类型是SOCK_DGRAM,则addr是默认情况下发送数据报的地 址,也是接收数据报的唯一地址; 若为SOCK_STREAM或SOCK_SEQPACKET,此调用将尝试与绑定到 addr指定地址的套接字建立连接; * param addrlen:传入结构体的大小; * return:成功返回0,失败返回-1; * */
- 套接字必须含有服务器的IP地址和端口号;客户在调用该函数不必调用bind函数,若需要,则内核会确定源IP地址,并选择
一个临时端口为源端口;
- 该函数调用即TCP的三路握手过程,错误返回的几种情况:
- 若TCP客户没有收到SYN分节的响应,则返回ETIMEDOUT错误,超时无响应;
- 若客户的SYN的响应是RST(复位),则该服务器主机在我们指定的端口上没有进程在等待与之连接;返回ECONNREFUSED错误;
- 若客户发出的SYN在中间的某个路由器上引发目的不可达ICMP错误(软错误);
【产生RST的三条件】:
- 目的地为某端口的SYN到达,而该端口上没有正在监听的服务器;
- TCP想取消一个已有连接;
- TCP接收到一个根本不存在的连接上的分节;
2.1 案例
指定本机地址获取时间;【OK】
指定本地子网上其主机ID并不存在的IP地址;当客户机发出ARP请求时,将不会收到;【TIMEOUT】
指定一个没有运行时间获取服务器程序的主机【RST】
指定英特网中不可达的IP地址【ICMP错误】
注意
- connect函数导致当前套接字从CLOSED状态,转移到SYS_SENT状态,若成功则再转移到ESTABLISHED状态,若connect失败
则该套接字不再可用,必须关闭,不能对这样的套接字再次调用connect函数;
- 当调用connect调用失败后,必须close当前的套接字描述符并重新调用socket;
3、bind函数
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen)
/** * func:当使用socket创建套接字时,它存在于一个名称空间(地址族)中,但没有分配给它地址。 Bind()将addr * 指定的地址分配给套接字; * param sockfd:返回的文件描述符; * param addr:传入sockaddr_in结构体且需要进行强制转换为(sockaddr); serv_addr.sin_family = AF_INET; // 通信域 serv_addr.sin_port = htons(SERV_PORT); // 端口 serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // IP * param addrlen:传入addr的长度; * return:成功返回0,失败返回-1; * */
【bind可指定一个端口号或IP地址或两者都可指定】
- 【TCP客户没有捆绑端口】:
当调用connect或listen时,内核会为响应的套接字选择一个临时端口;
- 如RPC服务器,内核为其选择临时端口,而端口通过RPC端口映射器进行注册,客户再connect之前,必须与端口映射器联系以获取它们的临时端口;
- 【捆绑IP地址】:
对于TCP客户,为该套接字上发送的IP数据报指派了源IP地址;
对TCP服务器,就限定该套接字只接收那些目的地为该IP地址的客户连接;
TCP客户通常不把IP地址捆绑到它的套接字上,当连接套接字时,内核将根据所用外出网络接口来选择源IP地址,
而外出接口这取决于到达服务器所需的路径;
若TCP服务器没有捆绑IP地址,内核及那个客户发送的SYN目的IP地址作为服务器的源IP地址;
3.1 通配地址
IPv4一般使用来INADDR_ANY(0)来指定
struct sockaddr_in servaddr;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
/** 由于INADDR_常值一般都为主机字节序定义,故对此类常值需要使用htonl */
IPV6一般为128位
struct sockaddr_in6 serv;
serv.sin6_addr = in6addr_any;
如果让内核来选择一个临时端口,bind无法获取,必须使用getsockname来获取;
3.2 运行捆绑通配地址的当个服务器
当一个连接到达时,服务器调用getsockname获取客户的目的IP地址,服务器在根据这个客户连接所发往的IP地址来
处理客户的请求;
【好处】:把一个给定的目的IP地址解复用到一个给定的服务器进程时是由内核完成的;
必须细分一个分组到达接口和该分组的目的IP地址;若使用弱端系统模型,则意味着一个分组只要其目的IP地址能够标识
目的主机的某个网络接口即可,不一定是它的到达接口;
捆绑非通配IP地址只是限定根据目的IP地址来确定递送到套接字的数据报,而对于到达接口则未做任何限制,除非系统采
用强端系统模型;
4、listen函数
#include <sys/socket.h>
int listen(int sockfd, int backlog)
/** * func:将套接字标记为被动套接字,即用于accept接收传入的连接请求的套接字; * param sockfd:返回的文件描述符; * param backlog:最大了连接数; - 内核为一个给定的监听套接字维护两个队列: 【未完成连接队列SYN_RCVD】:每个这样的SYN分节都对应某个客户发出并到达服务器,而服务器正在等待完成相应的TCP三路握手; 【已完成连接队列ESTABLISHED】:每个已完成TCP三路握手过程的客户对应其中一项; 当未完成连接队列时中创建一项时,来自监听套接字的参数本复制到将建立连接中;连接的创建机制是完成自动的, * return:成功返回0,失败返回-1; 【注意】:不要将backlog设置为0,若不想客户链接,关闭套接字即可; * */
【该函数仅由TCP服务器调用,做以下两件事】:
- 当socket创建一个套接字时,它时一个将调用connect发起连接的客户套接字;该函数把一个为连接的套接字转换成一个被动套接字,指示内核应接收指向该套接字的连接请求;
【调用导致套接字从CLOSED状态转换到LISTEN状态】
若三路握手正常,该项将从未完成连接队列中的队头移到已完成连接队列的队尾,当accept调用时,已完成连接队列中的队头项将返回给进程,若该队列未空,则进程被投入睡眠;
4.1 listen包裹函数,设置backlog
void Listen(int fd, int backlog){
char *ptr;
if((ptr) = getenv("LISTENQ") != NULL)
bakclog = atoi(ptr);
if(listen(fd, backlog) < 0)
exit(-1);
}
5、accept函数
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)
/** * func:侦听套接字sockfd的挂起连接队列中提取第一个连接请求,创建一个新的连接套接字,并返回一个指向该套 * 接字的新文件描述符; * param sockfd:返回的文件描述符; * param addr:指向sockaddr结构的指针,为传出参数; * param addrlen:为值-结果参数(可查看上一篇文章); * return:成功返回文件描述符,失败返回-1; * */
若无需客户的协议地址,可将addr和addrlen设置为NULL;
5.1 服务端程序:通过accept获取客户端ip和端口
/** 运行时 */
#include "../Jxiepc/unp.h"
#include <iostream>
using namespace std;
int main() {
int lfd, cfd;
socklen_t len;
struct sockaddr_in s_addr, c_addr;
char buf[128];
time_t ticks;
/* 创建套接字 */
lfd = socket(AF_INET, SOCK_STREAM, 0);
/* 绑定端口、IP */
bzero(&s_addr, sizeof(s_addr));
s_addr.sin_family = AF_INET;
s_addr.sin_addr.s_addr = htonl(INADDR_ANY);
s_addr.sin_port = htons(13);
bind(lfd, (struct sockaddr*)&s_addr, sizeof(s_addr));
/* 设置监听个数 */
listen(lfd, 128);
while (true) {
/* 接收客户端 */
len = sizeof(c_addr);
cfd = accept(lfd, (struct sockaddr*)&c_addr, &len);
/* 获取客户端ip、端口 */
cout << "connect from " <<
inet_ntop(AF_INET, &c_addr.sin_addr, buf, sizeof(buf))
<< ", port " << ntohs(c_addr.sin_port) << endl;
/* 回写时间 */
ticks = time(NULL);
snprintf(buf, sizeof(buf), "%.24s\r\n", ctime(&ticks));
write(cfd, buf, strlen(buf));
close(cfd);
}
return 0;
}
6、fork和exec函数
#include <unistd.h>
pid_t fork(void);
/* * @func: 该函数调用一次返回两次,父进程中返回子进程的ID号,子进程中返回0;通过判断返回值来确定是否为子进程; - 在子进程中,阔以通过getppid来获取父进程ID;父进程可以有多个子进程,但无法获取各个子进程的ID(需要对每个进行跟踪); - 父进程中调用fork之前打开的所有描述符在fork返回后由子进程分享; - 服务器可利用父进程中调用accept后调用fork,使用子进程来处理客户; * return: 失败返回-1,成功返回:父进程返回子进程的ID以及子进程返回0; */
6.1 fork的典型用法
- 一个进程创建一个自身的副本,都可在另一个副本执行其他任务的同时处理各自的某个操作;
- 一个进程想要执行另一个程序,在子进程中调用exec把自身替换成新的程序;
6.2 exec函数
#include <unistd.h>
int execl(const char *pathname, const char *arg0, ...);
int execv(const char *pathname, char *const *argv[]);
int execle(const char *pathname, const char *arg0, ...);
int execve(const char *pathanem, char *const argv[], char *const envp[]);
int execlp(const char *filename, const char *arg0, ...);
int execvp(const char *filename, char *const argv[]);
/** 上述函数中,只有函数错误时,才返回调用者; 只有execve是系统调用,其余为库函数; */
以上函数的区别
- 上图中上行的函数把新程序的每个参数字符串指定成exec的一个独立参数,并以空指针结束的可变参数;
下行以argv为参数数组,其中含有指向新程序各个参数字符串的所有指针;
- 左列两个函数以filename为参数,指定文件名;右两列四个函数以pathname为参数;
- 左两列函数不显示指定一个环境指针;它们使用外部变量environ的当前值构造一个传递给新程序的环境列表;
注意
在exec前打开的描述符在exec后依旧为打开状态,但可以通过fcntl设置FD_CLOEXEC禁止;
7、并发服务器
最简单的方法是使用fork创建子进程来服务每个客户;
- 当调用accept返回时,就使用fork由子进程来服务客户,父进程用来等待连接;
7.1 结合fork的大致用法
pid_t pid;
int lfd, cfd;
bind(lfd, ...);
listen(lfd, 128);
while(1) {
cfd = accept(lfd, ...);
if(pid == fork()) == 0) {
close(lfd);
doit(cfd);
close(cfd);
exit(0);
}
/** 当连接建立,accpet返回,调用fork,由子进程服务客户(cfd),父进程等待新连接,由于客户由子进程提供服务,故在父 进程中需要关闭已经连接的套接字; */
close(cfd);
}
7.2 为什么父进程对cfd调用close没有终止它与客户的连接呢?
每个文件或套接字都由一个引用计数,维护在文件标项中,是当前打开着的引用描述符的个数;
socket返回后与lfd关联的文件引用数为1,accept与cfd关联的文件引用数也为1;在fork中,该两
个描述符在父进程与子进程间共享,故两个描述符的引用数变为2,而父进程中关闭cfd,只是将
引用计数从2减到1;
- 而真正的清理和资源释放要等到为0时才发生;
- 若父进程对accept返回的已连接套接字不调用close则将会耗尽可以描述符;
8、close函数
#include <unistd.h>
int close(int sockfd);
/** @func: 关闭套接字,并终止TCP连接; */
8.1 描述符引用计数
上述中,close减少描述符的计数,有时并不会引发TCP终止序列;
若想要TCP发生FIN,则改用shutdown函数;
9、getsockname和getpeername函数
#include <says/socket.h>
int getsockname(int sockfd, strict sockaddr *localaddr, socklen_t *addrlen);
/** @func: - 在没有bind的TCP客户上,connect成功后,该函数用于返回由内核赋予连接的本地 IP地址和本地端口号; - 在以端口号0调用bind(由内核去选择本地端口号)后,该函数用于返回由内核赋予的本地端口号; - 可用于获取某个套接字地址族; - 在一个以通配地址调用bind的tcp服务器上,与某客户连接建立后,getsockname可用于返回由内核赋予该连接的本地IP地址; - 当一个服务器是由调用过accept的某个进程通过调用exec,该函数可获取客户身份; */
int getpeername(int sockfd, struct sockaddr *peeraddr, socklen_t *addrlen);
/** @func: */
描述符的传递:
- 调用exec的进程可以把这个描述符格式化成一个字符串,作为命令行参数传递;
- 约定exec之前,总是把某个特定描述符置为所接受的已连接套接字的描述符;
7.2 获取套接字的地址族
int sockfd_to_family(int sockfd) {
struct sockaddr_storage ss;
socklen_t len;
len = sizeof(ss);
if(getsockname(sockfd, (struct sockaddr*)&ss, &len) < 0)
return -1;
return ss.sin_family;
}
今天的文章套接字socket的作用_如何实现TCP套接字退出[通俗易懂]分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/89675.html