一文看懂 TCP 协议:从三次握手到拥塞控制,一篇读懂这台“网络的可靠搬运工“

💡 如果你觉得 TCP 就是"三次握手、四次挥手"两句口诀,那这篇文章就是为你写的。TCP 的真正魅力,在于它如何用一套自适应的机制,在不可靠的网络上为你提供可靠的传输——而且这套机制今天仍在我们每一次刷网页、发消息、传文件时默默运转。


一、为什么要发明 TCP?——给不可靠网络"上保险"

IP 网络本质上是尽最大努力交付的:数据包可能丢失、乱序、重复、延迟。如果应用直接跑在 IP 上,一次简单的文件传输就可能因为某个包丢失而彻底出错。

TCP(Transmission Control Protocol,传输控制协议)的出现,就是为了解决一个问题:

如何在不可靠的网络上,提供可靠的、有序的、双向的字节流传输?

它做了五件事,构成了 TCP 的"可靠性金字塔":

  1. 面向连接:通信前先三次握手,确认双方收发能力
  2. 序列号与确认应答:每个字节都有编号,收不到就重传
  3. 流量控制:通过滑动窗口,防止"淹死"接收方
  4. 拥塞控制:通过网络反馈,防止"挤垮"网络
  5. 全双工字节流:双向独立传输,不保留报文边界

下面我们一层层拆开看。


二、三次握手:不只是"打招呼",更是"对暗号"

很多人背得住三次握手的报文序列,但未必说得出为什么偏偏是三次

完整的握手过程

客户端                                服务器
  |                                      |
  |-------- SYN, seq=x ---------------->|   进入 SYN_SENT
  |                                      |   进入 SYN_RCVD
  |<------- SYN+ACK, seq=y, ack=x+1 ----|   
  |                                      |
  |-------- ACK, ack=y+1 -------------->|   进入 ESTABLISHED
  |                                      |   进入 ESTABLISHED
  • 第一次 SYN:客户端发送 SYN=1,随机初始序列号 seq=x,进入 SYN_SENT 状态
  • 第二次 SYN+ACK:服务器回 SYN=1, ACK=1,自己的序列号 seq=y,确认号 ack=x+1,进入 SYN_RCVD 状态
  • 第三次 ACK:客户端发送 ACK=1,确认号 ack=y+1,双方进入 ESTABLISHED 状态

为什么不是两次?

这是面试官最爱问的问题。核心原因是:防止"已失效的连接请求"突然传到服务器,导致服务器误建连接、白白消耗资源

想象一个场景:客户端早年发过一个 SYN,因为网络拥堵滞留了很久。如果只有两次握手,服务器收到这个迟到 SYN 就会直接分配资源进入连接状态,但客户端早就放弃了这次请求——服务器资源就被浪费了。三次握手要求客户端再发一个 ACK 确认,迟到的 SYN 拿不到这个 ACK,服务器就不会建连。

📌 顺带一提:SYN 本身占用一个序列号;第三次握手的 ACK 如果不携带应用数据,不消耗新的数据字节序号。双方还在握手时报文里协商了 MSS(最大报文段长度)、窗口缩放因子、SACK、时间戳等关键参数。


三、TCP 怎么做到"可靠"?——序号、确认与重传

1. 序列号与确认号:字节流的"GPS 坐标"

TCP 面向字节流,每个字节都有唯一序列号。比如发送方发送 Seq=100 的段(假设这个段承载 100 字节数据),接收方收到后返回 ACK=200(表示已收到 100 及之前的所有数据,期望下次从 200 开始收)。

确认号采用累计确认ACK=n 表示 n 之前的字节都已正确接收。这种机制简单高效,但有个小瑕疵——如果中间丢了单个包,后面的包虽然到了,确认号也只能停在丢失处,这就是后面 SACK 要解决的问题。

2. 重传机制:丢包后的"补救方案"

TCP 有两种主要的重传触发方式:

超时重传(RTO)

为每个报文段设置计时器。发送方未在 RTO(Retransmission Timeout)时间内收到 ACK,就重发该段。RTO 的计算基于平滑 RTT(SRTT)和 RTT 方差,公式大致为:RTO = SRTT + 4 * RTTVAR。这是"最后兜底"的机制,等待时间较长(通常 200ms 起步)。

快速重传(Fast Retransmit)

如果接收方收到乱序段(比如先收到 102,没收到 101),它会连续发送重复 ACK=101。当发送方连续收到 3 个重复 ACK​ 时,立即重传 Seq=101,无需等待超时。这把恢复时间从"秒级"压缩到"毫秒级"。

3. SACK:精准重传的"升级版"

RFC 2018 定义的 SACK(Selective Acknowledgment)允许接收方精确告知"缺了哪几段"。比如一次丢了 3 个段,无 SACK 时要全部重传,有 SACK 时只重传丢的那 3 段。在高丢包率或长肥管道(高带宽长时延链路)场景下,效率提升显著。

⚠️ 如果握手 SYN 里没有 SACK permitted 选项,说明有一方不支持 SACK,高丢包场景下恢复效率会显著下降。


四、滑动窗口与流量控制:别把接收方"淹死"

如果发送方每发一段数据都必须等 ACK 才能发下一段,效率极低。TCP 用滑动窗口解决这个问题:允许发送方在窗口范围内连续发送多段未确认的数据。

发送窗口的四个区域

| 已发送已确认 | 已发送未确认 | 允许发送未发送 | 不允许发送 |
            ^                               ^
        窗口左沿                        窗口右沿

随着接收端不断返回确认,窗口向前滑动——这就是"滑动窗口"名称的由来。

rwnd:接收方说了算的窗口

接收方通过 TCP 首部的"窗口大小"字段通告自己的接收窗口(rwnd)——也就是接收缓冲区还剩多少空间。发送方根据 rwnd 调整发送速率,防止接收方缓冲区溢出。

零窗口与窗口探测:当接收方应用消费数据慢(比如数据库查询卡住、磁盘 IO 瓶颈),rwnd 会逐渐减小到 0。此时发送方暂停发送,并周期性发送零窗口探测报文,等接收方缓冲区释放后通过窗口更新通告恢复发送。

💡 抓包时看到 tcp.analysis.zero_window别甩锅给网络——这通常意味着接收端应用消费太慢,是应用层瓶颈的信号。

窗口缩放:突破 64KB 天花板

TCP 窗口字段只有 16 bit,最大 65535 字节。对大带宽长时延链路远远不够。RFC 7323 的窗口缩放选项(Window Scale)在握手中协商一个缩放因子,把窗口最多放大 256 倍(shift=7 时窗口 ×128,最大可达 8MB)。

单连接理论吞吐 ≈ 窗口大小 / RTT。这就是为什么跨洋传输需要窗口缩放——否则带宽再大也跑不满。


五、拥塞控制:别把网络"挤垮"

流量控制保护的是接收方,拥塞控制保护的是网络本身。即使接收方缓冲区充裕,发送方也不能无限制向网络注入数据——中间路由器、交换机的队列容量有限,过量数据会导致排队、时延飙升甚至丢包。

TCP 用拥塞窗口(cwnd)​ 控制发送到网络中的数据规模。发送方实际可发数据量 = min(rwnd, cwnd) - 已发送未确认

经典 AIMD 模型(以 Reno/CUBIC 为代表)

阶段

触发条件

cwnd 变化

类比

慢启动

连接初始/超时后

指数增长(每 RTT 翻倍)

试探水温

拥塞避免

cwnd > ssthresh

线性增长(每 RTT +1 MSS)

稳步加码

快速重传

收到 3 个重复 ACK

立即重传丢失段

不等超时

快速恢复

快速重传后

cwnd 减半后线性增长

刹车再加速

核心思想:AIMD 原则——加性增(Additive Increase)、乘性减(Multiplicative Decrease)。网络通畅时小心翼翼地加速,一旦检测到拥塞就果断砍半,避免雪崩。

抓包里"看见"拥塞控制

在 Wireshark 中,你可以通过以下路径观察:

  • 统计 → TCP 流图 → 时间序列(Stevens):看序列号随时间增长
    • 阶梯式上升 → 慢启动爬坡
    • 锯齿状波动 → 拥塞控制循环
    • 突然塌陷 → 丢包/超时重传
  • 统计 → TCP 流图 → 往返时间:看 RTT 变化
    • 平稳 → 链路健康
    • 周期性尖峰 → 拥塞排队
    • 持续上升 → 链路劣化

现代算法 BBR:Google 的"另辟蹊径"

传统 Reno/CUBIC 依赖"丢包=拥塞"的信号,但在高带宽网络上,等丢包才减速已经太晚。Google 2016 年提出的 BBR(Bottleneck Bandwidth and RTT)直接测量带宽和延迟来建模,不再把丢包作为减速信号。Linux 4.9+ 可配置,谷歌云默认启用。

⚠️ BBR 的排障陷阱:启用 BBR 后,即使链路丢包,吞吐也可能保持高位。如果你习惯用"丢包就掉速"来判断拥塞,在 BBR 环境下会误判。


六、四次挥手:全双工的"礼貌告别"

TCP 是全双工的——两个方向的数据流相互独立。所以关闭连接时,每个方向都要单独关闭,这就是四次挥手

主动关闭方                             被动关闭方
  |                                      |
  |-------- FIN, seq=u ---------------->|   主动方: FIN_WAIT_1
  |                                      |   被动方: CLOSE_WAIT
  |<------- ACK, ack=u+1 ---------------|   主动方: FIN_WAIT_2
  |                                      |
  |                             (被动方继续发剩余数据)
  |                                      |
  |<------- FIN, seq=v -----------------|   被动方: LAST_ACK
  |                                      |
  |-------- ACK, ack=v+1 -------------->|   主动方: TIME_WAIT
  |                                      |   被动方: CLOSED
  |                                      |
  |   等待 2MSL 后 CLOSED                |

为什么需要 TIME_WAIT?为什么是 2MSL?

主动关闭方在发出最后一个 ACK 后,不立即关闭,而是进入 TIME_WAIT 状态等待 2MSL(Maximum Segment Lifetime,报文最大生存时间):

  1. 确保最后一个 ACK 能到达被动方:如果 ACK 丢失,被动方会重发 FIN,主动方在 TIME_WAIT 期间还能再回一个 ACK
  2. 让网络中残留的旧报文失效:2MSL 时间足以让本次连接产生的所有报文在网络中消失,防止新连接(同四元组)收到旧数据

七、实战:用 Wireshark 抓包看穿 TCP

光说不练假把式。当用户报"网络慢"时,怎么用 TCP 知识定位问题?

1. 一键找出所有异常

Wireshark 的 tcp.analysis 系列字段是你的好朋友:

tcp.analysis.flags          # 一键找出所有 TCP 异常
tcp.analysis.retransmission # 超时重传(链路长时间无响应)
tcp.analysis.fast_retransmission # 快速重传(随机丢包)
tcp.analysis.zero_window    # 零窗口(接收端应用慢)
tcp.analysis.duplicate_ack  # 重复 ACK(丢包前的信号)
tcp.analysis.out_of_order   # 乱序(多路径/负载均衡)

2. 统计重传率

# 统计抓包文件中的重传次数
tshark -r cap.pcap -Y "tcp.analysis.retransmission" | wc -l

经验阈值

  • 重传率 < 1% → 正常范围
  • 重传率 > 1% → 基本可判定链路劣化

3. 慢速问题排查决策树

用户报"慢"时,按这个顺序排查:

  1. 看握手:SYN → SYN-ACK 的延迟是否过大?过大说明网络路由或防火墙策略导致基础延迟
  2. 看请求处理:客户端发请求 PSH+ACK 到服务器开始发响应的时间间隔。如果间隔很长(比如 2 秒),说明服务器应用层处理慢(复杂查询、数据库锁、GC 停顿等)
  3. 看传输阶段:响应数据传输中是否有 retransmission?有则说明传输阶段丢包
  4. 看接收窗口:是否有 zero_window?有则说明客户端消费慢
  5. 看 RTT 曲线:RTT 持续上升 → 中间设备排队严重;RTT 突变 → 路由切换

📌 一个真实案例:用户反馈海外节点下载文件特别慢。抓包后发现握手正常,请求处理也快,但传输阶段出现大量 fast_retransmission 且 RTT 周期性尖峰——最终定位为跨国链路拥塞,通过调整 TCP 拥塞控制算法(切换到 BBR)解决。

4. 关键抓包指标速查

现象

Wireshark 标识

可能原因

链路随机丢包

大量 fast_retransmission

物理链路质量差

长时间无响应

大量 retransmission(RTO)

链路拥塞或对端假死

应用消费慢

zero_window

数据库/磁盘 IO 瓶颈

多路径乱序

out_of_orderdup_ack

负载均衡策略

回程路径丢包

发送端触发 RTO,接收端已收到

反向链路质量差


八、工程师必备:TCP 常见误区澄清

误区 1:TCP 保证数据绝对不丢

❌ TCP 的"可靠"是指:通过重传机制尽可能把数据送达,达到最大重试次数仍失败则通知应用层。不是物理意义上的"永不丢失",而是"丢了会重传,传不了会报错"。

误区 2:三次握手是多余的,两次就够了

❌ 如前所述,两次握手无法防止已失效的连接请求导致服务器误建连接、浪费资源。

误区 3:收到重复 ACK 就一定丢包了

❌ 不一定。乱序到达也会触发重复 ACK。需要结合 out_of_orderzero_window 等字段综合判断。

误区 4:TIME_WAIT 是 Bug,应该尽量避免

❌ TIME_WAIT 是 TCP 协议正确性的重要保障。盲目通过 tcp_tw_reusetcp_tw_recycle(后者已废弃)来"优化"可能引入新连接收到旧数据的 bug。正确做法是增大 net.ipv4.tcp_max_tw_buckets 或调整应用层连接复用。

误区 5:窗口越大越好

❌ 窗口大小需要与 RTT 匹配。单连接理论吞吐 ≈ 窗口大小 / RTT。盲目放大窗口但 RTT 很大,反而会增加丢包时的重传代价。


九、TCP 的优缺点与适用场景

优点

  • ✅ 数据可靠传输,保证顺序
  • ✅ 动态流量控制,自适应拥塞控制
  • ✅ 广泛兼容,几乎所有网络应用都基于 TCP

缺点

  • ❌ 连接建立有三次握手开销
  • ❌ 首部较大(最小 20 字节)
  • ❌ 队头阻塞:一个包丢失,后续包即使到达也要等待重传
  • ❌ 高延迟网络效率低
  • ❌ 缺乏原生的多路复用支持

适用场景

  • TCP 适合:Web 浏览(HTTP/HTTPS)、文件传输(FTP)、邮件(SMTP/POP3/IMAP)、远程访问(SSH)、数据库访问——任何需要可靠传输的场景
  • UDP 适合:视频流、实时游戏、DNS 查询——任何对实时性要求高、可容忍少量丢包的场景

💡 现代 HTTP/3 选择基于 UDP 的 QUIC 协议,正是为了规避 TCP 的队头阻塞问题,同时通过 QUIC 自身实现可靠性——这是 TCP 在现代网络环境下的一个有趣演进。


写在最后:TCP 的哲学

TCP 设计的精髓,在于它承认一个事实:网络是不可靠的,但我们可以通过聪明的机制在上面构建可靠性

它没有假定网络永远是好的,也没有在丢包时直接放弃,而是用确认、重传、窗口、拥塞控制这一套组合拳,在"激进发送"和"保守退让"之间找到动态平衡。这种"基于反馈的自适应"思想,不仅适用于网络协议,也适用于分布式系统、流控算法,甚至我们日常的工程决策。

下次当你 curl 一个接口、git clone 一个仓库、scp 传一个文件时,不妨想一想:在你看不见的地方,TCP 正在用三次握手问好,用滑动窗口调速,用拥塞控制避让,用重传机制兜底——稳稳地把每一个字节送到对端。

这才是真正的"网络的可靠搬运工"。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值