3、TCP协议详解:TCP报文段结构,三次握手与四次挥手,滑动窗口与流量控制,拥塞控制算法
TCP 协议,说白了就是分布式系统里最靠谱的那个老大哥。我做了这么多年分布式系统,见过无数网络协议来来去去,但 TCP 始终是那个「稳如狗」的存在。今天咱们就把它扒开看看,里面到底藏着什么门道。
3.1 TCP报文段结构
先看报文段。TCP 的报文段结构,我习惯把它分成两部分:头部和数据。头部固定 20 字节,后面可以跟选项。
| 字段 | 长度(位) | 说明 |
|---|---|---|
| 源端口 / 目的端口 | 16 / 16 | 标识通信的进程 |
| 序列号 | 32 | 本报文段数据的第一个字节序号 |
| 确认号 | 32 | 期望收到的下一个字节序号 |
| 数据偏移 | 4 | 头部长度,单位是 4 字节 |
| 标志位 | 6 | URG、ACK、PSH、RST、SYN、FIN |
| 窗口大小 | 16 | 接收方还能收多少数据 |
| 校验和 | 16 | 覆盖头部和数据 |
| 紧急指针 | 16 | 紧急数据的末尾位置 |
嗯,这里要注意:序列号和确认号是 TCP 可靠传输的核心。我见过不少新手搞混这两个字段。序列号是你自己发出去的字节位置,确认号是你期望对方下次发过来的字节位置。说白了,一个管「发」,一个管「收」。
核心要点:TCP 的序列号是字节流级别的,不是报文段级别的。这意味着每个字节都有自己的编号。我在项目中遇到过有人用报文段序号来追踪数据,结果丢包重传时全乱套了。
3.2 三次握手与四次挥手
三次握手,这可能是 TCP 最广为人知的部分了。但你真的理解它为什么是三次吗?
第一次握手:客户端发 SYN,序列号设为 x。
第二次握手:服务端回 SYN+ACK,序列号设为 y,确认号设为 x+1。
第三次握手:客户端发 ACK,序列号设为 x+1,确认号设为 y+1。
为什么不是两次?我举个例子你就明白了。假设客户端发了个 SYN,网络延迟了,客户端超时重发。结果第一个 SYN 又到了服务端。如果是两次握手,服务端就直接建立连接了。但客户端其实已经不需要了。三次握手可以避免这种「历史连接」的干扰。
避坑指南:我曾经在调优高并发服务时,发现大量 TIME_WAIT 状态的连接。后来才意识到,主动关闭连接的一方会进入 TIME_WAIT,持续 2MSL(最大报文段生存时间)。如果你的服务频繁主动关闭连接,端口会被占满。解决方案?要么让客户端主动关闭,要么调整 net.ipv4.tcp_tw_reuse 参数。
四次挥手就更有意思了。为什么是四次?因为 TCP 是全双工的,两边都要独立关闭。流程是这样的:
- 主动方发 FIN,表示「我不发了」
- 被动方回 ACK,表示「知道了」
- 被动方发 FIN,表示「我也不发了」
- 主动方回 ACK,表示「好的」
你想想看,第二步和第三步之间,被动方可能还有数据要发。所以不能合并。这就是四次挥手的本质原因。
3.3 滑动窗口与流量控制
滑动窗口,说白了就是接收方告诉发送方:「你慢点,我快撑不住了。」
接收方会在 TCP 头部里带上窗口大小字段。发送方根据这个值调整自己的发送速率。我刚开始做分布式存储时,觉得这玩意儿很简单。直到有一次,我们的数据节点因为接收缓冲区太小,导致发送方疯狂重传,整个集群的吞吐量直接腰斩。
滑动窗口的核心机制:
- 发送窗口 = min(拥塞窗口,接收窗口)
- 接收窗口由接收方通告,反映接收缓冲区剩余空间
- 发送方只能发送窗口内的数据
- 收到确认后,窗口向右滑动
注意:有一种情况叫「糊涂窗口综合征」。接收方每次只释放一个字节的缓冲区,就通告一个字节的窗口。发送方就发一个字节的数据。效率极低。解决方案是使用 Nagle 算法或延迟确认。我个人习惯在低延迟场景下关闭 Nagle 算法,在高吞吐场景下开启。
3.4 拥塞控制算法
流量控制是管「接收方能不能收」,拥塞控制是管「网络能不能传」。这是两个不同的维度。我见过很多人把这两个混为一谈,其实它们解决的问题完全不同。
3.4.1 慢启动
慢启动,名字叫「慢」,其实一开始是「快」的。拥塞窗口从 1 开始,每收到一个 ACK 就翻倍。指数增长,直到达到慢启动阈值。
为什么叫慢启动?因为相对于直接发一大波数据来说,它确实是从小到大慢慢试探的。我在项目中遇到过一个问题:慢启动阶段如果丢包,拥塞窗口会直接降到 1,然后重新开始。对于短连接来说,这简直是灾难。所以 HTTP 的 keep-alive 和连接池,本质上就是为了避免频繁慢启动。
3.4.2 拥塞避免
当拥塞窗口达到阈值后,进入拥塞避免阶段。增长方式从指数变成线性:每收到一个完整的窗口的 ACK,窗口加 1。说白了,就是「小心试探,稳步前进」。
为什么会这样?因为指数增长太猛了,很容易把网络撑爆。线性增长更温和,更容易找到网络的临界点。
3.4.3 快速重传
快速重传,我特别喜欢这个机制。它解决了超时重传效率低的问题。
当接收方收到乱序的报文段时,会立即发送重复的 ACK,告诉发送方「我缺了哪个包」。发送方收到 3 个重复的 ACK 后,不等超时计时器,直接重传丢失的包。
经验之谈:我曾经在调试一个跨机房同步服务时,发现延迟忽高忽低。抓包一看,大量重复 ACK 和快速重传。原因是中间链路的带宽被其他业务占满了。解决方案是给关键业务配置 QoS,或者使用多路径 TCP。
3.4.4 快速恢复
快速恢复是和快速重传配合使用的。当发生快速重传时,拥塞窗口不是降到 1,而是降到一半。然后进入拥塞避免阶段。
为什么不是降到 1?因为收到重复 ACK 说明网络还能传数据,只是偶尔丢了个包。如果降到 1,就太保守了。快速恢复的哲学是:「网络还行,别太怂。」
具体流程:
- 收到 3 个重复 ACK,阈值设为当前窗口的一半
- 拥塞窗口设为阈值 + 3(因为已经收到的重复 ACK 也算数据离开了网络)
- 每收到一个重复 ACK,窗口加 1
- 收到新数据的 ACK 后,窗口降到阈值,进入拥塞避免
嗯,这里要注意:快速恢复只在 Tahoe 和 Reno 版本中是这样。后来的 NewReno、CUBIC 等算法又有不同的实现。但核心思想没变:不要因为一次丢包就过度反应。
总结一下,TCP 的拥塞控制就像开车:慢启动是刚上路时慢慢加速,拥塞避免是到了限速后匀速行驶,快速重传是看到前车刹车灯立刻反应,快速恢复是轻踩刹车而不是急刹。这套机制保证了 TCP 在复杂的网络环境中依然能保持稳定和高效。
我在实际项目中,经常需要根据业务场景调整 TCP 参数。比如视频直播用 CUBIC 算法,金融交易用 BBR 算法。没有银弹,只有理解原理后的灵活运用。