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 是全双工的,两边都要独立关闭。流程是这样的:

  1. 主动方发 FIN,表示「我不发了」
  2. 被动方回 ACK,表示「知道了」
  3. 被动方发 FIN,表示「我也不发了」
  4. 主动方回 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,就太保守了。快速恢复的哲学是:「网络还行,别太怂。」

具体流程:

  1. 收到 3 个重复 ACK,阈值设为当前窗口的一半
  2. 拥塞窗口设为阈值 + 3(因为已经收到的重复 ACK 也算数据离开了网络)
  3. 每收到一个重复 ACK,窗口加 1
  4. 收到新数据的 ACK 后,窗口降到阈值,进入拥塞避免

嗯,这里要注意:快速恢复只在 Tahoe 和 Reno 版本中是这样。后来的 NewReno、CUBIC 等算法又有不同的实现。但核心思想没变:不要因为一次丢包就过度反应。

总结一下,TCP 的拥塞控制就像开车:慢启动是刚上路时慢慢加速,拥塞避免是到了限速后匀速行驶,快速重传是看到前车刹车灯立刻反应,快速恢复是轻踩刹车而不是急刹。这套机制保证了 TCP 在复杂的网络环境中依然能保持稳定和高效。

我在实际项目中,经常需要根据业务场景调整 TCP 参数。比如视频直播用 CUBIC 算法,金融交易用 BBR 算法。没有银弹,只有理解原理后的灵活运用。