值得收藏的TCP套接口编程文章 (2)

给函数bind指定用于捆绑的IP地址和/或端口号的结果:

IP地址 端口 结果
  0   内核选择IP地址和端口  
  非0   内核选择IP地址,进程指定端口  
本地IP地址   0   进程选择IP地址,内核指定端口  
本地IP地址   非0   进程选择IP地址和端口  
listen函数

函数listen仅被TCP服务器调用。

#include <sys/socket.h> /* basic socket definitions */ int listen(int sockfd, int backlog);/* 返回:0——成功,-1——出错 */

调用函数socket函数创建的套接口,默认是主动方,下一步应是调用connect,CLOSED的下一个状态是SYN_SENT(见TCP状态转换图)。而函数listen将套接口转换成被动方,告诉内核,应接受指向此套接口的连接请求,CLOSED状态变成LISTEN。

函数listen的第二个参数backlog表示内核为此套接口排队的最大连接数。对于给定的监听套接口,内核会维护两个队列:

未完成连接队列(incomplete connection queue) SYN分节已由客户发出,到达服务器,正在进行TCP的三路握手。此时这些套接口处于SYN_RCVD状态。

已完成连接队列(completed connection queue) SYN分节已由客户发出,到达服务器,并且已完成三路握手。此时这些套接口处于ESTABLISHED状态。

当来自客户的SYN到达时,TCP在未完成连接队列中创建一个新条目,直到三路握手中,第三个分节(客户对服务SYN的ACK)到达,这个条目移到已完成连接队列的队尾。

当进程调用accept函数时,已完成连接队列的头部条目返回给进程。

两个队列之和不能超过backlog

当一个客户SYN到达时,若这两个队列都是满的,TCP就忽略此分节,且不发送RST。客户TCP将重发SYN,期望不久就能在队列中找到空闲位置。

img

TCP为监听套接口维护的两个队列

accept函数

函数accept由TCP服务器调用,从已完成连接队列头部返回下一个已完成连接,若该队列为空,则进程睡眠(假定套接口为默认的阻塞方式)。

#include <sys/socket.h> /* basic socket definitions */ int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);/* 返回:非负描述字——成功,-1——出错 */

函数accept的第一个参数和返回值都是套接口描述字。其中,

第一个参数,称为监听套接口描述字,即由函数socket返回,也用于bind,listen的第一个参数。

返回值,称为已连接套接口描述字。

通常一个服务器,只生成一个监听套接口描述字,直到其关闭。而内核为每个被接受的客户连接,创建一个已连接套接口,当客户连接完成时,关闭该已连接套接口。

注意到intro/daytimetcpsrv.c中,后两个参数传的都是空指针,这是因为我们不关注客户的身份,无需知道客户的协议地址。

connfd = Accept(listenfd, (SA *) NULL, NULL);

稍作修改,不再传入空指针,见intro/daytimetcpsrv1.c:

socklen_t len; struct sockaddr_in servaddr, cliaddr; ... connfd = Accept(listenfd, (SA *) &cliaddr, &len); printf("connection from %s, port %d\n", Inet_ntop(AF_INET, &cliaddr.sin_addr, buff, sizeof(buff)), ntohs(cliaddr.sin_port));

kill掉之前的daytimetcpsrv进程:

$ sudo lsof -i -P | grep -i "listen" daytimetc 80986 root 3u IPv4 0xae12d925e4528793 0t0 TCP *:13 (LISTEN) $ sudo kill -9 80986

编译运行新的服务端程序:

$ make daytimetcpsrv1.c daytimetcpsrv1 $ ./daytimetcpsrv1

重复执行客户端程序,发几个请求:

$ ./daytimetcpcli 127.0.0.1 Wed Sep 26 14:11:20 2018 $ ./daytimetcpcli 127.0.0.1 Wed Sep 26 14:17:06 2018

查看服务端打印:

connection from 127.0.0.1, port 58201 connection from 127.0.0.1, port 58342

注意到,由于客户端程序没有调用bind函数,内核为它的协议地址选择了源ip作为IP地址,临时端口号也发生了变化。

fork和exec函数 #include <unistd.h> pid_t fork(void);/* 返回:在子进程中为0,在父进程中为子进程ID,-1——出错 */

fork函数调用一次,却返回两次。

在调用它的进程(即父进程),它返回一次,返回值是派生出来的子进程的进程ID。 父进程可能有很多子进程,必须通过返回值跟踪记录子进程ID。

在子进程,它还返回一次,返回值为0。 子进程只有一个父进程,总可以通过getppid来得到父进程的ID

通过返回值可以判断当前进程是子进程还是父进程。

父进程在调用fork之前打开的所有描述字在函数fork返回后都是共享的。网络服务器会利用这一特性:

父进程调用accept。

父进程调用fork,已连接套接口就在父进程与子进程间共享。(一般来说就是子进程读、写已连接套接口,而父进程关闭已连接套接口)。

fork有两个典型应用:

一个进程为自己派生一个拷贝,并发执行任务,这也是典型的并发网络服务器模型。

一个进程想执行其他的程序,于是调用fork生成一个拷贝,利用子进程调用exec来执行新的程序。典型应用是shell。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wpdjzs.html