最近在搞nginx源码,学习过程中经常会看到TIME_WAIT这个词汇,不禁想起以前搞netty异步服务器的时候遇到过涉及TIME_WAIT的神奇现象。这么有趣的东西,我就停下来总结一下吧!
先说说TIME_WAIT是啥?不得不直接抛TCP连接三次握手和四次挥手图了,具体不详说了,烂大街的资料~。
图中找啊找,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属性,这属性是干嘛的呢,容我照搬下它的具体作用:
从第一点描述内容就可以判断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,就可以完全避免上述情况发生。