先给出四次挥手过程中C/S的状态变化示意图。有了图理解起来就容易许多。
TIME_WAIT 表示主动关闭,CLOSE_WAIT 表示被动关闭。
CLOSE_WAIT状态的生成原因
首先我们知道,如果我们的服务器程序APACHE处于CLOSE_WAIT状态的话,说明套接字是被动关闭的!
因为如果是CLIENT端主动断掉当前连接的话,那么双方关闭这个TCP连接共需要四个packet:
Client ---> FIN ---> Server
Client <--- ACK <--- Server
这时候Client端处于FIN_WAIT_2状态;而Server 程序处于CLOSE_WAIT状态。
Client <--- FIN <--- Server
这时Server 发送FIN给Client,Server 就置为LAST_ACK状态。
Client ---> ACK ---> Server
Client回应了ACK,那么Server 的套接字才会真正置为CLOSED状态。
Server 程序处于CLOSE_WAIT状态,而不是LAST_ACK状态,说明还没有发FIN给Client,那么可能是在关闭连接之前还有许多数据要发送或者其他事要做,导致没有发这个FIN packet。
客户端主动关闭时,发出FIN包,收到服务器的ACK,客户端停留在FIN_WAIT2状态。而服务端收到FIN,发出ACK后,停留在COLSE_WAIT状态。 这个CLOSE_WAIT状态非常讨厌,它持续的时间非常长,服务器端如果积攒大量的COLSE_WAIT状态的socket,有可能将服务器资源(套接字描述符耗尽)耗尽,进而无法提供服务。 那么,服务器上是怎么产生大量的失去控制的COLSE_WAIT状态的socket呢?
我们来追踪一下。 一个很浅显的原因是,服务器没有继续发FIN包给客户端。
服务器为什么不发FIN,可能是业务实现上的需要,现在不是发送FIN的时机,因为服务器还有数据要发往客户端,发送完了自然就要通过系统调用发FIN了,这个场景并不是上面我们提到的持续的COLSE_WAIT状态,这个在受控范围之内。
那么究竟是什么原因呢,咱们引入两个系统调用close(sockfd)和shutdown(sockfd,how)接着往下分析。
在这儿,需要明确的一个概念---- 一个进程打开一个socket,然后此进程再派生子进程的时候,此socket的sockfd会被继承。
socket是系统级的对象,现在的结果是,此socket被两个进程打开,此socket的引用计数会变成2。
继续说上述两个系统调用对socket的关闭情况。 调用close(sockfd)时,内核检查此fd对应的socket上的引用计数。如果引用计数大于1,那么将这个引用计数减1,然后返回。如果引用计数等于1,那么内核会真正通过发FIN来关闭TCP连接。
调用shutdown(sockfd,SHUT_RDWR)时,内核不会检查此fd对应的socket上的引用计数,直接通过发FIN来关闭TCP连接。
现在应该真相大白了,可能是服务器的实现有点问题,父进程打开了socket,然后用派生子进程来处理业务,父进程继续对网络请求进行监听,永远不会终止。客户端发FIN过来的时候,处理业务的子进程的read返回0,子进程发现对端已经关闭了,直接调用close()对本端进行关闭。
实际上,仅仅使socket的引用计数减1,socket并没关闭。从而导致系统中又多了一个CLOSE_WAIT的socket。。。 如何避免这样的情况发生? 子进程的关闭处理应该是这样的: shutdown(sockfd, SHUT_RDWR); close(sockfd);
这样处理,服务器的FIN会被发出,socket进入LAST_ACK状态,等待最后的ACK到来,就能进入初始状态CLOSED 最后加上点干货(从百度百科上看到关于FIN_WAIT_1和FIN_WAIT_2的理解。)
其实FIN_WAIT_1和FIN_WAIT_2状态的真正含义都是表示等待对方的FIN报文。
而这两种状态的区别是:FIN_WAIT_1状态实际上是当SOCKET在ESTABLISHED状态时,它想主动关闭连接,向对方发送了FIN报文,此时该SOCKET即进入到FIN_WAIT_1状态。
而当对方回应ACK报文后,则进入到FIN_WAIT_2状态,当然在实际的正常情况下,无论对方何种情况下,都应该马上回应ACK报文,所以FIN_WAIT_1状态一般是比较难见到的,而FIN_WAIT_2状态还有时常常可以用netstat看到。