跳转至

TCP 核心机制

学习目标

学完本章后,学习者应该能够:

  1. 理解 TCP 三次握手、四次挥手和状态机。
  2. 理解滑动窗口、拥塞控制、流量控制和重传机制。
  3. 区分粘包、拆包、keepalive、TIME_WAIT 和 CLOSE_WAIT。
  4. 能解释 Go 服务中的连接泄漏和超时问题。

TCP 解决什么问题

TCP 提供面向连接、可靠、有序的字节流传输。

它负责:

  • 建立连接。
  • 保证数据按序到达。
  • 丢包后重传。
  • 根据接收方能力做流量控制。
  • 根据网络拥塞情况调节发送速度。

代价是状态更多、握手成本更高、头部更复杂。

三次握手与四次挥手

三次握手用于建立连接:

Client -> SYN -> Server
Client <- SYN+ACK <- Server
Client -> ACK -> Server

四次挥手用于关闭双向连接。TCP 是全双工,双方的读写方向可以分别关闭。

理解连接关闭很重要,因为 TIME_WAITCLOSE_WAIT 都和关闭过程有关。

TCP 是字节流

TCP 不保留应用消息边界。应用层一次 Write,接收方不一定一次 Read 到完整消息。

这就是粘包和拆包问题的来源。解决方式在应用协议层:

  • 固定长度。
  • 分隔符。
  • 长度前缀。
  • 使用 HTTP、gRPC 等成熟协议。

TIME_WAIT 与 CLOSE_WAIT

TIME_WAIT 通常出现在主动关闭连接的一方,用来等待旧包消失。

CLOSE_WAIT 表示对端已经关闭连接,本端应用还没有关闭连接。持续增长通常是应用代码没有释放连接。

Go 后端实际应用例子

例子一:TCP 协议要自己处理消息边界

func writeMessage(w io.Writer, payload []byte) error {
    var header [4]byte
    binary.BigEndian.PutUint32(header[:], uint32(len(payload)))
    if _, err := w.Write(header[:]); err != nil {
        return err
    }
    _, err := w.Write(payload)
    return err
}

长度前缀是一种常见做法。接收方先读 4 字节长度,再读指定长度的数据。

例子二:HTTP Client 必须关闭响应体

resp, err := client.Do(req)
if err != nil {
    return err
}
defer resp.Body.Close()

_, err = io.Copy(io.Discard, resp.Body)
return err

关闭并读取响应体有助于释放连接或复用连接。忘记关闭可能导致 CLOSE_WAIT 增多和文件描述符泄漏。

常见误区

  • 误区一:TCP 不会丢数据,所以应用不用处理错误。

TCP 会尽力重传,但连接仍可能超时、重置、中断。应用必须处理错误和超时。

  • 误区二:一次 Write 对应一次 Read。

TCP 是字节流,不保留消息边界。应用层协议必须自己定义消息边界。

  • 误区三:TIME_WAIT 多就一定要调内核参数。

先判断是否大量短连接、连接复用不足或关闭方向不合理。不要一上来改内核参数。

线上问题案例

某服务调用下游 HTTP API 后只读取部分响应,没有关闭 body。运行一段时间后 CLOSE_WAIT 连接持续增长,最终文件描述符耗尽。

修复方式是确保所有 client.Do 成功返回的路径都关闭 body,并为 HTTP Client 设置超时和连接池参数。

实战任务

排查一个 Go 服务 CLOSE_WAIT 持续增长的问题:

  1. 查看 TCP 连接状态。
  2. 找出目标远端地址。
  3. 检查代码是否关闭连接或响应体。
  4. 检查错误路径是否遗漏资源释放。
  5. 给出修复和监控方案。
参考答案

先用 ss -antpnetstat -ano 查看连接状态和 PID,确认是否是目标进程的 CLOSE_WAIT 持续增长。然后根据远端地址判断是哪类下游调用。Go 代码里重点检查 HTTP Client 是否在所有成功返回响应的路径 defer resp.Body.Close(),TCP 连接是否在退出时关闭。

修复后应增加文件描述符数量、TCP 状态分布、下游错误率和超时指标。必要时在测试环境写压测脚本复现,确认连接数不会持续上涨。

面试题

1. TCP 为什么需要三次握手?

参考答案

三次握手用于确认双方收发能力和初始序列号。第一次客户端发送 SYN,第二次服务端返回 SYN+ACK,第三次客户端返回 ACK。

如果只有两次,服务端无法确认客户端是否收到自己的 SYN+ACK,也更容易受到历史连接包影响。三次握手能让双方都确认连接状态。

2. TCP 粘包和拆包是什么?

参考答案

TCP 是字节流协议,不保留应用消息边界。发送方多次写入的数据可能被接收方一次读到,看起来像粘包;发送方一次写入的数据也可能被接收方多次读到,看起来像拆包。

解决方式是在应用协议层定义边界,例如长度前缀、分隔符、固定长度,或直接使用 HTTP、gRPC 等成熟协议。

3. TIME_WAIT 和 CLOSE_WAIT 有什么区别?

参考答案

TIME_WAIT 通常出现在主动关闭连接的一方,用来等待旧包在网络中消失,是 TCP 正常机制。CLOSE_WAIT 表示对端已经关闭连接,本端应用还没有关闭。

TIME_WAIT 多要看是否导致端口或资源压力;CLOSE_WAIT 持续增长通常说明应用没有正确关闭连接。