TCP 可靠传输:滑动窗口、重传、流量控制与拥塞控制
一、先把这几个概念串起来
学 TCP 的时候,最容易乱掉的不是某一个知识点本身,而是这些概念老是缠在一起:
- 滑动窗口
- 发送窗口
- 接收窗口
- 重传
- 流量控制
- 拥塞控制
- 慢启动
- 快速重传
- 快速恢复
如果不先把主线抓住,很容易背一堆术语,最后还是讲不顺。
我自己的理解是:TCP 想做可靠传输,就必须回答四个问题。
- 数据丢了怎么办?
- 不想傻等 ACK,怎么提高发送效率?
- 接收方处理不过来怎么办?
- 网络本身快堵了怎么办?
对应起来就是:
- 重传:解决“丢了怎么办”
- 滑动窗口:解决“怎么提高发送效率”
- 流量控制:解决“接收方吃不消怎么办”
- 拥塞控制:解决“网络吃不消怎么办”
后面的很多机制,其实都围绕这四件事展开。
二、为什么 TCP 不能“发一个等一个”
如果 TCP 采用最笨的做法:
- 发一个报文段
- 停下来等 ACK
- ACK 回来后再发下一个
那在 RTT 比较大的网络里,效率会非常低。
比如:
- 一次只发 1KB
- RTT 是 100ms
那么发送方很多时间都在“等确认”,链路其实没有被充分利用。
这就像你搬货时,一次只搬一箱,然后站着等对方点头,再搬下一箱。动作是对的,但效率太差。
所以 TCP 不能只追求“可靠”,还得想办法把链路跑起来。滑动窗口就是在这个背景下出现的。
三、滑动窗口到底在做什么
1. 核心思想
滑动窗口的核心思想很简单:
在还没有收到 ACK 之前,允许发送方连续发送多个未确认的数据。
也就是说,发送方不必发一段等一段,而是可以在窗口范围内先把数据发出去,让这些数据在网络里“飞着”。
2. 一个简单例子
假设当前发送窗口允许发送 4 个 MSS 的数据,那么发送方可以连续发:
1-1000
1001-2000
2001-3000
3001-4000如果接收方返回:
ACK = 2001说明 1-2000 这部分已经确认收到,那么发送窗口左边界就可以向前移动,继续发送新的数据:
4001-5000
5001-6000这就是“窗口滑动”。
3. 它为什么叫“滑动”
因为窗口不是固定死的。随着 ACK 到来:
- 窗口左边界向前移动
- 窗口右边界也可能跟着向前移动
于是发送方就能不断把新数据放进可发送区域。
4. 一个提醒
提醒:滑动窗口不是“收到一个 ACK 就往前移动一格”。
TCP 是按字节编号的,不是按“包”编号的。
所以窗口前移多少,取决于 ACK 确认到了哪个字节位置,而不是固定挪一个格子。
四、先区分三个很容易混的窗口
这一块特别容易混。很多人一提“窗口”,脑子里就糊成一团了。
1. 发送窗口
发送窗口可以理解成:
当前发送方被允许发送出去的数据范围。
它不是单独某一个字段,而是综合约束后的结果。
2. 接收窗口 rwnd
接收窗口是接收方维护的,它会告诉发送方:
“我现在还能接收多少数据,你别发太猛。”
它主要反映的是:
- 接收缓冲区还剩多少空间
- 应用程序读取数据的速度
3. 拥塞窗口 cwnd
拥塞窗口是发送方维护的,它表示:
“根据我对当前网络情况的判断,我最多允许多少未确认数据同时在网络中飞行。”
它反映的不是接收方能力,而是网络承载能力。
4. 三者之间的关系
日常面试里最常见的表达是:
发送方实际发送窗口 = min(rwnd, cwnd)这句话本身没问题,足够应付大多数场景。
如果再说严谨一点:
当前还能继续发送多少新数据,还要再扣掉“已经发出去但还没确认的在途数据”。
不过在学习阶段,先记住下面这句就够用了:
rwnd控制“别把接收方撑爆”cwnd控制“别把网络打爆”- 真正能发多少,要看这两者里更小的那个
5. 一个提醒
提醒:
rwnd不是发送方维护的,cwnd也不是接收方维护的。
这个地方面试里说反了会比较伤。
五、MSS 到底是什么
在讲窗口和拥塞控制时,经常会看到 MSS。
MSS,Maximum Segment Size,最大报文段大小。
它表示的是:
一个 TCP 报文段中,最多能放多少字节的有效数据。
注意,这里说的是 TCP 数据部分,不包括 IP 头和 TCP 头本身。
比如常见以太网环境里:
- MTU = 1500 字节
- IP 头 = 20 字节
- TCP 头 = 20 字节
那么通常:
MSS = 1500 - 20 - 20 = 1460 字节所以很多地方写:
cwnd = 10 MSS
意思就是:
- 当前大概允许 10 个 MSS 大小的数据在网络中未被确认
关于 MSS 的提醒
提醒:MSS 和 MTU 不是一个东西。
MTU更偏链路层约束MSS更偏 TCP 发送数据时可承载的有效载荷大小
六、TCP 怎么知道数据丢了,要不要重传
TCP 要做可靠传输,核心就得能发现:
- 哪些数据已经到了
- 哪些数据没到
- 没到的什么时候补发
1. 依赖什么判断
TCP 主要依赖:
- 序列号
seq - 确认号
ack - 重传定时器
发送方发出去的数据都有序列号,接收方通过 ACK 告诉发送方:
“我已经连续收到哪里了,下一段你该从哪开始发。”
2. 超时重传
最基础的重传方式就是:
发出去后等 ACK,超过一段时间还没等到,就重传。
这个等待时间就是 RTO。
如果 RTO 太短,会误判;如果太长,恢复又太慢。所以 TCP 会根据 RTT 动态估算 RTO。
超时重传的优点是简单直接,缺点是:
- 发现丢包偏慢
这也是为什么后面还需要快速重传。
3. RTT、SRTT 和 RTO 到底是什么
既然超时重传要靠 RTO,那就必须先把几个常见缩写分清楚。
RTT
RTT,Round Trip Time,往返时延。
可以简单理解成:
一段数据发出去,到对应 ACK 回来,花了多久。
比如某次发送后,120ms 才收到 ACK,那么这次采样到的 RTT 就是 120ms。
SRTT
SRTT,Smoothed RTT,平滑往返时延。
它不是某一次真实测出来的 RTT,而是:
对历史 RTT 样本做平滑后的结果。
为什么要平滑?因为网络时延本身就是抖动的。
例如连续几次测到:
- 80ms
- 90ms
- 85ms
- 140ms
- 88ms
如果 TCP 每次都直接拿“当前这一次 RTT”去决定超时时间,就会非常不稳定。
所以它不会只看单次样本,而是会维护一个更平滑的 RTT 估计值,也就是 SRTT。
RTO
RTO,Retransmission Timeout,重传超时时间。
它表示:
发送方愿意等 ACK 等多久;如果超过这个时间还没等到,就触发超时重传。
所以:
RTT是一次测量值SRTT是平滑后的时延估计RTO是最终用来决定“等多久才重传”的超时门槛
4. RTO 为什么不能直接等于某次 RTT
最直观的想法是:
既然 RTT 是往返时延,那 RTO 直接等于 RTT 不就行了?
但这么做问题很大。
如果 RTO 设得太短:
- ACK 只是稍微慢一点
- 发送方就会误以为丢包
- 触发没必要的重传
如果 RTO 设得太长:
- 真丢包了也要等很久
- 恢复速度就会很慢
所以 TCP 在估算 RTO 时,一般不会只看某一次 RTT,而是会综合考虑:
- 平滑后的平均时延,也就是
SRTT - 时延抖动的幅度
经典思路可以简单理解成:
RTO 大致基于 SRTT + 一定的抖动冗余很多资料里会写成类似:
RTO ≈ SRTT + 4 * RTTVAR这里的 RTTVAR 可以理解成 RTT 的波动程度。
这个公式不用硬背得太死,但要知道背后的直觉:
网络越稳定,RTO 可以设得更紧一点;网络抖动越大,RTO 就要留更多余量。
5. 关于 RTT 采样和 RTO 估算的提醒
这里有两个很容易被忽略的点。
提醒一:重传后的 ACK,RTT 不好直接拿来算
假设一段数据发出去后迟迟没收到 ACK,于是发送方重传了一次。
这时候如果 ACK 回来了,发送方其实很难判断:
- 这个 ACK 对应的是第一次发送
- 还是重传后的那一次发送
这个现象就叫重传歧义。
所以 TCP 通常不会随便拿“重传后的那次 ACK”去更新 RTT 估计。
这个思路通常会和 Karn 算法 联系在一起。
提醒二:连续超时后,RTO 往往还会退避
如果已经超时了一次,结果重传后还是没等到 ACK,TCP 一般不会还按原来的 RTO 傻等,而是会做超时退避,也就是把 RTO 继续调大。
这个思路很朴素:
既然网络现在明显有问题,就别继续太激进地频繁重传。
6. 快速重传
快速重传解决的是:
能不能别傻等超时,尽早发现丢包。
举个例子,发送方依次发出:
1-1000
1001-2000
2001-3000
3001-4000如果 1001-2000 这一段丢了,但后面的数据先到了,接收方会不断返回:
ACK = 1001
ACK = 1001
ACK = 1001也就是重复 ACK。
当发送方连续收到 3 个重复 ACK 时,通常就会认为:
中间某一段大概率丢了,不等超时了,直接重传。
这就是快速重传。
7. SACK
有些 TCP 实现还支持 SACK(Selective Acknowledgment,选择确认)。
它的作用是:
告诉发送方“哪些块我已经收到了,哪些块没收到”。
这样发送方可以更精准地补发丢失部分,不用做过多重复发送。
如果没有 SACK,接收方通常只能通过累计 ACK 告诉发送方:
“我连续收到了哪里。”
但它没法很细地表达:
- 后面哪些数据其实已经到了
- 只是中间哪一段丢了
而有了 SACK 之后,接收方就可以把“已经收到的离散数据块”一并告诉发送方。这样发送方在重传时会更有针对性,不至于把很多其实已经到达的数据再发一遍。
8. D-SACK
在 SACK 的基础上,还有一个很容易被忽略的点,叫 D-SACK(Duplicate SACK)。
它主要用来告诉发送方:
“你刚才重传的那段数据,其实我已经收过了,这次收到的是重复数据。”
也就是说,D-SACK 不是为了告诉发送方“哪一段丢了”,而是为了告诉发送方:
- 哪些数据是重复到达的
- 某次重传是不是其实没必要
- 某次乱序到达是不是被误判成丢包了
这个信息对发送方很有价值,因为它可以帮助发送方判断:
- 这次重传是不是伪重传
- 网络上是不是主要是乱序,而不是真的丢包
- 当前拥塞判断是否过于激进
从工程实践上看,D-SACK 的意义更多偏向:
- 帮助发送方更准确地分析网络状况
- 优化后续的重传和拥塞控制行为
9. 关于 SACK 和 D-SACK 的提醒
提醒:SACK 和 D-SACK 不是一回事。
- SACK:告诉发送方“哪些数据块我已经收到了”
- D-SACK:告诉发送方“你有些数据发重了,我收到的是重复块”
10. 关于快速重传的提醒
提醒:快速重传的触发信号通常是“3 个重复 ACK”,不是“重传次数很多了再决定重传”。
这个顺序不要说反。
七、流量控制:别把接收方撑爆了
1. 流量控制解决什么问题
流量控制解决的是:
发送方速度太快,接收方来不及处理怎么办?
这里要注意,它关注的是通信两端主机之间的处理能力匹配,不是整个网络是否拥堵。
2. rwnd 是怎么来的
接收方会根据自己的接收缓冲区情况,在 TCP 首部里通告窗口大小,也就是 rwnd。
它的意思很直白:
“我现在还能再接收这么多字节。”
发送方必须尊重这个限制。
3. rwnd = 0 会发生什么
如果接收方来不及处理,缓冲区满了,就可能通告:
rwnd = 0这表示:
先别发了,我现在吃不下。
但发送方也不能永远停死,所以 TCP 里还有零窗口探测,会过一段时间试探一下:
你现在能接了吗?
4. 关于流量控制的提醒
提醒:流量控制主要看的是接收方缓冲区和应用读取速度,不是“当前网络质量好不好”。
很多人会把流量控制和拥塞控制混在一起,这里要刻意区分。
八、拥塞控制:别把网络打爆了
1. 拥塞控制解决什么问题
拥塞控制解决的是:
网络中间的路由器、交换设备、链路本身已经很忙了,发送方不能还照样猛发。
和流量控制相比:
- 流量控制看的是接收方
- 拥塞控制看的是网络
2. cwnd 是谁维护的
cwnd 是发送方维护的。
它表示:
发送方根据当前网络反馈,估计自己最多可以发送多少未确认数据。
这也是前面反复强调的一个点:
rwnd来自接收方cwnd来自发送方
3. 拥塞窗口什么时候会变大
如果发送方持续正常收到 ACK,通常会认为:
网络目前还能继续承受更多流量。
于是 cwnd 会逐步增大。
4. 拥塞窗口什么时候会变小
当发送方感知到拥塞信号时,cwnd 就会减小。
典型信号包括:
- 超时重传
- 3 个重复 ACK
- ECN 拥塞标记
超时一般表示问题更严重;3 个重复 ACK 说明网络还在传,只是中间丢了一段;ECN 则是路由器提前告诉你“快堵了”。
5. 关于拥塞窗口收缩的提醒
提醒:不是等“重传很多次了”才减小拥塞窗口,而是只要 TCP 认为出现了拥塞信号,就可能收缩
cwnd。
九、慢启动:名字叫慢,其实一开始涨得不慢
1. 为什么要慢启动
一个新的 TCP 连接刚建立时,发送方并不知道:
- 当前网络有多空闲
- 能扛住多大的发送速率
如果一上来就猛发,很容易把网络直接冲堵。
所以 TCP 的策略是:
先从比较小的
cwnd开始,边发边试探。
2. 它怎么增长
在慢启动阶段,cwnd 的增长速度整体上接近指数增长。
可以粗略理解为:
1 MSS -> 2 MSS -> 4 MSS -> 8 MSS所以“慢启动”这个名字其实有点迷惑人,它不是说增长速度慢,而是说:
起步比较保守,不是一开始就直接拉满。
3. 到了什么时候会停下来
当 cwnd 增长到某个阈值 ssthresh 后,通常就不会继续按慢启动那种速度涨了,而会进入拥塞避免阶段。
十、拥塞避免:继续涨,但别涨得太猛
1. 为什么还要有这一段
如果 cwnd 一直指数增长,很快就可能把网络挤爆。
所以 TCP 在 cwnd 达到 ssthresh 后,会进入一个更谨慎的阶段:
继续增大窗口,但按更温和的速度增长。
2. 典型行为
这一阶段通常接近线性增长。
也就是:
- 每过一个 RTT,大致增加 1 个 MSS
你可以把它理解成:
- 慢启动:快速试探
- 拥塞避免:小步慢走
这两个阶段是连起来看的。
十一、快速重传:别等超时,先把丢的那段补上
前面已经提到过快速重传,这里单独拎出来再说一次,是因为它和快速恢复经常一起考。
当发送方收到 3 个重复 ACK 时,会认为:
- 某一段数据丢了
- 但网络还没有完全堵死
于是它会:
- 立即重传丢失的那一段
- 不再傻等超时
这一步就叫快速重传。
它解决的是:
丢包发现得太慢的问题。
十二、快速恢复:别每次都被打回新手村
1. 它为什么存在
如果一出现丢包,TCP 就直接把 cwnd 砍回很小,再重新慢启动,那代价会比较大。
但 3 个重复 ACK 这个信号说明:
- 网络还在传数据
- 后面的包还能到
- 只是中间有一段丢了
也就是说,这种情况没有超时那么严重。
所以 TCP 的思路是:
先收缩窗口,但别收缩得像超时那样激进,然后尽快恢复到正常发送节奏。
这就是快速恢复。
2. 它通常怎么做
经典 TCP Reno 中,收到 3 个重复 ACK 后,通常会:
- 触发快速重传
- 将
ssthresh设为当前cwnd的一半 - 将
cwnd收缩到一个较小但不至于太夸张的值 - 然后进入拥塞避免,而不是直接退回到最开始的慢启动状态
3. 和超时重传的区别
如果是超时重传,TCP 往往会更悲观:
- 认为拥塞比较严重
cwnd会被大幅缩小- 重新进入慢启动
如果是3 个重复 ACK:
- 认为网络还没有完全坏掉
- 触发快速重传
- 再走快速恢复
4. 关于快速恢复的提醒
提醒:快速重传和快速恢复不是一回事。
- 快速重传:赶紧把丢的那段补发
- 快速恢复:窗口别降得太狠,尽快回到正常节奏
这两个概念经常被连着说,但不是同一个动作。
十三、ECN:不靠丢包,也能提前告诉你网络堵了
1. ECN 解决什么问题
传统 TCP 很多时候是把丢包当成拥塞信号:
- 先丢
- 再发现
- 再收缩窗口
这个过程有点晚。
所以后来引入了 ECN(Explicit Congestion Notification,显式拥塞通知)。
2. 它大概怎么工作
简单理解就是:
- 发送方先声明自己支持 ECN
- 中间路由器发现队列开始拥堵
- 路由器不一定马上丢包,而是先给数据包打拥塞标记
- 接收方把这个拥塞信号反馈给发送方
- 发送方据此收缩
cwnd
3. 它的意义
能在真正丢包之前,就先让发送方降速。
这样有机会:
- 减少丢包
- 减少重传
- 降低时延
4. 关于 ECN 的提醒
提醒:ECN 的拥塞标记通常不是发送方自己打的,而是网络中的中间设备,最常见就是路由器。
十四、把这些机制放到一起看
如果把 TCP 可靠传输的过程串起来,大概可以这样理解:
先用滑动窗口提高发送效率 不再发一个等一个,而是允许多个未确认数据同时在路上。
用 ACK 和重传保证可靠性 丢了就补发,别让数据悄悄丢掉。
用
rwnd做流量控制 接收方处理不过来时,发送方要收着点。用
cwnd做拥塞控制 网络快堵时,发送方也要收着点。用慢启动和拥塞避免动态调整速率 一开始先试探,后面再谨慎增长。
用快速重传和快速恢复优化丢包后的表现 既要尽快补丢失数据,也别因为一次丢包就把吞吐打得太狠。
所以这几个点不是彼此独立的,而是一套完整的配合机制。
十五、几个特别容易说混的点
1. 滑动窗口不等于接收窗口
滑动窗口是一个更大的发送机制概念。
接收窗口 rwnd 只是其中影响发送范围的一个约束条件。
2. 流量控制不等于拥塞控制
它们看起来都在“控制发送速度”,但关注对象不同:
- 流量控制:保护接收方
- 拥塞控制:保护网络
3. cwnd 不是接收方告诉你的
cwnd 是发送方自己根据网络反馈维护出来的。
4. TCP 没有“TCP 帧”这个标准说法
TCP 更标准的术语是:
- 报文段(segment)
“帧”这个词在教材里通常更偏向链路层。
这一点和 HTTP/2 frame、WebSocket frame 不是一个语境。
5. 3 个重复 ACK 不等于 ACK 丢了
通常它说明的是:
- 前面某一段没到
- 后面的段先到了
- 接收方只能反复确认自己当前连续收到的位置
6. 粘包 / 拆包不是 TCP 出错
很多人第一次学到这里时,会把“粘包 / 拆包”理解成 TCP 不可靠。其实不是。它真正反映的是:
TCP 是面向字节流的协议,不负责替应用层保留消息边界。
什么是粘包
发送方发了多条应用层消息,但接收方一次读取时把它们连在一起读出来了。例如:
发送:Hello | World
接收:HelloWorld什么是拆包
发送方只发了一条完整消息,但接收方需要分多次才能读完。例如:
发送:HelloWorld
第一次读:Hello
第二次读:World为什么 TCP 会有这个现象
核心原因只有一个:
- TCP 只保证字节流可靠、有序到达
- 不保证一次
write()对应一次read()
再具体一点:
- 发送方多次小数据写入,可能被 TCP 合并发送
- 网络传输过程中也可能被拆成多个报文段
- 接收方每次
read()能读到多少,取决于接收缓冲区当时有多少数据,而不是“业务消息刚好到哪里结束”
怎么解决粘包 / 拆包
要解决这个问题,必须由应用层协议自己定义消息边界。常见做法有三种:
- 固定长度:每条消息长度固定
- 分隔符:例如以
\n结尾 - 长度字段 + 消息体:先读长度,再按长度读消息体
工程里最常见、也最稳妥的方式通常是:
长度字段 + 消息体
因为它既适合文本协议,也适合二进制协议。
十六、面试里怎么回答会比较顺
如果面试官让你概括 TCP 的可靠传输机制,可以这么说:
TCP 为了实现可靠传输,首先通过序列号、确认应答和重传机制来保证数据不丢;为了提高发送效率,又引入了滑动窗口,让多个未确认数据可以同时在网络中传输。与此同时,TCP 还需要做两类控制:流量控制通过接收方通告窗口
rwnd来避免把接收方缓冲区撑爆;拥塞控制通过发送方维护的拥塞窗口cwnd来避免把网络打爆。拥塞控制里典型还包括慢启动、拥塞避免、快速重传和快速恢复等机制。发送方最终实际可发送的数据量,一般取决于min(rwnd, cwnd)。
如果对方继续追问“快速恢复是什么”,可以再补一句:
快速恢复是配合快速重传使用的。当发送方收到 3 个重复 ACK 时,说明网络还没完全堵死,只是中间某一段丢了,所以这时没必要像超时那样把
cwnd直接打回很小,而是先重传丢失数据,再把窗口适度收缩后继续进入拥塞避免阶段。
十七、小结
把这篇内容压缩成几句话,其实就够用了:
- 滑动窗口解决的是发送效率问题
- 重传解决的是数据丢失问题
- 流量控制解决的是接收方处理不过来的问题
- 拥塞控制解决的是网络本身拥堵的问题
- 快速重传是尽早补丢的数据
- 快速恢复是别因为一次丢包就把窗口砍得太狠
最后记住一句最常用的话:
发送方实际能发多少,通常看 min(rwnd, cwnd)这句话虽然不是 TCP 全部,但已经能把主线抓住了。
