问题模型:
server1为服务端,在本地的9999端口监听,server2相对server1是客户端,server2启动后首先向server1发起连接,然后再8888端口监听。程序代码不在列出。
先后启动server1、server2,然后查看当前连接,如图1所示。
图1.建立连接,正常
之后强制结束server1(ctrl+c),再次查看当前连接状态,如图2所示。
图2.server1主动关闭后的状态
我们发现连接并没有消失(close),这是什么原因呢?我们用时序图分析一下,如图3.
图3.server1主动关闭时序
从时序图中可以看出server1终止伴随着FIN分节的发送,server2收到FIN导致进入CLOSE_WAIT状态,同时server2发送FIN分节的ACK,server1收到ACK后进入FIN_WAIT2状态。
注意:虽然进程server1已经终止,但是这条server2到server1的连接并没有终止,因为还没有完成TCP的最后“四次挥手”。
接着强制终止server2(ctrl+c),再次查看端口状态,如图4所示。
图4.server2终止后连接状态
终止server2,导致server向server1发送FIN。我们发现这个连接进入了TIME_WAIT状态。
我们再将终止server2后的时序图画出,如图5。
图5.关闭server2时序
关于什么是TIME_WAIT状态,以及为什么要有TIME_WAIT状态,网上可以很容易查到,这里不再赘述。但要说明的,从图4从可以看出,TIME_WAIT状态是在server1端出现的,也就是整个连接的主动关闭端。
下面开始我们真正的问题,我们重新启动server1,并用server2连接server1,结果如图6所示。此时,server2向server1发起链接,调用connect会失败。
图6.server1重启后,server2再连接
我们发现连接失败,而从出错的信息(connection refused)可以推断应该是服务端(server1)没有在相应端口(9999)监听。等一段时间过后,在查看端口状态,如图7所示,我们发现TIME_WAIT状态消失了。
图7.TIME_WAIT消失
server1并没有在9999端口监听,而我们的server1确的的确确的在运行如图8所示。
图8
这是什么原因?其实看似server1在运行,其实bind时已经出错,只是程序没有将出错信息打印出来。在server1添加如下代码:
n=bind(listenfd, (sockaddr *) &serveraddr, sizeof(serveraddr));
if(n!=0)
{
perror("bind error:");
}
再次启动server1(此时server1之前的连接仍处于TIME_WAIT状态),如图9所示。
图9
造成这个错误的原因是之前的连接还并没有消失(处于TIME_WAIT),而server1又试图bind一个现有连接(处于TIME_WAIT的连接)上的端口(9999),所以bind失败。Server2当然也就不能连接成功了。那么如何使服务端(server1)主动关闭后可以立即重启呢?
解决方法:在bind设置SO_REUSEADDR套接字选项。
const int on=1;
setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));
之后再次重复上述重启操作,结果如图10所示,重启成功。
图.10
l SO_REUSEADDR选项
SO_REUSEADDR选项的用途有多中,我们只讨论这里使用到的功能。先来看看UNP V1对这种情况的描述。
SO_REUSEADDR允许启动一个监听服务器并捆绑其众所周知端口,即使以前建立的将该端口用作它们的本地的连接仍存在。这个条件通常是这样碰到的:
(1) 启动一个监听服务器;
(2) 连接请求到达,派生一个子进程来处理这个客户;
(3) 监听服务器终止,但子进程继续为现有连接上的客户提供服务;
(4) 重启监听服务器。
默认情况下,当监听服务器在步骤(4)中通过调用socket、bind和listen重新启动时,由于它试图捆绑一个现有连接(即正由早先派生的那个子进程处理着的连接)上的端口,从而bind调用会失败。但如果该服务器在socket和bind中间调用设置了SO_REUSEADDR选项,那么bind将成功。 ——以上摘自UNP V1
下面对比我们这里遇到的情况,server1主动关闭后进入TIME_WAIT状态,此时对server1来说原有连接没有彻底终止,当重启server1时,就试图bind一个现有的连接,所以造成bind失败。所以一般TCP服务端都要设置SO_REUSEADDR选项,以便可以快速重启。