Go语言客户端发起的TCP四次挥手未完成问题排查与解决
TCP连接四次挥手未完成的问题排查与解决
问题描述
用Go开发的TCP客户端/服务器程序,支持UDP与TCP协议切换。客户端运行正常,但服务器在连接处理循环末尾调用tcpConn.Close()后,无法完成客户端发起的TCP四次挥手。抓包显示只有杀死服务器进程后,才会完成挥手的最后阶段,服务器端连接长期处于CLOSE_WAIT状态,客户端处于FIN_WAIT2状态。
原因分析
从抓包数据和代码逻辑来看,核心问题是服务器未处理客户端发送的FIN包:
- 客户端发送完数据后调用
socket.Close(),会向服务器发送FIN包(对应抓包中第6条记录)。 - 服务器此时已经完成
tcpConn.Read()读取客户端数据的操作,回到循环顶部调用listener.Accept()阻塞等待新连接,完全没机会处理这个FIN包。 - 服务器调用
tcpConn.Close()时,TCP连接的读取通道仍处于等待客户端FIN的状态,因此Close()无法触发服务器发送FIN包,导致连接卡在CLOSE_WAIT。 - 只有当服务器进程被杀死时,操作系统会强制回收所有打开的文件描述符,才会触发FIN包发送,完成四次挥手。
解决方案
要解决这个问题,必须确保服务器在关闭连接前,处理完客户端发送的FIN包(即读取到io.EOF)。以下是两种可行方案:
方案1:循环读取直到遇到EOF
在处理完客户端的业务数据后,继续循环调用Read()直到返回io.EOF,确保接收到客户端的FIN包后再关闭连接:
// 处理完receivedText后添加读取逻辑 for { _, err := tcpConn.Read(recvBuffer) if err != nil { if err == io.EOF { // 客户端已主动关闭连接,正常结束读取 break } logMessage(fmt.Sprintf("Error reading from connection: %v", err)) break } } // 此时再关闭连接,能正常触发四次挥手 tcpConn.Close()
方案2:设置读取超时
如果不想无限等待客户端的FIN(比如客户端可能异常断开),可以设置读取超时,超时后直接关闭连接,避免连接长期卡在CLOSE_WAIT:
// 在处理完业务数据后设置读取超时 tcpConn.SetReadDeadline(time.Now().Add(5 * time.Second)) // 尝试读取剩余数据(包括客户端FIN) _, err := tcpConn.Read(recvBuffer) // 无论读取结果如何,关闭连接 tcpConn.Close()
优化后的服务器核心代码
修改serverModeTCP函数,整合超时读取逻辑,确保正常完成四次挥手:
import "time" import "io" func serverModeTCP(listenAddress string, confRemoteAddr string, filterMessage string) { listener, err := net.Listen("tcp", listenAddress) logError("failed to create local TCP listener", err, true) defer listener.Close() fmt.Printf("Server started, listening on TCP socket %s\n", listenAddress) recvBuffer := make([]byte, 1448) for { tcpConn, err := listener.Accept() if err != nil { logMessage(fmt.Sprintf("Failed: error accepting TCP connection: %v", err)) continue // tcpConn为nil时无需调用Close } recvDataLen, err := tcpConn.Read(recvBuffer) if err != nil { logMessage(fmt.Sprintf("Failed: reading packet payload resulted in error: %v", err)) tcpConn.Close() continue } remoteAddr := tcpConn.RemoteAddr() receivedText := recvBuffer[:recvDataLen] BreakLoop := ParsePayload(receivedText, remoteAddr.String(), confRemoteAddr, filterMessage) // 新增:设置超时并读取剩余数据,处理客户端FIN tcpConn.SetReadDeadline(time.Now().Add(2 * time.Second)) for { _, err := tcpConn.Read(recvBuffer) if err != nil { // 遇到EOF或超时,退出读取循环 break } } tcpConn.Close() if BreakLoop { break } } }
关键说明
tcpConn.Close()本身是正确的连接关闭方法,但仅当连接的读写状态都处理完成时,才能触发完整的四次挥手。如果服务器未读取到客户端的FIN就调用Close(),只会关闭本地写入通道,读取通道仍处于等待状态,导致TCP连接无法进入正常挥手流程。- 客户端的
FIN_WAIT2状态是因为客户端发送FIN后,一直未收到服务器的FIN,直到服务器进程被杀死才收到操作系统发送的FIN包。
内容的提问来源于stack exchange,提问作者bdrun33
相关产品推荐
相关产品推荐

