理解产生TIME_WAIT原因和SO_REUSEADDR设置(TCP)

    最近在搞nginx源码,学习过程中经常会看到TIME_WAIT这个词汇,不禁想起以前搞netty异步服务器的时候遇到过涉及TIME_WAIT的神奇现象。这么有趣的东西,我就停下来总结一下吧!

    先说说TIME_WAIT是啥?不得不直接抛TCP连接三次握手和四次挥手图了,具体不详说了,烂大街的资料~。

理解产生TIME_WAIT原因和SO_REUSEADDR设置(TCP)_第1张图片理解产生TIME_WAIT原因和SO_REUSEADDR设置(TCP)_第2张图片

   图中找啊找,TIME_WAIT找到了!不就是在TCP连接释放,主动关闭TCP连接的一方,差不多最后的状态么(持续2MSL即可进入最终closed状态)。

   先来说说主动关闭方最后回复ack后为啥要进入这个状态,持续2MSL(MSL值是数据包在网络中的最大生存时间)后才真正closed!主要是基于两方面原因:

1.TCP连接可靠释放:假设没有这个TIME_WAIT状态,直接进入closed状态。如果主动关闭方发送的ack包,对端未收到,对端会重发fin包,然后就会导致收到RST了,这会让对端产生connect reset by peer的错误,这是很不友好的。如果有TIME_WAIT状态,就能再次收到fin包时,重发ack包,让连接可靠释放。

2.确保旧连接的数据包在网络中因过期drop:五元组唯一标志一个tcp连接,如果没有TIME_WAIT状态,匆匆关闭一个连接启动新连接,是有可能重新收到网络中旧连接的数据包(比如ack包),从而产生问题。

   但是TIME_WAIT的2MSL对于监听服务器重启是极端不友好的,举个之前我搞netty服务器碰到的有趣现象

   调试服务器,每次立即重启服务器总是会报错,但是过一段时间就可以重启了,真神奇!其实就是TIME_WAIT在作怪!每次kill服务器都得等TIME_WAIT结束后,才能重启服务器,这样子可真麻烦啊。有没有好的解决办法呢?让我们看看nginx的热加载启动是怎么做的,源码如下:

if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR,
                           (const void *) &reuseaddr, sizeof(int))
                == -1)
{
    ngx_log_error(NGX_LOG_EMERG, log, ngx_socket_errno,
                  "setsockopt(SO_REUSEADDR) %V failed",
                      &ls[i].addr_text);

    if (ngx_close_socket(s) == -1) {
        ngx_log_error(NGX_LOG_EMERG, log, ngx_socket_errno,
                        ngx_close_socket_n " %V failed",
                           &ls[i].addr_text);
    }

    return NGX_ERROR;
}

    在nginx初始化启动中给每个即将监听的socket开启SO_REUSEADDR属性,这属性是干嘛的呢,容我照搬下它的具体作用:

  1. 允许启动一个监听服务器并捆绑其众所周知端口,即使以前建立的将此端口用做他们的本地端口的连接仍存在。这通常是重启监听服务器时出现,若不设置此选项,则bind时将出错。
  2. 允许在同一端口上启动同一服务器的多个实例,只要每个实例捆绑一个不同的本地IP地址即可。对于TCP,我们根本不可能启动捆绑相同IP地址和相同端口号的多个服务器。
  3. 允许单个进程捆绑同一端口到多个套接口上,只要每个捆绑指定不同的本地IP地址即可。这一般不用于TCP服务器。
  4. SO_REUSEADDR允许完全重复的捆绑:
    当一个IP地址和端口绑定到某个套接口上时,还允许此IP地址和端口捆绑到另一个套接口上。一般来说,这个特性仅在支持多播的系统上才有,而且只对UDP套接口而言(TCP不支持多播)。

   从第一点描述内容就可以判断SO_REUSEADDR可以实现我们服务器立即重启的需求,而且一般tcp服务器建议均是在所有TCP服务器中,在调用bind之前设置SO_REUSEADDR套接口选项。

   不明白第一点的童鞋,这里我再详细解释下第一点的情况,以及底层原理,举个例子: 

    开启服务器监听socket,listen后,每当新来个client连接,会accept返回一个新的socket套接字(accpet其实只是从已经建立好TCP连接的队列里拿连接而已),然后新连接会在新的socketfd上进行数据的读写。

   监听socket和新建的读写socket都是对应本地同一个端口的(比如都是80端口,多个新建的socket连接可以通过客户端的源端口、源地址区分)。这个时候你主动关闭服务器程序,新建的连接也全部关闭,主动释放socket资源,服务器就成为了主动关闭方,占用端口的连接会进入TIME_WAIT状态,并不会立即closed,这样我们就不能立即重启服务器监听原端口了。

  如果绑定监听前设置了SO_REUSEADDR,就可以完全避免上述情况发生。

 

你可能感兴趣的:(网络杂谈)