TypeCodes

使用TCPDUMP和Wireshark排查服务端CLOSE_WAIT(二)

前文《使用TCPDUMP和Wireshark排查服务端CLOSE_WAIT(一)》通过TCPDUMP和Wireshark在利用CentOS7作为服务端、Windows10作为客户端,模拟演示了一个TCP通信的CLOSE_WAIT状态,这篇文章主要利用前文的数据尝试解释Linux服务端产生CLOSE_WAIT状态的原因。

客户端和服务端的TCP通信流程

1 原因分析:从客户端和服务端TCP通信的流程出发

从前文中的tcpdump和Wireshark抓包都可看到当Windows客户端关闭后,会主动发送带有FIN+ACK标志的报文给Linux服务端。那么从上图TCP客户端和服务端的通信流程图开始分析:客户端先进入FIN_WAIT_1状态,在收到服务端应答的ACK标志的报文后进入FIN_WAIT_2状态(在Windows中重新打开一个PowerShell窗口,然后输入命令netstat -na|findstr 8000查看)。

Windows客户端出现FIN_WAIT_2状态

同时,服务端的TCP状态也就变成了CLOSE_WAIT。但是后面由于Linux服务端没有调用close()函数关闭socket链路,也即没有发送FIN标志的报文给主动关闭TCP链路的客户端,所以造成这个问题。

2 原因分析:从服务端程序出发

在服务端程序的第69行可以看到:一旦客户端关闭socket后,服务端也会调用close( client_sockfd );来关闭链路。那为什么还是会出现CLOSE_WAIT现象呢?答案是因为服务端在与客户端三次握手完后,只有一个进程(PID:5325)在处理客户端的TCP数据交互,而这个进程正在处理在Linux中使用telnet命令建立起来的这个客户端(PID:5331)的请求。

因此,在Windows中使用telnet命令作为客户端与Linux服务端完成三次握手后,没有相关进程来处理。这点也可以通过前文小节4中的截图看出,虽然TCP状态为ESTABLISHED,但是对应的进程PID/Program name为空,这点也可以通过lsof -i:8000命令验证(没有因为Windows客户端的连接出现进程打开的文件)。

Linux中利用netstat命令查看TCP服务状态

当Windows客户端关闭telnet界面后,Linux服务端虽然收到了客户端的FIN+ACK标志的报文,但是没有相关进程调用close()函数通知内核发送FIN报文给客户端。这样就造成了Linux服务端的TCP状态出现了CLOSE_WAIT,同时Windows客户端的TCP状态变成了对应的FIN_WAIT_2

3 问题延伸:从服务端程序出发

这里可能会存在疑问了,明明Windows客户端与Linux服务端建立了ESTABLISHED状态,也就是server_socket进程对它进行了处理,这不是与小节2中的原因分析相矛盾了吗?其实,这是由于对服务端的一些认识有偏差造成的,BZ之前也错误地认为以下命题是成立的:

listen()函数会使进程阻塞等待客户端的连接,也就是等待与客户端完成三次握手;
accept()函数就是服务端进程在完成三次握手后,接收客户端发送报文数据的请求,然后调用recv()函数来接收;
close()函数就是服务端进程直接向客户端发送FIN报文给客户端。

其实不然,在查阅了相关资料后,个人觉得正确的理解如下:

listen()函数不会使进程阻塞,UNP第3版84页有一句话:listen函数把一个未连接的套接字转换成一个被动套接字,指示内核应接受指向该该套接字的连接请求。
内核为任何一个给定的监听套接字维护两个队列:未完成连接队列和已完成连接队列。
因此,三次握手是由内核自动完成的,无需服务器进程插手。

accept()函数功能是从由内核维护的处于established状态的已完成连接队列列头部取出下一个已经完成的连接。
如果这个队列为空,accept()函数就会阻塞让进程进入睡眠状态。

close()函数是把一个TCP套接字标记成已关闭,然后立即返回调用进程。
TCP尝试发送已排队等待发送到对端的任何数据,发送完毕后发生的是正常的TCP连接终止序列,于是有了著名的四次挥手。

到这里问题其实已经很简单明了了,Linux内核完成“三次握手”跟服务端进程无关,当然这点也可以由程序没有打印第51、60行的数据证实。

4 总结

socket被动关闭的服务端产生CLOSE_WAIT的根本原因是没有调用close()函数关闭socket链路,也即没有发送FIN标志的报文给主动关闭TCP链路的客户端。

Comments »