TCP可靠传输机制——“不丢包“背后的技术秘密 ⏱️ 阅读约 15 分钟标签TCP协议可靠传输重传机制网络编程数据传输**一句话总结**TCP的可靠传输不是魔法而是一套精心设计的快递签收系统——每寄一个包裹都要对方签收没收到签收就重新寄还能智能判断哪些包裹丢了只补寄那些最终实现99.99%的可靠性。你有没有想过当你在浏览器里输入一个网址背后发生了什么数据是怎么从服务器飞到你电脑上的更重要的是——如果中间有包裹丢了怎么办IP协议说我不管丢了就丢了爱咋咋地。TCP翻了个白眼行吧烂摊子我来收拾。今天我们就来扒一扒TCP这位快递界强迫症是如何做到一个都不能少的。一、序列号与确认号每个包裹都有身份证想象一下你要给朋友寄10个快递包裹分别装着《三体》的10个章节。如果没有任何标记朋友收到后怎么知道第1章在哪、第5章是不是丢了** 快递公司的做法**每个包裹上贴个编号——这是第1个包裹、这是第2个包裹...朋友收到后告诉你我收到第1、2、3个了第4个还没到。TCP的做法一模一样只是更精细。1.1 序列号Sequence NumberTCP给每个字节都编了号。注意是每个字节不是每个包。假设你要发送Hello World这11个字节含空格TCP会这样编号H(1000) e(1001) l(1002) l(1003) o(1004) _(1005) W(1006) o(1007) r(1008) l(1009) d(1010)假设初始序列号是1000那么• H 的序列号是 1000• e 的序列号是 1001• ...以此类推当TCP发送一个包含Hello的包时包头里的Sequence Number字段就是1000表示这个包从第1000个字节开始。1.2 确认号Acknowledgment Number接收方收到数据后需要告诉发送方我收到哪儿了。这就是确认号的作用。但这里有个极易混淆的点**⚠️ 重要**确认号表示我期望收到的下一个字节的序号而不是我收到的最后一个字节的序号。如果接收方收到了字节1000-1004Hello它会回复确认号1005意思是1004及之前的都收到了下次请从1005开始发。发送方 接收方 │ │ │ SEQ1000, DataHello (1000-1004) │ │ ───────────────────────────────────── │ │ │ │ ACK1005 (期望收到1005) │ │ ───────────────────────────────────── │ │ │ │ SEQ1005, DataWorld (1005-1010) │ │ ───────────────────────────────────── │ │ │ │ ACK1011 (期望收到1011) │ │ ───────────────────────────────────── │ │ │这个机制看似简单却是TCP可靠传输的基石。没有编号你连丢了什么都不知道更别提补发了。二、超时重传机制没收到签收就重新寄现在问题来了发送方发了一个包但接收方的确认迟迟不来。是包丢了还是确认丢了还是只是网络慢了TCP的解决方案很直接设置一个闹钟超时了就重发。** 快递公司的做法**你寄出一个包裹然后设置一个闹钟比如3天。如果3天内没收到朋友的签收短信你就认为包裹可能丢了重新寄一个。这个闹钟时间在TCP里叫RTORetransmission Timeout重传超时时间。2.1 RTO的计算不是拍脑袋定的你可能会想那就固定设个3秒呗简单粗暴。Too young too simple。网络环境千差万别局域网里1毫秒就能到跨国链路可能要几百毫秒。固定RTO要么太激进频繁重传浪费带宽要么太保守丢包后等太久。TCP用了一套自适应算法来动态计算RTO基于两个核心指标•RTTRound Trip Time往返时间发一个包到收到确认的时间•RTTVARRTT VarianceRTT方差RTT的波动程度经典算法Jacobson算法如下# 每次测量到新的RTT样本时 SRTT (1 - α) * SRTT α * RTT_sample # 平滑RTT RTTVAR (1 - β) * RTTVAR β * |SRTT - RTT_sample| # 平滑方差 RTO SRTT 4 * RTTVAR # 最终RTO # 推荐值α 0.125, β 0.25简单说就是RTO 平均RTT 4倍波动范围这样网络稳定时RTO就小网络抖动大时RTO就大实现了因网制宜。2.2 Karn算法重传的包不算数这里有个坑如果发生了重传你怎么知道收到的确认是针对第一次发的还是重传的发送方 接收方 │ │ │ T0: 发送数据包 SEQ1000 │ │ ───────────────────────────────────── │ │ │ │ (包在网络中延迟还没到达) │ │ │ │ T1: RTO超时重传 SEQ1000 │ │ ───────────────────────────────────── │ │ │ │ T2: 收到 ACK1100 │ │ ───────────────────────────────────── │ │ │ │ 问题这个ACK是针对第一次的包 │ │ 还是重传的包 │Karn算法给出了答案重传的包不参与RTT计算。只有那些没重传就收到确认的包才用来更新SRTT。同时Karn算法还建议每次重传后RTO应该指数退避翻倍避免在网络拥塞时雪上加霜。** 小知识**Linux内核中RTO有上下限——最小200ms最大120秒由/proc/sys/net/ipv4/tcp_rto_min和tcp_rto_max控制。三、快速重传3个重复ACK触发超时重传有个致命问题太慢了。假设RTO是1秒那丢包后至少要等1秒才能重传。对于实时性要求高的应用比如在线游戏、视频会议这1秒简直是 eternity。TCP工程师们想能不能不等超时提前发现丢包于是**快速重传Fast Retransmit**诞生了。** 快递公司的做法**朋友收到第1、3、4个包裹后连续给你发了3条消息我收到第1个了第2个呢、我收到第1个了第2个呢、我收到第1个了第2个呢你一看连催3次肯定是第2个丢了赶紧补发3.1 重复ACK的奥秘当接收方收到乱序的包时比如先收到了3、4但2还没来它会重复发送最后一个按序收到的确认号。看下面这个例子发送方 接收方 │ │ │ SEQ1000 (包1) ──────────────────── │ 收到包1期望1001ACK1001 │ ──────────────────────────── ACK1001 │ │ │ │ SEQ2000 (包3) ──────────────────── │ 收到包3乱序仍ACK1001 │ ──────────────────────────── ACK1001 │ (重复ACK #1) │ │ │ SEQ3000 (包4) ──────────────────── │ 收到包4乱序仍ACK1001 │ ──────────────────────────── ACK1001 │ (重复ACK #2) │ │ │ SEQ4000 (包5) ──────────────────── │ 收到包5乱序仍ACK1001 │ ──────────────────────────── ACK1001 │ (重复ACK #3) │ │ │ 触发快速重传重传SEQ1001 (包2) │ │ SEQ1001 (包2) ──────────────────── │ │ │发送方收到3个重复的ACK后就认为对应的包丢了立即重传不用等RTO超时。为什么是3个不是2个也不是4个因为网络乱序是常态。如果只用2个可能误判用4个又太慢。3个是工程上的经验折中RFC 5681规定。** Linux调优**可以通过/proc/sys/net/ipv4/tcp_reordering调整重复ACK阈值但一般不建议修改。四、选择性确认SACK只补寄丢了的那个快速重传解决了何时重传的问题但还有个问题重传什么传统TCP收到3个重复ACK后会重传从那个确认号开始的所有数据。这叫Go-Back-N策略。但问题是如果我只丢了1个包为什么要重传后面100个已经收到的包** 快递公司的做法**朋友说我收到第1、3、4、5个了第2个没收到。聪明的你会只补寄第2个而不是把3、4、5也重新寄一遍。这就是SACKSelective Acknowledgment选择性确认。4.1 SACK选项格式SACK是TCP选项的一种在包头里携带。格式如下TCP SACK选项格式Kind5 ----------------------------------------------------------------... | Kind5 | Length | 左边界1 | 右边界1 | 左边界2 | 右边界2 | ... ----------------------------------------------------------------... 1字节 1字节 4字节 4字节 4字节 4字节 最多可以报告4个已接收的块受TCP选项长度限制40字节每个块表示一段已收到的数据范围。比如发送的数据序列号1000-2000, 2000-3000, 3000-4000, 4000-5000 │ │ │ │ 包1 包2 包3 包4 接收情况包1 ✓ 包2 ✗丢失 包3 ✓ 包4 ✓ 接收方回复 ACK2000期望收到2000 SACK选项: [3000-4000], [4000-5000]报告已收到的块 发送方一看哦3000-5000都收到了就2000-3000没收到只发这个就行4.2 SACK的启用SACK需要在TCP握手时协商SYN包SACK-Permitted选项Kind4 SYN-ACK包如果支持也回复SACK-Permitted 之后的数据包就可以使用SACK选项了现代操作系统Linux、Windows、macOS都默认支持SACK。你可以用以下命令检查# Linux检查SACK是否启用 $ sysctl net.ipv4.tcp_sack net.ipv4.tcp_sack 1 # 1表示启用 # 临时关闭仅测试 $ sudo sysctl -w net.ipv4.tcp_sack0**⚠️ 注意**SACK虽然高效但也有安全风险。2019年曝光的SACK Panic漏洞CVE-2019-11477就是利用SACK选项的边界问题。保持内核更新很重要。五、校验和检测包裹有没有被掉包前面说的都是丢包的情况但还有一种更隐蔽的问题包被篡改了。网络设备故障、电磁干扰、恶意攻击...都可能导致数据在传输过程中发生变化。如果接收方毫无察觉地使用了错误数据后果不堪设想。** 快递公司的做法**每个包裹封口处有个防伪标签朋友收到后检查标签是否完好。如果标签对不上说明包裹被拆过或掉包了拒收TCP的防伪标签就是校验和Checksum。5.1 校验和计算TCP校验和覆盖三部分1. TCP伪首部包含源IP、目的IP、协议类型、TCP长度2. TCP首部3. TCP数据计算方法是16位反码求和# 伪代码TCP校验和计算 def tcp_checksum(pseudo_header, tcp_header, data): # 把所有数据按16位分组 words split_into_16bit_words(pseudo_header tcp_header data) # 求和32位累加处理溢出 total 0 for word in words: total word if total 0xFFFF: # 溢出回卷 total (total 0xFFFF) 1 # 取反 checksum ~total 0xFFFF return checksum发送方计算校验和填入包头接收方重新计算比对。如果不匹配直接丢弃这个包不发送确认。发送方RTO超时后会重传。5.2 校验和的局限性需要明确的是TCP校验和不是加密只是简单的完整性校验• 它能检测出大多数随机错误• 但它不是密码学安全的恶意攻击者可以构造出校验和正确的篡改数据• 对于安全需求高的场景需要用TLS/SSL** 有趣的事实**UDP的校验和是可选的可以填0表示不计算但TCP的校验和是强制的。这就是可靠与不可靠的区别之一。六、流量控制滑动窗口机制前面讲的都是可靠性但TCP还要解决另一个问题速度匹配。发送方可能是个千兆网卡的服务器接收方可能是个内存紧张的手机。如果发送方不管不顾地猛发接收方处理不过来只能丢包——这就违背了可靠传输的初衷。** 快递公司的做法**朋友告诉你我家门口只能放5个包裹放满了你就别寄了等我处理完再告诉你。你根据这个信息控制发货速度。这就是流量控制Flow Control通过**滑动窗口Sliding Window**实现。6.1 接收窗口rwndTCP包头里有个Window Size字段接收方用它告诉发送方我的缓冲区还剩多少空间你最多发这么多。发送方缓冲区 ------------------------------------------------------------- │ 已确认并发送 │ 已发送未确认 │ 允许发送但还未发送 │ 不可发送 │ │ (1000-2000) │ (2000-5000) │ (5000-8000) │ (8000) │ ------------------------------------------------------------- ↑ ↑ ↑ 已确认 发送窗口 窗口前沿 (rwnd3000) 接收方通告Window Size 3000字节窗口随着数据流动态滑动• 收到确认后窗口左沿右移释放已确认的空间• 接收方处理数据后窗口右沿右移腾出新的接收空间6.2 窗口缩放选项TCP头部的Window Size字段只有16位最大值65535字节。在现代高速网络中这太小了。解决方案是Window Scale选项RFC 1323# 在SYN包中协商窗口缩放因子 Window Scale Factor: 0-14 实际窗口大小 Window Size字段值 × 2^Scale Factor # 例如 Window Size 65535 Scale Factor 8 实际窗口 65535 × 256 16,776,960 字节 (~16MB)Linux中可以通过以下命令查看和设置# 查看窗口缩放是否启用 $ sysctl net.ipv4.tcp_window_scaling net.ipv4.tcp_window_scaling 1 # 查看默认接收缓冲区大小 $ sysctl net.ipv4.tcp_rmem net.ipv4.tcp_rmem 4096 87380 6291456 ↑最小 ↑默认 ↑最大6.3 零窗口与窗口探测如果接收方缓冲区满了会发送Window Size 0的包告诉发送方停别发了发送方收到零窗口后会启动持续定时器Persist Timer定期发送窗口探测包Window Probe——只带1个字节数据询问可以发了吗这是为了防止死锁如果接收方的窗口更新包丢了发送方一直等接收方也一直等连接就僵死了。七、实际案例高丢包网络中的TCP表现理论讲了一堆我们来看看真实场景。7.1 场景设定假设你在用SSH远程管理一台海外服务器网络环境很差• RTT200ms• 丢包率5%• 带宽10Mbps在这种环境下TCP的各种机制是如何协同工作的7.2 抓包分析我们用tcpdump抓包看看# 抓取TCP流量 $ sudo tcpdump -i eth0 -w bad_network.pcap tcp port 22 # 用Wireshark或tshark分析 $ tshark -r bad_network.pcap -T fields \ -e frame.number -e ip.src -e tcp.seq -e tcp.ack -e tcp.len -e tcp.analysis.retransmission \ -Y tcp.port22 | head -50你可能会看到这样的输出1 192.168.1.100 SEQ1 ACK1 LEN1460 2 10.0.0.1 SEQ1 ACK1461 LEN0 3 192.168.1.100 SEQ1461 ACK1 LEN1460 4 192.168.1.100 SEQ2921 ACK1 LEN1460 5 192.168.1.100 SEQ4381 ACK1 LEN1460 6 10.0.0.1 SEQ1 ACK1461 LEN0 [Dup ACK #1] 7 10.0.0.1 SEQ1 ACK1461 LEN0 [Dup ACK #2] 8 10.0.0.1 SEQ1 ACK1461 LEN0 [Dup ACK #3] 9 192.168.1.100 SEQ1461 ACK1 LEN1460 [Fast Retransmission] 10 10.0.0.1 SEQ1 ACK5841 LEN0分析• 第1行发送方发送数据SEQ1• 第2行接收方确认收到ACK1461• 第3-5行继续发送后续数据包• 第6-8行收到3个重复ACKACK都是1461说明SEQ1461的包丢了• 第9行触发快速重传重发SEQ1461• 第10行接收方确认收到所有数据ACK58417.3 性能影响在高丢包网络中TCP的性能会急剧下降。这是因为1.重传开销每个丢包都要重传浪费带宽2.拥塞控制TCP会把丢包当作拥塞信号减小发送窗口3.RTO退避连续丢包会导致RTO指数增长等待时间越来越长在5%丢包率下TCP的有效吞吐量可能只有理论值的30-50%。7.4 优化建议针对高丢包网络可以考虑以下优化# 1. 启用BBR拥塞控制算法对丢包不敏感 $ sudo sysctl -w net.ipv4.tcp_congestion_controlbbr # 2. 增大初始拥塞窗口 $ sudo sysctl -w net.ipv4.tcp_slow_start_after_idle0 # 3. 调整重传参数 $ sudo sysctl -w net.ipv4.tcp_retries28 # 减少重传次数 # 4. 使用多路径TCP或QUIC应用层** 终极方案**如果网络质量实在糟糕考虑在应用层使用FEC前向纠错或改用QUIC协议基于UDP但内置了类似TCP的可靠性机制对丢包更友好。总结TCP可靠传输的七剑合璧回顾一下TCP通过以下机制实现了可靠传输机制作用快递比喻序列号/确认号标识每个字节追踪传输状态包裹编号超时重传RTO超时后重发数据闹钟提醒重寄快速重传3个重复ACK立即重传连催3次补寄SACK选择性确认只重传丢失部分精准补寄校验和检测数据是否损坏防伪标签滑动窗口流量控制匹配收发速度门口容量限制这六大机制相互配合构成了TCP可靠传输的完整体系。它们不是孤立的而是相互协作、相互补充• 序列号/确认号是基础没有它们其他都无从谈起• 超时重传是保底确保最终能恢复• 快速重传是加速减少等待时间• SACK是优化提高重传效率• 校验和是质检保证数据正确性• 滑动窗口是调速防止过载丢包正是这些机制的精密配合让TCP在复杂的互联网环境中实现了99.99%以上的可靠性成为互联网数据传输的基石。 源码获取本文涉及的实验代码和抓包脚本已整理到GitHub仓库https://github.com/yourname/tcp-reliability-lab包含• TCP客户端/服务器示例代码Python/C• Wireshark抓包分析脚本• Linux内核参数调优配置• 模拟丢包网络的tc命令集 思考题1. **问题1**为什么快速重传要等待3个重复ACK而不是2个或4个2个会有什么风险4个会有什么缺点2. **问题2**假设RTT是100ms如果TCP只使用超时重传而不使用快速重传在丢包场景下恢复时间至少是多少如果有快速重传呢3. **问题3**SACK选项最多能报告4个接收块如果乱序很严重比如收到10个不连续的块TCP会怎么处理4. **问题4**在高丢包网络中为什么TCP的吞吐量会急剧下降除了重传本身的开销还有什么因素在起作用5. **问题5**TLS/SSL在TCP之上提供了加密和完整性保护那为什么TCP还要有校验和不能直接依赖TLS吗 系列文章预告《网络协议深度解析》系列持续更新中期数主题01TCP三次握手与四次挥手02TCP拥塞控制算法演进03TCP粘包与拆包问题04TCP可靠传输机制✓05HTTP/3与QUIC协议解析06从TCP到UDP实时通信协议选择关注公众号第一时间获取更新 如果本文对你有帮助欢迎点赞、收藏、转发有问题或建议欢迎在评论区留言讨论 标签TCP协议可靠传输重传机制网络编程数据传输