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

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

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

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

同时,服务端的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客户端的连接出现进程打开的文件)。

当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链路的客户端。

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章