一般而言,对于处理2MSL状态的套接字(一般为服务端套接字)是不允许接受从同一客户端重新发起一个新的连接的,但是套接字编程系统接口允许应用程序通过设置一个REUSEADDR选项,使处于2MSL状态的套接字重新接受从相同客户端发起的新的请求。很多教科书上都是这么说,但是其中有一个最为关键的问题大家都避而不谈,那就是在客户端请求连接时,服务器是新创建一个套接字用于通信的,侦听套接字还处于侦听状态,其不进行任何实际的数据交换。那么现在双方关闭连接,实际上从服务器的角度而言,处于2MSL等待状态也就是先前那个用于通信的套接字,这时候侦听套接字依然处于侦听状态,而且所谓接不接受新的连接请求,只能对侦听套接字而言,对于那个新创建的用于数据交换的套接字是不存在所谓的接不接受请求的情况的。那么此处就涉及一个底层sock数据结构的存储和查找问题。
get_sock函数,改函数根据套接字四元素(双方的IP地址和端口号),从系统队列中寻找满足条件的sock结构。实际上对于侦听套接字sock结构只有本地IP地址本地端口号进行标识,其不与任何远端IP地址端口号进行绑定,只有在处理一个连接请求时新创建的那个用于通信的套接字才进行四元素的绑定,而且这个新创建的套接字的本地IP地址和端口号大多数情况下是和侦听套接字相同的。
由于sock结构查找过程中首先是通过本地端口号进行数组元素队列的寻址,所以对于一个侦听套接字而言,其在调用tcp_conn_request函数处理一个连接请求时创建的新的套接字对应的sock结构都与该侦听sock结构处于同一个队列中,只不过在查找具体sock结构时,是查找最佳匹配的sock结构。由于侦听套接字只绑定了本地IP地址和端口号,而负责通信的套接字sock结构绑定了本地和远端的IP和端口号,在查找时匹配度叫侦听套接字高,所以通常在通信套接字处于非TCP_CLOSE状态其sock结构依然处于系统队列中,返回的是通信套接字的sock结构,而非侦听套接字sock结构。
那么对于新连接的请求,为何返回侦听套接字sock结构?这时候,侦听套接字以前创建的所有的新的套接字本地IP和端口号都符合条件,原因是:其一在查找对应sock结构时,远端IP地址和端口号都没有匹配项;其二侦听套接字处于队列的最前端,虽然由侦听套接字所创建的sock结构都满足本地IP地址和端口号的匹配,但查找规则是除非找到更合适的否则返回第一个满足条件的sock结构。
另外一个查找策略是如果套接字状态为TCP_CLOSE,则将该套接字排除在查找对象之外。综上所述,虽然在检查服务器端套接字状态为2MSL时使用的是通信套接字,但在接受一个远端的连接请求时,使用的确是侦听套接字。
4139 /* 4140 * BSD has a funny hack with TIME_WAIT and fast reuse of a port. There is 4141 * a more complex suggestion for fixing these reuse issues in RFC1644 4142 * but not yet ready for general use. Also see RFC1379. 4143 */ 4144 #define BSD_TIME_WAIT 4145 #ifdef BSD_TIME_WAIT 4146 if (sk->state == TCP_TIME_WAIT && th->syn && sk->dead && 4147 after(th->seq, sk->acked_seq) && !th->rst)
4146 行 if 语句是对处于 2MSL 状态的套接字是否接受到一个连接请求进行判断, 如果条件都满足,则表示接收到一个具有相同远端地址,远端端口号的连接请求(这一点是由 4540 行代码保证的)。在处理上是将听任原来的这个通信套接字释放,而将请求转移给侦听套接字,通过调用tcp_conn_request 函数重新创建一个套接字用于通信。下面我们逐行分析这时如何完成的。
4149 long seq=sk->write_seq;
保存原来这个通信套接字本地发送序列号最后值,下面( 4165 行)将这个序列号加上 128000作为新创建套接字的初始序列号。
4150 if(sk->debug) 4151 printk("Doing a BSD time wait "); 4152 tcp_statistics.TcpEstabResets++; 4153 sk->rmem_alloc -= skb->mem_len; 4154 skb->sk = NULL; 4155 sk->err=ECONNRESET; 4156 tcp_set_state(sk, TCP_CLOSE); 4157 sk->shutdown = SHUTDOWN_MASK; 4158 release_sock(sk); 4152-4158 行代码将原来的这个通信套接字完全置于关闭状态,首先将这个新的连接请求数据包 与这个通信套接字脱离干系( 4153, 4154 行),并设置套接字状态为完全关闭( 4156, 4157 行)。 最后调用 release_sock 函数进行释放(注意 release_sock 函数除了对缓存与 sock 结构中 back_log 队列中数据包调用 tcp_rcv 进行重新处理外, 对于处于 TCP_CLOSE 状态以及 sk->dead 设置为 1 的套接字进行释放操作(具体的通过设置一个定时器完成,定时器到期后方才进行释放)。换句 话说,以上这段代码是将原先的通信套接字完全置于“毁灭”。下面对于相同远端的连接请求通 过转移给侦听套接字, 而侦听套接字通过调用 tcp_conn_request 函数创建一个新的通信套接字儿 完成。 4159 sk=get_sock(&tcp_prot, th->dest, saddr, th->source, daddr); 当 4156 行代码将原来的处理那个相同远端的套接字被设置为 TCP_CLOSE 状态后, 其就不具备 被查找的资格,所以 4159 行代码调用 get_sock 函数返回的就是侦听套接字 sock 结构!即从 4159 行开始,如下的 sk 变量指向的都是侦听套接字的 sock 结构,那么当然如果服务应用程序仍然 运行的话, 4160 行为真,从进行 4165 行 tcp_conn_request 函数的调用处理连接请求(即又创建 一个新的通信套接字进行数据交互)。当然如果服务应用程序被关闭,简单丢弃此次连接请求。 如果远端再次发送连接请求,在对其下一个连接请求数据包进行处理时,本地会“毫不客气” 的回复一个 RST 数据包的。 4160 if (sk && sk->state==TCP_LISTEN) 4161 { 4162 sk->inuse=1; 第二章 网络协议实现文件分析 LINUX1.2.13 内核网络栈实现代码分析 4163 skb->sk = sk; 4164 sk->rmem_alloc += skb->mem_len; 4165 tcp_conn_request(sk, skb, daddr, saddr,opt, dev,seq+128000); 4166 release_sock(sk); 4167 return 0; 4168 } 4169 kfree_skb(skb, FREE_READ); 4170 return 0; 4171 } 4172 #endif 4173 }// if(sk->state!=TCP_ESTABLISHED) 通过 4146-4171 行代码的分析,读者现在应该明白通常我们所说的使用 REUSEADDR 选项的侦 听套接字究竟是如何处理的(注意 REUSEADDR 选项只可被用于侦听套接字)。在处理上,原 来的通信套接字仍然进行释放,只不过侦听套接字又创建了一个新的套接字用于同一远端的通 信。本质上,这与平常的处理连接请求的方式并无区别,仅仅在于现在这个请求发生在原来的 套接字还没有被释放(那么在处理上就加速了释放过程,从而为创建新的套接字扫清道路),而 且原来通信中可能被延迟的数据包会被发送到这个新创建的连接通道中。当然对于使用 REUSEADDR 选项的应用而言,如果真的发生这种情况,这也是自找的。对于以上代码的分析, 读者需要结合 get_sock 函数进行理解。