listen函数仅由TCP服务器调用,它做两件事情:
(1) 当socket函数创建一个套接字时,它被假设为一个主动套接字,也就是说,它是一个将调用connect发起连接的客户套接字。listen函数把一个未连接的套接字转换成一个被动套接字,指示内核应接受指向该套接字的连接请求。根据TCP状态转换图,调用listen导致套接字从CLOSED状态转换到LISTEN状态。
(2) 本函数的第二个参数规定了内核应该为相应套接字排队的最大连接数。
#include <sys/socket.h>
int listen(int sockfd, int backlog);
返回值:若成功则返回0,否则返回-1
本函数通常应该在调用socket和bind两个函数之后,并在调用accept函数之前调用。为了理解其中的backlog参数,我们必须认识到内核为任何一个给定的监听套接字维护两个队列:
(1) 未完成连接队列(incomplete connection queue),每个这样的SYN报文段对应其中一项:已由某个客户发出并到达服务器,而服务器正在等待完成相应的TCP三次握手过程。这些套接字处于SYN_RCVD状态。
(2) 已完成连接队列(completed connection queue),每个已完成TCP三次握手过程的客户对应其中一项。这些套接字处于ESTABLISHED状态。
图1描绘了监听套接字的这两个队列。
图1 TCP为监听套接字维护的两个队列
每当在未完成连接队列中创建一项时,来自监听套接字的参数就复制到即将建立的连接中。连接的创建机制是完全自动的,无需服务器进程插手。图2展示了用这两个队列建立连接时所交换的分组。
图2 TCP三次握手和监听套接字的两个队列
当来自客户的SYN到达时,TCP在未完成连接队列中创建一个新项,然后响应以三次握手的第二个报文段:服务器的SYN,其中捎带对客户SYN的ACK。这一项一直保留在未完成连接队列中,直到三次握手的第三个报文段(客户对服务器SYN的ACK)到达或者该项超时为止。如果三次握手正常完成,该项就从未完成连接队列移到已完成队列的队尾。当进程调用accept时,已完成连接队列中的队头项将返回给进程,或者如果该队列为空,那么进程将被投入睡眠,直到TCP在该队列中放入一项才唤醒它。
关于这两个队列的处理,以下几点需要考虑:
•listen函数的backlog参数曾被规定为这两个队列总和的最大值。
•源自Berkeley的实现给backlog增设了一个模糊因子(fudge factor):把它乘以1.5得到未处理队列最大长度。
•不要把backlog指定为0,因为不同的实现对此有不同的解释。如果你不想让任何客户端连接到你的监听套接字上,那就关闭该监听套接字。
•在三次握手正常完成的前提下(也就是说没有丢失报文段,从而没有重传),未完成连接队列中的任何一项在其中的存留时间就是一个RTT,而RTT的值取决于特定的客户与服务器。
•历来沿用的样例代码总是给出值为5的backlog,因为这是4.2BSD支持的最大值。这个值在20世纪80年代是足够的,当时繁忙的服务器一天也就处理几百个连接。然后随着万维网的发展,繁忙的服务器一天要处理几百万个连接,这个偏小的值就根本不够了。
•问题是既然backlog值为5往往不够,那么应用进程应该指定多大值的backlog呢?这个问题不好回答。当今的HTTP服务器指定了一个较大的值,但是如果这个指定值在源代码中是一个常值,那么增长其大小需要重新编译服务器程序。另一个方法是设定一个默认值,不过允许通过命名行选项或环境变量覆写该默认值。指定一个比内核能够支持的值还要大的backlog也是可以接受的,因为内核应该悄然把所指定的偏大值截成自身支持的最大值,而不返回错误。
•当一个客户SYN达到时,若这些队列是满的,TCP就忽略该报文段,也就是不发送RST。这么做是因为:这种情况是暂时的,客户TCP将重发SYN,期望不久就能在这些队列中找到可用空间。要是服务器TCP立即响应以一个RST,客户的connect调用就会立即返回一个错误,强制应用进程处理这种情况,而不是让TCP的正常重传机制来处理。另外,客户无法区分响应SYN的RST究竟意味着“该端口没有服务器在监听”,还是意味着“该端口有服务器在监听,不过它的队列满了”。
•在三次握手完成后,但在服务器调用accept之前到达的数据应由服务器TCP排队,最大数据量为相应已连接套接字的接收缓冲区大小。
图3给出了7种操作系统,backlog参数取不同值时已排队连接的实际数目。
图3 不同backlog值时已排队连接的实际数目