|
/************************************
* 版权声明
* 本文为本人原创,本人拥有此文的版权。鉴于本人持续受益于开源软件社区,
* 本人声明:任何个人及团体均可不受限制的转载和复制本文,无论是否用于盈利
* 之目的,但必须在转载及复制时同时保留本版权声明,否则为侵权行为,本人保
* 留追究相应法律责任之权利。
* speng2005@gmail.com
* 2008-7
************************************/
近日无意间发现了一个小窍门:当TCP连接所对应socket的接收队列中仍有未读数据时,将此socket强行close后,将使此socket连接不会进入TIME_WAIT状态,用"netstat -anp"命令查看可发现此连接将消失的无影无踪!上述情形在linux2.6.18-5-686-bigmem内核及winxp平台上验证通过。其他平台上,以及当socket的发送队列中仍有剩余数据未发送时强行close后是否可重现此现象,尚未进行验证。
那么说了这么多,这个窍门有什么用呢?对TCP的socket编程比较熟悉的人都知道,通常情况下,当一个socket已成功地在服务端和客户端建立连接后,无论是哪一方,先行调用close的那一方所对应的socket最终必会进入TIME_WAIT状态,并且会持续2*MSL,大约1-4分钟,然后由操作系统自动回收并将TCP连接设为CLOSED初始状态。这个过程按下图中的TCP连接状态转换过程进行:
在socket的TIME_WAIT状态结束之前,该socket所占用的本地端口号将一直无法释放。编写过高TCP并发并且采用短连接方式进行通讯的软件程序的人,都可能体会到,这样的通讯系统在高并发高负载下运行一段时间后,就常常会出现做为客户端的程序无法向服务端建立新的socket连接的情况。此时用"netstat-anp"命令查看系统将会发现机器上存在大量处于TIME_WAIT状态的socket连接,并且占用大量的本地端口号。最后,当该机器上的可用本地端口号被占完,而旧的大量处于TIME_WAIT状态的socket尚未被系统回收时,就会出现无法向服务端创建新的socket连接的情况。此时的通讯系统几乎停转,空有再好的性能也发挥不出来。
如果能够在客户端程序主动关闭socket之前,让该socket的接收队列中仍保留一些数据(至少要有多余的一个字节的数据),然后调用close关闭,那么上述的无法向服务端创建新的socket连接的情况将不会出现。这是因为当socket的接收队列中仍有数据未被应用程序读走就被强行关闭时,操作系统(至少在笔者验证过的操作系统上的确如此)的TCP/IP协议栈驱动程序会在底层主动向服务端发送一个要求结束TCP连接的控制包,并将该TCP包头的flag控制字段中的RESET位置位,从而迅速结束了此TCP连接。这其实是操作系统对TCP连接断开的一种异常处理。而正常情况下(socket的接收队列中无未读数据),当应该程序调用close关闭连接时,底层驱动程序向服务端发送的要求结束TCP连接的控制包头的flag控制字段中是将FIN位置位,并且严格遵循上图所示的状态转换过程,最终到达TIME_WAIT状态并持续2*MSL的时间。
知道了上述原理,具体如何利用呢?这有两种思路。第一种,从服务端入手,在服务端向客户端返回所有有效数据后,再在后面插入若干填充字节;而在客户端将有效数据读完之后,将填充字节留在对应socket的接收队列中,然后直接关闭连接。这个思路的好处是对客户端程序不需要做改动。第二种思路是从客户端入手,在客户端程序中控制从socket的接收队列中recv数据的节奏,将最后一个有效字节留在接收队列中不取出。但是怎么知道哪里是最后一个字节呢?最后一个字节的内容是什么?解决第一个问题的方法就是制定客户端与服务端通讯协议时应该考虑提供某种机制,让客户端解包时能够知道数据包何时结束,例如可以在通讯包的头部指示整个包的长度。解决第二个问题的方法是调用recv时使用MSG_PEEK标志,例如:
ssize_t recvBytes = recv(socket,buf,buflen,MSG_PEEK);
当以MSG_PEEK标志接收数据时,数据将只从socket的接收队列中拷贝到指定的buf中,而相应的数据不会被移出接收队列。这样就能够既获得应用程序所需数据,又能将数据留在socket接收队列中,进而能够在关闭时使该TCP连接不进入TIME_WAIT状态。第二种思路的好处是不需要对服务端程序进行改动。笔者已将第二种思路实现并验证可行。
备注:在网上,解除TIME_WAIT状态限制有多种方法,其中有种方法是在socket建立后调用setsockopt函数如下:
int option = 1;
setsockopt(socket, SOL_SOCKET, SO_REUSEADDR, &option, sizeof(option));
如此就可以解决问题了。但实际上,有的平台不支持setsockopt函数,既使是在能够支持的平台,笔者经实际验证发现并不能达到目的。另有一些其他方法也各有利弊。所以,如果其他方法无效,本文中的方法就值得一试了。 |
|