listen函数
每当在未完成连接队列中创建一项时,来自监听套接字的参数就复制到即将建立的连接中。连接的创建机制是完全自动的。无需服务器进程插手。下图展示了这两个队列建立连接时所交换的分组。
![8a81093522c304b13882006b8b952537.png](https://i-blog.csdnimg.cn/blog_migrate/bcebcb1c4c5d16ae1e9b276942d23e3c.png)
当来自客户的SYN到达时,TCP在未完成连接队列中创建一个新项,然后响应以三路握手的第二个分节:服务端的SYN响应,其中捎带对客户端SYN的ACK。这一项一直保留在未完成连接队列中,直到三路握手的第三个分节到达或者超时未止。如果三路握手正常完成,该项就从未完成连接队列移到已完成连接队列的队尾。当进程调用accept时,已完成连接队列中的队头将返回给进程,或者如果该队列为空,那么进程将被投入睡眠,直到TCP在该队列中放入一项才唤醒他。
关于这两个队列的处理,以下几点需要考虑
1) listen函数的backlog参数曾被规定为这两个队列总和的最大值
2) 源自Berkeley的实现给backlog增设了一个模糊因子(fudge factor)。把他乘以1.5得到未处理队列最大长度。举例来说,通常指定为5的backlog值实际上允许最多有8项在排队。
3) 不要把backlog定义为0,因为不同的实现对此有不同的解释。如果你不想让任何客户连接到你的监听套接字上,那就关掉该监听套接字。
4) 在三路握手正常完成的前提下(也就是说没有丢失分节,从而没有重传),未完成连接队列中的任何一项在其中的留存时间就是一个RTT,而RTT的值取决于特定的客户与服务器
5) 当一个客户SYN到达时,若这些队列是满的,TCP就忽略该分节,也就是不发送RST。这么做是因为:这种情况是暂时的,客户TCP将重发SYN,期望不久就能在这些队列中找到可用的空间。要是服务器TCP立刻响应一个RST,客户端connect调用就会立即返回一个错误,强制应用进程处理这种情况,而不是让TCP的正常重传机制来处理。另外,客户无法区分响应SYN的RST究竟意味着“该端口没有服务器在监听”,还是意味着“该端口有服务器在监听,不过他的队列满了”。
6) 在三次握手完成之后,但在服务器调用accept之前到达的数据应由服务器TCP排队,最大数据量为相应已连接套接字的接收缓冲区大小。
因特网受到一种称之为SYN泛滥(SYN flooding)的攻击。编写一个以高速率给受害主机发送SYN的程序,以装填一个或者多个TCP端口的未完成连接队列。该程序把每个SYN的源IP地址都置成随机数(称为IP欺骗 ip spoofing)这样服务器的SYN/ACK就发往不知道什么地方。这样使合法的SYN排不上队,导致针对合法客户的服务被拒绝(denial of service)。
accept函数
accept函数由TCP服务器调用,用于从已完成连接队列队头返回下一个已经完成连接。如果已完成连接队列为空,那么进程被投入睡眠(假定套接字为默认的阻塞方式)。
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr* cliaddr, socklen_t* addrlen);
返回:若成功则为非负描述符,若出错则为-1
参数cliaddr和addrlen用来返回已连接的对端进程的协议地址。addrlen是值-结果参数:调用前,我们将由*addrlen所引用的整数值置为由cliaddr所指的套接字地址结构的长度,返回时,该整数值即为由内核放在该套接字地址结构内的确切字节数。
如果accept成功,那么其返回值是由内核自动生成的一个全新的描述符,代表与所返回客户的TCP连接。在讨论accept函数时,我们称他的第一个参数为监听套接字描述符。他的返回值为已连接套接字描述符。一个服务器通常只创建一个已连接套接字,他在该服务器的生命周期内一直存在。内核为每个由服务器进程接收的客户连接创建一个已连接套接字。当服务器完成对某个客户的服务时,相应的已连接套接字就被关闭。如果我们对客户端地址不感兴趣的话,第二、三个参数可以都为NULL。
close函数
通常的Unix close函数也用来关闭套接字,并终止TCP连接。
#include <unistd.h>
int close(int sockfd);
返回:若成功则为0,若出错则为-1
close一个TCP套接字的默认行为是把该套接字记成已关闭,然后立即返回到调用进程。该套接字描述符不能再由调用进程使用,也就是说他不能再作为read或者write的第一个参数。然而TCP将尝试发送已排队等待发送到对端的任何数据,发送完毕后发送正常的TCP连接终止序列。我们可以用SO_LINGER套接字选项可以改变TCP套接字的这种默认行为。
描述符引用计数,并发服务器中父进程关闭已连接套接字只是导致相应描述符的引用计数减1。如果引用计数仍大于0,这个close调用并不引发TCP的四路挥手终止序列。对于父进程与子进程共享已连接套接字的并发服务器来说,这正是所期望的。
如果我们确实想在某个TCP连接上发送一个FIN,那么可以改用shutdown函数代替close。如果父进程对每个由accept返回的已连接套接字都不调用close,那么并发服务器中将会发生什么。首先,父进程最终将耗尽可用描述符,因为任何进程在任何时刻可拥有的打开着的描述符数通常是有限的。更严重的是,没有一个客户连接将会被终止。当子进程关闭已连接套接字时,他的引用计数值由2减为1且保持为1,因为父进程永不关闭已连接套接字。这将妨碍TCP连接终止序列的发生,导致连接一直打开着。
getsockname和getpeername函数
这两个函数或者返回与某个套接字关联的本地协议地址(getsockname),或者返回某个套接字关联的外地协议地址(getpeername)。
#include <sys/socket.h>
int getsockname(int sockfd, struct sockaddr* localaddr, socklen_t * addrlen);
int getpeername(int sockfd, struct sockaddr* peeraddr, socklen_t* addrlen);
返回:若成功返回0,若出错则为-1
需要这两个函数的理由如下所述:
1) 在一个没有调用bind的TCP客户端上,connect成功返回后,getsockname用于返回由内核赋予该连接的本地IP地址和本地端口号。
2) 在以端口号0调用bind(告知内核去选择本地端口号)后,getsockname用于返回由内核赋予的本地端口号。
3) getsockname可用于获取某个套接字的地址族
4) 在一个以通配IP地址调用bind的TCP服务器上,与某个客户的连接一旦建立,getsockname就可以用于返回内核赋予该连接的本地IP地址。在这样的调用中,套接字描述符参数必须时已连接的套接字描述符,而不是监听套接字描述符。
5) 当一个服务器是由调用过accept的某个进程通过调用exec执行程序时,他能够获取客户端身份的唯一途径便是调用getpeername。inetd fork并exec某个TCP服务器程序时就是如此情形。