TCP 是怎么做到不丢包的?6 个机制一次讲清
先说结论:TCP 并不能保证物理上"一个包都不丢"。
网络层(IP)本来就是尽力而为的,路由器队列满了照样丢包。
TCP 真正做到的是——只要连接还在,丢了的数据它能发现并补回来,最终交给应用层的字节流完整、有序、不重复。
这才是"可靠传输"的准确含义。
下面把支撑这件事的几个机制拆开讲,顺带纠正几个常见的误解。
校验和:先确认数据没坏
数据从一端跑到另一端,可能在链路上被干扰,某几个比特翻转了。
TCP 头部里有个 16 位的校验和(checksum),发送方算一遍填进去,接收方收到后再算一遍。
两边对不上,说明数据损坏,接收方直接丢弃,不发确认。
很多人以为序列号是用来保证数据完整性的,其实不是。
完整性靠的是校验和,序列号管的是顺序和去重。
这俩别搞混。
序列号 + 确认应答:每个字节都有编号
这是 TCP 可靠性的地基。
TCP 给发送的每一个字节都编了号,这个编号就是序列号(Sequence Number)。
注意,不是"每个包 seq 加 1"。
假设你发了一个 1000 字节的报文,起始序列号是 1,那下一个报文的序列号就是 1001,是按字节数往上累加的。
接收方收到数据后,回一个 ACK(确认号,Acknowledgment Number),告诉对方"我下一个想要的字节是几号"。
比如收到了 1 到 1000,它就回 ACK = 1001,意思是"1000 之前的我都收齐了,接着发 1001"。
这里有个细节叫累计确认:ACK = 1001 不只表示收到了第 1000 字节,而是表示 1000 及之前的全收到了。
所以偶尔丢一两个 ACK 也不要紧,后面的 ACK 能把前面的覆盖掉。
发送方 接收方
| --- seq=1, 1000字节 --> |
| <------- ACK=1001 ----- | 收到,下次要 1001
| --- seq=1001,1000字节-> |
| <------- ACK=2001 ----- |超时重传:等不到 ACK 就重发
发出去的数据,如果在规定时间内没等到对应的 ACK,发送方就认为它丢了,重新发一遍。
这个等待时间叫 RTO(Retransmission Timeout)。
RTO 不是写死的固定值。
TCP 会持续测量往返时间 RTT(Round-Trip Time),动态算出一个合适的 RTO。
网络慢的时候 RTO 自动变大,避免误判;网络快的时候缩小,丢包能更快补上。经典的算法是 Jacobson 提出的,用 RTT 的平滑均值加上偏差来估算。
如果重传后还是没收到 ACK,RTO 会翻倍(指数退避),第二次等更久,第三次更久。重传也有次数上限,Linux 默认 tcp_retries2 大概对应 15 次,反复重传都失败,连接就会被判定断开。
快速重传:不傻等超时
光靠超时重传有个问题——等 RTO 太慢了。于是有了快速重传。
场景是这样:发送方连续发了 1、2、3、4、5 号报文,其中 2 号丢了。接收方收到 1,回 ACK 期望 2;然后收到 3、4、5,但因为 2 没来,它每次都只能回"我还要 2"。
发送方一连收到 3 个重复的 ACK,不用等 RTO 超时,立刻把 2 号重发出去。
这一招能省下一大段等待时间,对实际吞吐影响挺大的。
还有个增强版叫 SACK(选择性确认),接收方能明确告诉发送方"3、4、5 我收到了,就缺 2",这样发送方只补 2 号,不用把 2 之后的全重发,省流量。
滑动窗口:别一股脑全发出去
如果发一个包就停下来等一个 ACK,那效率低得没法看。
滑动窗口让发送方可以一次性发出多个报文,不必逐个等待。
接收方在 ACK 里会带上一个接收窗口(rwnd),告诉发送方"我缓冲区还能装多少"。
发送方据此控制在途数据量,不会发到把对方缓冲区撑爆。
这其实就是流量控制——快的一方迁就慢的一方。
窗口为什么叫"滑动"?
随着 ACK 不断回来,已确认的数据移出窗口左边,新的数据从右边进来,整个窗口像一条履带往前挪。
拥塞控制:照顾整个网络
流量控制只管收发两端,拥塞控制管的是中间这张网。
如果大家都拼命发,路由器扛不住就开始丢包,越丢越重传,越重传越堵,最后雪崩。
TCP 为此维护一个拥塞窗口(cwnd),实际能发多少取 cwnd 和 rwnd 里更小的那个。
核心有四个阶段:
- 慢启动:刚开始 cwnd 很小,每收到一个 ACK 就翻倍式增长,快速探测网络容量
- 拥塞避免:cwnd 涨到一个阈值后,改成每个 RTT 才加 1,温和地试探
- 快速重传:收到 3 个重复 ACK,判定轻度丢包,立即重传
- 快速恢复:配合快速重传,把 cwnd 减半而不是直接清零,避免一遇到丢包就回到龟速
这套组合让 TCP 既能跑得快,又不会把网络压垮。
后来还有 CUBIC、BBR 等更现代的算法,但思路都是在"尽量快"和"别拥塞"之间找平衡。
关键机制一览
| 机制 | 解决什么问题 | 一句话作用 |
|---|---|---|
| 校验和 | 数据损坏 | 算一遍对不上就丢弃 |
| 序列号 + ACK | 丢失 / 乱序 / 重复 | 给每字节编号,累计确认 |
| 超时重传 | 数据丢了 | RTO 内没 ACK 就重发 |
| 快速重传 | 重传太慢 | 3 个重复 ACK 立刻补发 |
| 滑动窗口 | 收发速度不匹配 | 流量控制,不撑爆接收方 |
| 拥塞控制 | 网络整体拥堵 | 动态调 cwnd,避免雪崩 |
常见问题
TCP 真的能保证一个包都不丢吗?
不能。
物理丢包随时会发生,TCP 保证的是最终交付的字节流完整有序。
丢了的它能检测出来并重传补上,对应用层来说就像没丢过。
序列号和校验和有什么区别?
校验和判断数据有没有损坏,损坏了直接丢。
序列号负责排序和去重,保证乱序到达的数据能拼回正确顺序,重复的数据能被识别丢弃。
两者职责不同。
超时重传和快速重传哪个更快?
快速重传更快。
超时重传要傻等一个 RTO,而快速重传收到 3 个重复 ACK 就立即触发,省掉了等待时间。
实际网络里大部分丢包是靠快速重传补回来的。
流量控制和拥塞控制是一回事吗?
不是。
流量控制(滑动窗口 rwnd)只管接收方扛不扛得住,是收发两端的事;拥塞控制(cwnd)管的是中间网络堵不堵,是对整张网负责。最终发送量取两者的较小值。
UDP 为什么不保证不丢包?
UDP 没有这些机制,没有序列号、没有 ACK、不重传,发出去就不管了。
换来的是低延迟和低开销,适合直播、游戏、DNS 这类对实时性要求高、能容忍少量丢失的场景。
写在最后
我自己以前一直把"序列号保证完整性"挂在嘴边,后来翻 RFC 793 才发现是校验和在干这事,序列号其实管的是顺序。这种细节平时面试不一定问得到,但搞混了挺尴尬的。
这篇里的几个机制其实是配合着工作的:校验和挡住坏数据,序列号和 ACK 发现丢失,重传补回来,滑动窗口和拥塞控制控制节奏。
少了哪一个,可靠性都要打折扣。
如果哪里讲得不对或者你有更细的理解,欢迎在评论区交流~~~
版权声明
未经授权,禁止转载本文章。
如需转载请保留原文链接并注明出处。即视为默认获得授权。
未保留原文链接未注明出处或删除链接将视为侵权,必追究法律责任!
本文原文链接: https://fiveyoboy.com/articles/how-tcp-ensures-reliable-transmission/
备用原文链接: https://blog.fiveyoboy.com/articles/how-tcp-ensures-reliable-transmission/