客户端TIME_WAIT的解决方法与思考

来源:互联网 发布:使用vmware安装linux 编辑:程序博客网 时间:2024/05/29 19:00

客户端TIME_WAIT的解决方法与思考

本文目的是为了记录在工作中遇到的问题及解决方法,便于以后学习总结,文中参考了以下两篇博客的内容:

  • http://www.cnblogs.com/cenhao/archive/2011/06/11/2078481.html
  • http://www.cnblogs.com/wangbin/p/4633049.html

TIME_WAIT

TIME_WAIT状态是指在SOCKET连接主动断开后,客户端会进入2MSL的等待期,这个机制的目的有两个:1)保证FIN的ACK能到达;2)确保迷路数据不会影响新的连接。具体在网上很多介绍。

现象

因为工作需要,最近写了一个客户端测试程序,其中客户端需要BIND本地端口,第一次向服务端发送数据正常。连接断开后,第二次重新发送,连接失败。
测试的环境是在Windows下,看了Netstat后,发现相应socket状态为TIME_WAIT。最开始想到的方式是设置socket选项,SO_REUSEADDR尝试客户端的socket句柄复用。在修改了客户端代码后,connect时报错。
这个问题困扰了很久,后来尝试使用SO_LINGER选项(使用RST代替FIN,避免TIME_WAIT的产生),这个选项暂时解决了问题,但是仍然有问题,这个问题在后面详细说明。

思考

虽然程序暂时可用,但是为什么SO_REUSEADDR没有效果。在阅读了以上两篇博客后,才发现这么多的需要注意的东西。

SO_REUSEADDR

UNPv1中有详细的说明:
SO_REUSEADDR可以用在以下四种情况下。

  • 1、当有一个有相同本地地址和端口的socket1处于TIME_WAIT状态时,而你启动的程序的socket2要占用该地址和端口,你的程序就要用到该选项。

  • 2、SO_REUSEADDR允许同一port上启动同一服务器的多个实例(多个进程)。但每个实例绑定的IP地址是不能相同的。在有多块网卡或用IP
    Alias技术的机器可以测试这种情况。

  • 3、SO_REUSEADDR允许单个进程绑定相同的端口到多个socket上,但每个socket绑定的ip地址不同。这和2很相似,区别请看UNPv1。

  • 4、SO_REUSEADDR允许完全相同的地址和端口的重复绑定。但这只用于UDP的多播,不用于TCP。

值得注意的是在百度百科中的一段话:
- 一个套接字由相关五元组构成,协议、本地地址、本地端口、远程地址、远程端口。SO_REUSEADDR 仅仅表示可以重用本地本地地址、本地端口,整个相关五元组还是唯一确定的。所以,重启后的服务程序有可能收到非期望数据。必须慎重使用 SO_REUSEADDR 选项。

也就是说,对于此选项来说,仅对本地IP及端口有效。也就是说如果TCP五元组的对端IP及端口仍然没变,那么仍然无法摆脱TIME_WAIT对socket的影响,这也就解释了第二次connect报错的原因,因为connect的时候,涉及到了对端的IP及端口,这样五元组完全一样,这是不允许的。

WINDOWS及LINUX区别

初步测试也验证了这一说法。但是问题出现了,在Linux端测试,发现使用SO_REUSEADDR,五元组完全一致的情况下,connect没有报错。
在引用的第二篇博客也有说明,原因在于:在Linux下socket API中的socket选项SO_REUSEADDR完全不同于TCP/IPv1中描述(那么就应该不同与大部分UNIX下的功能).

在UDP下:

  • 在LINUX下, 绑定相同端口(port)不同IP不需要任何额外的操作; 而在书中指出, 绑定相同端口不同IP需要在除了第一次绑定外的所有后续绑定前声明SO_REUSEADDR, 并且在这种情况下可以绑定通配IP地址(例如绑定了192.168.1.2:12345 后使用SO_REUSEADDR就可以绑定*:12345, 而在linux这是不可行的).
  • 在LINUX下, 绑定相同端口相同IP需要在每一次绑定(包括第一次)前声明SO_REUSEADDR, 绑定后如果收到多播或者广播的数据报, 那么每一个绑定该IP和端口的socket都可以收到这个数据报, 如果是收到单播的话就只有一个程序收到这个数据报(据说是最后一个绑定该IP:port的socket); 这和书中指出的做法基本相同, 除了某些unix系统(4.4BSD)使用的是SO_REUSEPORT, 而LINUX下无此选项, 另外如果linux需要绑定某个接口的IP和通配IP和相同端口(如192.168.1.2:12345和*:12345), 那么应该每次绑定前都声明SO_REUSEADDR这个选项.

在TCP下:

  • TCP要求正在使用的IP:port(处于TIME_WAIT状态的除外)不能通过任何形式来重用, 以防止端口盗用(TCP是面向链接的协议, 通过一个socket pair四元组确定一个链接, 如果链接的一端两个socket都能绑定同一个IP:port, 那么就破坏了四元组关系, 到底哪个socket应该收到数据就无法确定). 虽然某些系统能够这样做, 但是我们不应该违反约定而去做一些不兼容的coding. LINUX不支持这种操作.
  • 对于绑定不同IP相同端口的情况, LINUX也不需要任何操作, 这种情况和UDP的第一种完全一样, 和TCP/IPv1中的区别也和UDP的第一种完全一样, 因此, 在linux下TCP不能同时绑定某个接口的IP和通配地址的相同端口.
  • 对于处于TIME_WAIT的链接的复用, TCP 要求的是可以复用构成TIME_WAIT socket pair中的本机IP:port, 当不可使用这个IP:port去与TIME_WAIT socket pair 的另外一端的IP’:port’建立链接. 而在linux下, 这么做是可行的, 只要在每次绑定之前声明SO_REUSEADDR, 那么就可以重用正在TIME_WAIT的socket pair, 反之如果没有每次绑定前都声明好SO_REUSEADDR, 那么无论如何都无法重用正在TIME_WAIT的本机IP:port(即使这个IP:port和另外一端的IP:port构成的socket pair不是TIME_WAIT的socket pair, 并且必须每次都声明SO_REUSEADDR, 即使除了第一次之外都声明了SO_REUSEADDR, 都会绑定失败); 而在TCP/IPv1中描述的是, 只要在后续的绑定前声明好SO_REUSEADDR, 那么就可以绑定正在处于TIME_WAIT的本机IP:port, 只要不要再去connect那个和本机IP:port构成TIME_WAIT socket pair的就好(如果这么做connect会失败).

SO_REUSEADDR 使用结论

也就是说LINUX的socket中SO_REUSEADDR的实现与WINDOWS及大多数UNIX不同。TCP的五元组可以完全复用。那么为了解决TIME_WAIT,使客户端可以复用同样五元组socket,不受其影响。

  • 在WINDOWS端使用SO_REUSEADDR无效,可以尝试SO_LINGER,使用RST避免TIME_WAIT。
  • 在LINUX端,SO_REUSEADDR可以正常解决TIME_WAIT问题。

另一种解决方法

SO_LINGER作用

设置函数close()关闭TCP连接时的行为。缺省close()的行为是,如果有数据残留在socket发送缓冲区中则系统将继续发送这些数据给对方,等待被确认,然后返回。
利用此选项,可以将此缺省行为设置为以下两种

  • 立即关闭该连接,通过发送RST分组(而不是用正常的FIN|ACK|FIN|ACK四个分组)来关闭该连接。至于发送缓冲区中如果有未发送完的数据,则丢弃。主动关闭一方的TCP状态则跳过TIMEWAIT,直接进入CLOSED。网上很多人想利用这一点来解决服务器上出现大量的TIMEWAIT状态的socket的问题,但是,这并不是一个好主意,这种关闭方式的用途并不在这儿,实际用途在于服务器在应用层的需求。
  • 将连接的关闭设置一个超时。如果socket发送缓冲区中仍残留数据,进程进入睡眠,内核进入定时状态去尽量去发送这些数据。 在超时之前,如果所有数据都发送完且被对方确认,内核用正常的FIN|ACK|FIN|ACK四个分组来关闭该连接,close()成功返回。 如果超时之时,数据仍然未能成功发送及被确认,用上述a方式来关闭此连接。close()返回EWOULDBLOCK。

SO_LINGER选项使用如下结构:
typedef struct linger {
u_short l_onoff; //开关,零或者非零
u_short l_linger; //优雅关闭最长时限 } linger;

l_onoff l_linger closesocket行为 发送队列 底层行为 零 忽略 立即返回。 保持直至发送完成。 系统接管套接字并保证将数据发送至对端。 非零 零 立即返回。 立即放弃。 直接发送RST包,自身立即复位,不用经过2MSL状态。对端收到复位错误号。 非零 非零 阻塞直到l_linger时间超时或数据发送完成。(套接字必须设置为阻塞zhuan) 在超时时间段内保持尝试发送,若超时则立即放弃。 超时则同第二种情况,若发送完成则皆大欢喜。

总的来说,SO_LINGER选项,需要注意:

  • 服务端对RST的处理,RECV返回10054
  • 使用延时时间==0时,要考虑缓存队列是否会被放弃;
  • 使用延时时间>0时,考虑数据队列是否能发完;
0 0
原创粉丝点击