TCP 基础:可靠传输是如何实现的
上一篇讲 UDP 时我们说,它把「要不要可靠」这个问题原样甩给了应用。这一篇的主角 TCP,则是另一种哲学的极致——它主动把丢包、乱序、重复、拥塞这些底层世界的脏活全揽下来,向应用呈现出一条「像可靠管道一样」的字节流。底层网络其实一点没变好,TCP 只是用一整套机制,把这些现实问题尽量藏在了协议内部。
很多人学 TCP,张口就是「三次握手、四次挥手」,仿佛 TCP 就等于建连和断连。其实握手只是 TCP 的入口仪式(那部分我们在下一篇《TCP 连接管理》里专门拆状态机和序列号变化,这里不重复),TCP 真正的主体,是连接建立之后那段「可靠传输」是怎么运转的。所以这一篇我们聚焦总览:先把可靠传输的几大机制——序列号与确认、超时重传、滑动窗口、按序交付、流量控制、拥塞控制——分别在「对抗哪一种不可靠」这个角度上讲清运转方式,再回头解释 TCP 为什么要把它们组合成这么一套系统。
学 TCP 最关键的不是背机制的名字,而是理解:每一个机制,分别是在堵「丢包 / 乱序 / 重复 / 接收方过载 / 网络过载」这五个洞里的哪一个。
一、不可靠到底体现在哪几方面
要理解 TCP 的机制,得先把它要对抗的「不可靠」拆开。底层 IP 网络不保证什么?具体有五种现实问题:
- 丢包:链路拥塞、路由器缓存溢出,包可能凭空消失;
- 乱序:不同包可能走不同路径,后发的先到;
- 重复:重传、网络异常可能让同一份数据到达两次;
- 接收方处理不过来:发太快,接收方缓冲区被打爆;
- 网络处理不过来:发太快,把中间链路挤垮,引发更严重拥塞。
TCP 的全部可靠机制,本质上就是逐一封堵这五个洞。把每个机制对应到它堵的洞,TCP 就不再是一团乱麻,而是一套各司其职的系统。
二、字节流:理解一切的起点
在讲机制前,必须先建立一个最容易被忽略、却决定性的前提——TCP 是字节流(byte stream),不是消息流。
TCP 眼里没有「一条消息」的概念,它看到的是一串连续编号的字节。你调用一次 send 发了 100 字节,内核可能把它和下一次的数据合并成一个段发出,也可能拆成两个段;对方一次 recv 可能收到 30 字节、也可能一次收到 200 字节。应用写入的次数、内核分段的方式、对方读取的次数,三者完全不对应。
这就是大名鼎鼎的「粘包 / 拆包」——其实这俩词都不准确,根源就一句话:TCP 本来就不保证消息边界。所以:
任何基于 TCP 的应用层协议,都必须自己定义消息边界——要么用长度字段(先发 4 字节表示后面消息多长),要么用分隔符(如 HTTP 用
\r\n)。指望「一次 send 对应一次 recv」写出来的网络程序,迟早出 bug。
为什么 TCP 选字节流而不是消息流?因为字节流更通用、更利于它做缓冲和分段优化(比如把小数据攒一攒一起发)。代价就是把「划分消息」的责任交给了应用——这又是一个典型的「机制选择带来取舍」。
三、序列号与确认:可靠的地基
TCP 可靠性的最底层基石,是给每个字节编号 + 确认。这套机制同时对付「丢包」和「乱序」。
- 序列号(seq):TCP 给字节流里的每一个字节都编了号。每个段的首部带一个序列号,表示「这个段携带的数据,第一个字节在整个字节流里的编号」。
- 确认号(ack):接收方用确认号告诉发送方「我已经连续收到了哪些字节」。确认号 = 期望收到的下一个字节的编号。这是累积确认:ack=1001 意味着「1000 及之前的字节我全收齐了,请从 1001 开始发」。
举例:发送方发了 seq=1、长 500 字节的段,接收方收齐后回 ack=501(「501 之前都收到了」)。发送方一看,知道这 500 字节安全送达。靠序列号,接收方能识别重复(编号重了就丢)、能发现乱序(编号不连续就先存着);靠确认号,发送方能知道哪些数据对方收到了。 丢包和乱序这两个洞,地基就由它们堵上。
四、超时重传与快速重传:丢了怎么补
光有确认还不够,还要有「发现丢失并补发」的机制——这就是重传,专堵「丢包」洞。
- 超时重传(RTO):发送方发出一个段后启动一个计时器。如果在**重传超时(RTO)时间内没等到对应的确认,就认为它丢了,重发。RTO 不是固定值,TCP 会动态测量 RTT(往返时延)**并据此估算——网络快就调小、网络慢就调大,避免误判或等太久。
- 快速重传:等超时太慢了。TCP 还有个更灵敏的机制:如果发送方连续收到 3 个重复的 ACK(对方反复说「我还在等 1001」),说明 1001 这个段很可能丢了(但后面的段到了,所以对方才一直在催 1001),于是不等超时,立即重传。
重传是 TCP「可靠」二字最直接的体现:丢了就补,直到对方确认收到。代价是丢包会带来延迟——这也是为什么实时场景宁可用 UDP 丢掉也不等重传。
五、滑动窗口:别一次只发一个
如果发一个段就停下来等确认(停等协议),效率低到无法接受——每个段都要白等一个 RTT。TCP 用**滑动窗口(sliding window)**解决这个问题:允许发送方在没收到确认前,连续发出一批数据。
把字节流想象成一条长纸带,窗口是盖在上面的一个框。框内的字节可以「在途」(已发出但还没确认),框左边是「已确认」的,框右边是「还不能发」的。随着确认不断回来,窗口整体向右滑动,新的字节进入可发送范围。
滑动窗口让 TCP 能「流水线式」地连续发送,把一个 RTT 内能传的数据量从「一个段」放大到「一整个窗口」,这是 TCP 能跑出高吞吐的关键。窗口越大、RTT 越小,吞吐越高(吞吐 ≈ 窗口 / RTT)。
窗口的大小不是发送方拍脑袋定的,它同时受两个因素夹击——接收方能力和网络能力,这就引出下面两个机制。
六、流量控制:别淹没接收方
滑动窗口的大小,首先受接收方约束,这就是流量控制,专堵第 4 个洞「接收方处理不过来」。
机制很直接:接收方在每个 ACK 的首部里带一个**接收窗口(rwnd)**字段,告诉发送方「我的接收缓冲区现在还能收多少」。发送方的发送窗口不能超过这个 rwnd。如果接收方应用读取太慢、缓冲区快满了,它就把 rwnd 报得越来越小,直到为 0——发送方一看窗口为 0,就停下来不发了(之后靠零窗口探测包定期试探,等接收方腾出空间再继续)。
流量控制是端到端的:它只解决「发送方别淹没接收方」这一对一的问题,靠接收方主动通告自己的接纳能力来反向刹车。
七、拥塞控制:别压垮网络
光不淹没接收方还不够——你的接收方很能扛,但中间网络可能扛不住。拥塞控制专堵第 5 个洞「网络处理不过来」,它是 TCP 最精妙、也最复杂的部分。
核心思路:发送方自己再维护一个拥塞窗口(cwnd),实际能发的数据 = min(接收窗口 rwnd, 拥塞窗口 cwnd)——两个约束取更小的那个。cwnd 的大小靠「探测 + 反馈」动态调整,经典阶段有:
- 慢启动:连接刚建立,cwnd 从很小开始,每个 RTT 翻倍,指数级快速试探网络容量;
- 拥塞避免:cwnd 涨到一定阈值后,改成每个 RTT 只加一点(线性增长),小心地继续试探;
- 遇到丢包就退避:TCP 把丢包当作网络拥塞的信号(这是经典 TCP 的核心假设)。一旦丢包,就大幅调小 cwnd(具体策略因算法而异,如 Reno、CUBIC、BBR 各不同),然后重新慢慢爬升。
拥塞控制的哲学是「慢慢试探、出事就退」:没人告诉 TCP 网络此刻能承受多少,它只能自己一点点加速、把丢包当成「踩到上限」的信号然后回退。无数条 TCP 连接各自这么自律,整个互联网才不会因为大家拼命发包而集体雪崩。
注意区分:流量控制是为接收方服务(rwnd),拥塞控制是为网络服务(cwnd),两者独立、共同决定实际发送速率,别搞混。
八、把机制拼成一张图:TCP 是怎么「可靠」的
现在把六大机制按「堵哪个洞」归位,TCP 这套系统就清晰了:
- 序列号 + 累积确认 → 对付丢包、乱序、重复(地基);
- 超时重传 + 快速重传 → 补回丢失的数据;
- 接收端缓存乱序段、按序号重排后交付 → 保证应用拿到的是有序字节流;
- 滑动窗口 → 流水线发送,提升吞吐;
- 流量控制(rwnd) → 不淹没接收方;
- 拥塞控制(cwnd) → 不压垮网络。
TCP 的「可靠」从来不是某一个炫技的技巧,而是这一堆机制咬合在一起、持续运转、持续兜底的结果。它像一个长期值守的交通调度系统:不断观察(测 RTT、收 ACK)、不断调整(滑窗、调 cwnd)、不断补救(重传)。理解 TCP 的最好方式,永远是回问那句话——这个机制是在补哪一个洞。
九、工程视角:为什么后端 / SRE 必须吃透 TCP
网页、数据库连接、RPC、消息队列客户端、服务网格 sidecar……几乎所有基础设施都架在 TCP 上。排障时只盯应用日志、不懂 TCP,效率会很低:
- 吞吐上不去:吞吐 ≈ 窗口 / RTT。跨地域高 RTT 链路下,如果窗口(受 rwnd 或内核
tcp_wmem/rmem限制)太小,吞吐就被锁死——这就是「带宽时延积」问题,要调大缓冲区或开窗口扩展选项。 - 重传率高:
ss -ti/netstat -s能看到重传次数。重传率高通常意味着链路丢包,往往是网络问题而非应用问题,别在代码里瞎找。 - 延迟毛刺:Nagle 算法(攒小包)和延迟确认(Delayed ACK)凑一起可能造成意外延迟,小包交互场景常需
TCP_NODELAY关掉 Nagle。 - 连接状态排查:
CLOSE_WAIT堆积、TIME_WAIT过多这些状态机层面的问题,属于连接管理范畴——下一篇《TCP 连接管理:三次握手与四次挥手》会专门拆状态机讲透,这里先记住「状态能告诉你连接卡在哪一步」。
十、学习这一部分最容易踩的坑
1. 以为 TCP = 三次握手
握手只是 TCP 的入口。TCP 的主体是连接建立后的可靠传输机制(序列号、重传、滑窗、拥塞控制)。只会背握手,等于只认识了门口。
2. 把流量控制和拥塞控制搞混
流量控制靠 rwnd,保护接收方;拥塞控制靠 cwnd,保护网络。两者独立,实际发送量取二者更小值。混为一谈,调优就会找错方向。
3. 忽略 TCP 是字节流
「一次 send 对应一次 recv」是错的。TCP 不保证消息边界,应用必须自己用长度字段或分隔符划分消息。这是新手网络编程最高频的 bug 来源。
4. 以为「可靠」=「绝对送达」
TCP 是「相对可靠」:它尽最大努力对抗不稳定,但网络彻底断了,它也会超时失败、断开连接。它降低的是出错概率,不是把概率清零。
总结
这一篇我们做了可靠传输的总览,坚持「每个机制对应它堵的那个洞」:
- TCP 把不可靠网络包装成相对可靠的字节流服务,所以应用必须自己划分消息边界;
- 序列号 + 累积确认是地基,对付丢包、乱序、重复;
- 超时重传与快速重传负责补回丢失的数据;
- 滑动窗口让发送方流水线式连续发包,是高吞吐的关键,吞吐 ≈ 窗口 / RTT;
- 流量控制(rwnd)防止淹没接收方,拥塞控制(cwnd)防止压垮网络,二者独立、取小值生效;
- 拥塞控制靠「慢启动 → 拥塞避免 → 丢包退避」自律地试探网络容量,是互联网不雪崩的基石。
当你能把任意一个 TCP 机制都准确归位到「它在堵丢包 / 乱序 / 重复 / 接收方过载 / 网络过载哪个洞」,可靠传输这部分就真正属于你了。下一篇,我们顺着进入连接的入口与出口——三次握手与四次挥手,把那个 SYN/FIN 驱动的状态机拆开看。
参考资源:
- 《计算机网络:自顶向下方法》
- 《TCP/IP 详解 卷一:协议》
- RFC 9293 - Transmission Control Protocol (TCP)
- RFC 5681 - TCP Congestion Control