TCP 核心机制¶
学习目标¶
学完本章后,学习者应该能够:
- 理解 TCP 三次握手、四次挥手和状态机。
- 理解滑动窗口、拥塞控制、流量控制和重传机制。
- 区分粘包、拆包、keepalive、TIME_WAIT 和 CLOSE_WAIT。
- 能解释 Go 服务中的连接泄漏和超时问题。
TCP 解决什么问题¶
TCP 提供面向连接、可靠、有序的字节流传输。
它负责:
- 建立连接。
- 保证数据按序到达。
- 丢包后重传。
- 根据接收方能力做流量控制。
- 根据网络拥塞情况调节发送速度。
代价是状态更多、握手成本更高、头部更复杂。
三次握手与四次挥手¶
三次握手用于建立连接:
四次挥手用于关闭双向连接。TCP 是全双工,双方的读写方向可以分别关闭。
理解连接关闭很重要,因为 TIME_WAIT、CLOSE_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 持续增长的问题:
- 查看 TCP 连接状态。
- 找出目标远端地址。
- 检查代码是否关闭连接或响应体。
- 检查错误路径是否遗漏资源释放。
- 给出修复和监控方案。
参考答案
先用 ss -antp 或 netstat -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 持续增长通常说明应用没有正确关闭连接。