基本的TCPt套接字编程
socket函数
1 | int socket(int family, int type, int protocol); |
执行网络IO的第一步就是要执行socket函数,通过指定期望的通信协议,并返回socketfd(套接字描述符,与文件描述符类似)
connect函数
1 | int connect(int sockfd, const struct sockaddr *serveraddr, socklen_t addrlen); |
客户在调用connect之后会激发TCP的三次握手过程,使得当前套接字从CLOSED转移到SYN_SEND,若成功即转移到ESTABLISHED,但其中有三种出错情况:
- 客户端没有收到响应,那么会进行超时重发;
- 客户端收到的响应为RST(表示复位),也就是服务端并没监听指定端口;
- 客户发出的SYN在某个路由器上引发了不可到达的ICMP错误,内核会进行超时重发;
而一旦connect失败了,当前的套接字将不再可用,必须先close,再重新调用socket。
bind函数
1 | int bind(int sockfd, const struct sockaddr* myaddr, socklen_t addrlen); |
bind函数的作用就是把协议族赋予给套接字。对于TCP来说,bind函数既可以指定IP地址,也可以指定端口,甚至可以两者都指定。但一般来说,为了实现特定的服务,我们都需要指定一个端口,而不是由内核来选择临时端口。
至于通配地址,内核会在连接上建立或者在套接字上发出数据报才会选择一个本地IP地址,对于IPv4和IPv6来说,有不同的指定方式:
1 | //IPv4,INADDR_ANY是一个常量值 |
绑定非通配符地址的例子,通常是为多个组织提供web服务器的主机上。
listen函数
1 | int listen(int sockfd, int backlog); |
listen函数能够把一个主动套接字转换成一个被动套接字。至于backlog参数,我们需要知道的是内核为任何一个给定的监听套接字维护两个队列:
- 未完成连接队列:在三次握手开始后,完成前,队列中会维护一项;
- 已完成队列:在三次握手成功后,未完成队列的一项将会转移到该队列的末尾;
而backlog就是指两个队列之和。
accept函数
1 | int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen); |
accept函数由TCP的服务器调用,用于从已连接的队列头中返回一项。如果队列为空,则进入睡眠状态。另外,accept调用成功的话会返回一个已连接套接字。
fork和exec函数
1 | pid_t fork(void); |
fork函数在父进程中返回子进程的pid,因为父进程可能有多个子进程,所以必须通过返回值记录pid;而在子进程中则返回0,这是因为子进程只有一个父进程,因此可以通过函数getppid()取得父进程的pid。
并发编程
看一个简单的并发编程代码:
1 | pid_t pid; |
在上面的例子中,注意两个close操作。由于每个套接字其实都会有一个引用计数,而引用计数就是在文件标表项中维护着。在fork出一个新的进程后,connfd和listenfd的引用计数都变成了2。但我们的模型中更希望的是,listenfd在父进程中,connfd在子进程,所以我们分别执行了close操作。
close函数
1 | int close(int sockfd); |
这个函数的功能在上面已经说了,就是使得套接字描述符的引用计数减一。如果子进程关闭了connfd,而父进程没有,那么在一定时间之后,套接字描述符将会被用完。
总结
大多数TCP服务器都是并发的,堆每个待处理的客户连接调用一个fork派生一个子进程。而大多数UDP都是迭代的。