Unix网络编程——chap4

基本的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
2
3
4
5
6
//IPv4,INADDR_ANY是一个常量值
struct sockaddr_in servaddr;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
//IPv6, in6addr_any是一个结构
struct sockaddr_in6 servaddr;
servaddr.sin6_addr = in6addr_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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pid_t pid;
int listenfd, connfd;
listenfd = Socket(...);
Bind(listenfd, ...);
Listen(listenfd, LISTENQ);
for (;;) {
connfd = Accept(listenfd, ...);
if ( (pid = Fork()) == 0) {
Close(listenfd);
doit(connfd);
Close(connfd);
exit(0);
}
Close(connfd);
}

在上面的例子中,注意两个close操作。由于每个套接字其实都会有一个引用计数,而引用计数就是在文件标表项中维护着。在fork出一个新的进程后,connfd和listenfd的引用计数都变成了2。但我们的模型中更希望的是,listenfd在父进程中,connfd在子进程,所以我们分别执行了close操作。

close函数

1
int close(int sockfd);

这个函数的功能在上面已经说了,就是使得套接字描述符的引用计数减一。如果子进程关闭了connfd,而父进程没有,那么在一定时间之后,套接字描述符将会被用完。

总结

大多数TCP服务器都是并发的,堆每个待处理的客户连接调用一个fork派生一个子进程。而大多数UDP都是迭代的。