1. 超时与重传概述:为什么需要超时与重传?
网络不可靠的本质
说实话,我刚入行那会儿,总觉得网络应该挺靠谱的。数据发出去,对方就能收到——这不是天经地义的事吗?直到我第一次在生产环境排查一个“数据丢了”的Bug,才真正被现实上了一课。
网络为什么不可靠?我总结了几点:
- 物理层不稳定:网线松动、电磁干扰、信号衰减,这些都会导致比特翻转或丢包。我在机房见过一根被老鼠咬了一半的网线,偶尔通偶尔断,排查了整整两天。
- 缓冲区溢出:路由器、交换机的队列是有限的。流量一拥而上,队列满了怎么办?直接丢包。说白了,网络设备不会等你,满了就扔。
- 路由变化:数据包走的路径不是固定的。今天走A路径,明天可能走B路径。路径变了,延迟和丢包率也跟着变。
- 拥塞控制:TCP自己会主动丢包。你没看错,当网络拥塞时,TCP会故意丢一些包来告诉发送端“慢点发”。
核心观点:网络不可靠不是“偶尔发生”,而是“必然发生”。你设计的系统,必须假设任何一次传输都可能失败。
可靠传输的基石:确认与重传
那怎么办?总不能因为网络不可靠就不传数据了吧。其实思路很简单——发出去的东西,得确认对方收到了。没收到就再发一次。
这就是可靠传输的两个基本动作:
- 确认(ACK):接收方收到数据后,告诉发送方“我收到了”。
- 重传(Retransmission):发送方在一定时间内没收到确认,就重新发送数据。
你想想看,这就像你给人发。如果对方没回,你会等一会儿再问一次“收到了吗?”——超时重传就是这个道理。
超时与重传的关系
超时和重传是绑在一起的。没有超时,重传就不知道什么时候该触发。没有重传,超时就是个没用的定时器。
我习惯用一个简单的状态机来理解:
发送数据 → 启动定时器 → 等待ACK
↓ ↓
收到ACK 定时器超时
↓ ↓
取消定时器 重传数据
↓ ↓
完成 重新启动定时器
嗯,这里要注意:超时时间设多长是个大学问。设短了,容易误判丢包,造成大量不必要的重传。设长了,真丢包时恢复太慢,用户体验极差。
我的经验:超时时间不能是固定的。网络状况随时在变,RTT(往返时间)也在变。我建议用动态算法来计算超时,比如TCP用的RTO(Retransmission TimeOut)估算。后面章节我会详细讲。
一个简单的例子
假设你在写一个文件传输工具。客户端向服务端发送一个1KB的数据块:
客户端 → 服务端: [数据块 #1]
客户端: 启动定时器,等待5秒
场景A(正常):
服务端 → 客户端: ACK #1
客户端: 取消定时器,发送数据块 #2
场景B(丢包):
(5秒后,定时器超时)
客户端: 重传数据块 #1
客户端: 重新启动定时器,等待5秒
你看,就这么简单。但实际生产环境远比这个复杂——ACK本身也可能丢,数据可能延迟到达,重传的数据和原始数据可能同时到达……这些坑,我后面都会一一讲到。
知识体系总览
为了让你对整个章节有个全局认识,我画了一张图:
避坑指南
我曾经踩过的坑:有一次我写一个UDP之上的可靠传输协议,把超时时间设成了固定值1秒。结果在跨洋链路上,RTT经常超过2秒。客户端疯狂重传,服务端收到大量重复数据,最终导致内存溢出。后来我改成动态RTO估算,问题才解决。
教训:永远不要用固定超时。网络是动态的,你的超时策略也必须是动态的。
小结
超时与重传,说白了就是给不可靠的网络加了一层“保险”。确认机制告诉你“到了没”,超时机制告诉你“啥时候再发”,重传机制负责“再发一次”。这三者缺一不可。
我个人习惯在设计任何网络协议时,先把这三个问题想清楚:
- 超时时间怎么算?
- 重传策略是什么?
- 重复数据怎么处理?
想清楚了,再动手写代码。后面几章,我会逐一拆解这些细节。