You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Go语言客户端发起的TCP四次挥手未完成问题排查与解决

TCP连接四次挥手未完成的问题排查与解决

问题描述

用Go开发的TCP客户端/服务器程序,支持UDP与TCP协议切换。客户端运行正常,但服务器在连接处理循环末尾调用tcpConn.Close()后,无法完成客户端发起的TCP四次挥手。抓包显示只有杀死服务器进程后,才会完成挥手的最后阶段,服务器端连接长期处于CLOSE_WAIT状态,客户端处于FIN_WAIT2状态。

原因分析

从抓包数据和代码逻辑来看,核心问题是服务器未处理客户端发送的FIN包:

  1. 客户端发送完数据后调用socket.Close(),会向服务器发送FIN包(对应抓包中第6条记录)。
  2. 服务器此时已经完成tcpConn.Read()读取客户端数据的操作,回到循环顶部调用listener.Accept()阻塞等待新连接,完全没机会处理这个FIN包。
  3. 服务器调用tcpConn.Close()时,TCP连接的读取通道仍处于等待客户端FIN的状态,因此Close()无法触发服务器发送FIN包,导致连接卡在CLOSE_WAIT。
  4. 只有当服务器进程被杀死时,操作系统会强制回收所有打开的文件描述符,才会触发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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 09:52:07