资讯详情

资讯详情

TCP可靠传输机制全解析:从序列号到拥塞控制

1. 从一个抓包现场说起为什么TCP让人又爱又恨很多人第一次接触网络编程都是从“TCP是可靠的、UDP是不可靠的”这句话开始的。但如果你真的在深夜抓过一次包看着Wireshark里密密麻麻的Seq、Ack、Len、Win字段你就会明白——TCP的可靠性不是一句口号而是一整套精密机制在背后撑着。它像一个极其负责的快递公司你只管把包裹交给它剩下的丢件补发、错件重发、顺序错乱重新排、路上堵车自动降速全部由它内部消化。这篇文章想做的事情很纯粹把TCP保证可靠传输的每一层机制拆开讲透。不是背八股文而是从“如果让我来设计一个可靠传输协议我会怎么做”的角度一步步推导出TCP为什么长成今天这个样子。适合谁看刚学完计算机网络但还停留在背诵阶段的学生、工作中需要排查网络问题的后端和运维同学、准备面试但想把知识点串成体系的开发者。看完之后你至少能做到两件事第一面试时不再干巴巴地背“三次握手四次挥手”第二线上遇到丢包、重传、连接卡顿这类问题时知道从哪里下手。我个人的习惯是学任何协议都要问三个问题它要解决什么问题它用什么手段解决这些手段的代价是什么TCP的可靠性设计本质上就是在“可靠”和“效率”之间反复权衡的产物。下面我们就沿着这条主线一层一层往下挖。2. 可靠传输的底层逻辑先搞清楚要解决哪些问题2.1 不可靠的信道到底会出哪些幺蛾子在讨论TCP怎么保证可靠之前得先明确“不可靠”具体指什么。物理链路和中间网络设备不会承诺任何事情数据在传输过程中可能遭遇以下几类问题比特差错信号在传输中被干扰0变成11变成0。虽然现代链路的误码率很低但长距离、无线环境、老旧设备下依然存在。丢包路由器缓冲区满了会主动丢弃链路故障会直接断流拥塞时丢包更是家常便饭。乱序数据包走不同路径到达顺序和发送顺序不一致。IP网络本身不保证顺序。重复某些情况下一个包会被复制成多份送达接收方可能收到重复数据。延迟抖动有的包快有的包慢导致接收方等待时间不确定。这五类问题里比特差错和丢包是最核心的乱序和重复是衍生问题。TCP的可靠性机制就是针对这五类问题逐一设计的。你可以把TCP想象成一个负责任的仓库管理员货发出去要登记序列号收货要签字确认ACK没签字就重新发超时重传签字了但货不对就退货校验和货到了顺序乱了就重新摆重组排序同样的货收到两次就扔掉去重。2.2 可靠传输的三大基石确认、重传、排序不管什么可靠传输协议底层都离不开三个基本动作。第一个是确认Acknowledgement接收方收到数据后要告诉发送方“我收到了”。第二个是重传Retransmission发送方在一定时间内没收到确认就认为数据丢了重新发一遍。第三个是排序Sequencing给每个数据块编号接收方按编号重组保证上层拿到的数据顺序正确。这三个动作听起来简单但魔鬼在细节里。确认怎么确认是收到一个包确认一个还是收到一批确认一批重传什么时候触发是固定时间还是动态计算排序的编号怎么设计是给每个字节编号还是给每个包编号TCP在这些细节上做了大量精巧的设计也正是这些设计让它既可靠又高效。接下来我们就逐个拆解。2.3 为什么不能只靠“重传”一招打天下有人可能会想既然会丢包那我发出去之后没收到确认就无限重传不就行了理论上可行但实际会带来灾难性后果。如果发送方不管不顾地重传网络里会充斥大量重复数据拥塞进一步加剧形成“越丢越传、越传越丢”的恶性循环。所以可靠传输必须和流量控制、拥塞控制配合使用。发送方要知道接收方能接多少流量控制也要知道网络能承受多少拥塞控制。这两个“控制”是TCP可靠性能够落地的前提否则再完美的重传机制也会把网络压垮。这也是为什么我在排查网络问题时从来不孤立地看“重传率”这一个指标。重传率高可能是链路质量差也可能是接收方窗口太小还可能是网络拥塞导致的大面积丢包。只有把确认、重传、排序、流控、拥塞控制放在一起看才能定位到真正的根因。3. 序列号与确认号TCP可靠性的“账本系统”3.1 字节流编号为什么不是给每个包编号TCP把上层交下来的数据看成一条无结构的字节流而不是一个个独立的消息包。这个设计非常关键。如果给每个包编号那么包的大小一变编号体系就乱了而给每个字节编号无论怎么切分数据编号都是连续的、唯一的。TCP首部里的**序列号Sequence Number**字段标识的就是本报文段数据第一个字节在整个字节流中的位置。举个例子假设你要发送1000字节的数据TCP可能把它切成两个报文段第一个携带字节0到499序列号就是0第二个携带字节500到999序列号就是500。接收方收到后根据序列号就能知道每个字节在流中的位置即使到达顺序是反的也能正确重组。这种设计的好处是TCP不需要关心应用层怎么切分数据它只负责把字节流可靠地搬过去。注意序列号的初始值不是从0开始的而是通过三次握手随机生成的。这样做是为了避免旧连接的残留报文干扰新连接属于安全性和可靠性的双重考虑。3.2 累积确认一次ACK能确认多少数据接收方收到数据后会发送确认号Acknowledgment Number表示“我已经收到了这个序号之前的所有字节下次请从这个序号开始发”。这就是累积确认机制。比如接收方回了ACK500意思是“0到499我全收到了请给我500及以后的”。累积确认的好处是抗丢包能力强。假设发送方发了1、2、3三个报文段接收方只收到了1和3没收到的2。接收方回ACK时只会确认到1的末尾因为2缺失了3虽然收到了但无法确认累积确认要求连续。发送方收到这个ACK后知道2丢了就会重传2。等2到达后接收方发现3已经在缓冲区里了于是直接确认到3的末尾。整个过程不需要发送方逐个重传效率很高。但累积确认也有缺点如果中间丢了一个包后面收到的包都无法被确认发送方可能误以为后面也丢了导致不必要的重传。这就是为什么后来出现了**选择确认SACK**扩展允许接收方告诉发送方“我收到了哪些不连续的块”。SACK在Linux上默认开启抓包时经常能看到。3.3 确认号的实战解读抓包时怎么看在实际抓包中确认号是最常看的字段之一。我一般会关注这几个点第一ACK号是否在稳步增长如果长时间不变说明接收方没有收到新数据可能卡住了。第二ACK号是否跳跃式增长说明中间有数据被确认了但没抓到包可能是抓包点位置问题。第三是否出现重复ACK重复ACK是快速重传的触发信号连续三个重复ACK通常意味着中间有丢包。举个实际例子某次排查一个文件上传慢的问题抓包发现发送方一直在重传同一个序列号的报文段而接收方反复回同一个ACK号。这说明接收方在等一个特定的字节但发送方发了好几次都没成功。进一步看TTL和路由发现是中间某条链路有间歇性丢包。这种问题光看应用日志是看不出来的必须结合序列号和确认号才能定位。4. 超时重传与快速重传丢包了到底怎么补4.1 超时重传RTO是怎么算出来的发送方每发一个报文段就会启动一个重传定时器。如果在**重传超时时间RTO**内没收到确认就重传该报文段。RTO的设置是个技术活设得太短会导致不必要的重传浪费带宽设得太长丢包后恢复慢影响吞吐。TCP的RTO不是固定值而是动态计算的。核心公式基于**往返时间RTT**的平滑估计先测量每个报文段的RTT样本。计算平滑往返时间SRTT和RTT偏差RTTVAR。RTO SRTT 4 × RTTVAR这是Jacobson/Karels算法的经典形式。这个公式的直觉是RTO要略大于平均RTT并且留出足够的余量应对抖动。4倍的偏差系数是为了在RTT波动较大时也能覆盖。实际实现中RTO还有一个下限比如Linux上通常是200ms和一个上限比如120秒防止极端情况。提示RTO的计算在Linux内核里由tcp_rtt_estimator函数完成如果你做内核调优可以关注tcp_rto_min和tcp_rto_max这两个参数。4.2 快速重传不等超时靠重复ACK触发超时重传的问题是等待时间太长。如果RTO是200ms丢一个包就要等200ms才能补发对于高延迟网络来说太慢了。于是TCP引入了快速重传机制当发送方收到三个重复ACK时不等超时就直接重传丢失的报文段。为什么是三个因为一个重复ACK可能是乱序造成的两个也可能是偶然三个重复ACK基本可以确定是丢包了。这个阈值是经验值在大多数网络环境下能有效区分乱序和丢包。快速重传大大缩短了丢包恢复时间是现代TCP性能的重要保障。但快速重传也有局限如果丢包发生在窗口末尾后续没有足够的数据包来触发重复ACK就只能靠超时重传。另外如果窗口很小可能凑不齐三个重复ACK。所以实际网络中超时重传和快速重传是互补的不能偏废。4.3 重传的代价为什么不能无脑重传重传虽然能补丢包但代价不小。首先重传会占用带宽如果网络已经拥塞重传会加剧拥塞。其次重传会导致接收方收到重复数据虽然TCP会去重但去重本身也有开销。第三重传会打乱RTT测量因为重传报文的ACK无法区分是确认原始报文还是重传报文Karn算法就是解决这个问题的。所以TCP在重传时会配合拥塞控制一旦发生重传就认为网络可能拥塞了主动降低发送速率。具体来说超时重传会把拥塞窗口降到1个MSS快速重传会触发快速恢复。这种“丢包即拥塞”的假设在无线网络下不一定成立因为无线丢包可能只是信号问题不是拥塞。这也是为什么后来出现了各种TCP变体如CUBIC、BBR来优化这个问题。5. 滑动窗口与流量控制别把接收方撑爆5.1 窗口机制接收方的“容量告示牌”TCP是全双工协议双方都可以发送和接收。每个方向都有一个接收窗口Receive Window表示接收方当前还能接收多少字节。发送方根据这个窗口来决定最多能发多少未确认的数据。窗口字段在TCP首部里占16位最大65535字节后来通过窗口缩放选项可以扩展到更大。滑动窗口的核心思想是窗口内的数据可以连续发送不需要每发一个就等确认。窗口左沿是已确认的数据右沿是还能发送的数据边界。随着ACK到来窗口左沿右移新的数据可以进入窗口。这种机制让TCP可以流水线式发送大幅提升吞吐。我经常用“传送带”来类比滑动窗口传送带上有若干个格子每个格子放一个数据包。接收方确认一个传送带就往前挪一格后面就能放新包。如果接收方说“我这边堆满了”传送带就暂停等接收方消化一些再继续。5.2 零窗口与窗口探测接收方满了怎么办当接收方的缓冲区满了它会通告一个零窗口告诉发送方“别再发了”。发送方收到零窗口后会停止发送数据但会启动一个持续定时器定期发送窗口探测报文询问接收方“现在有空间了吗”。这个探测报文只带一个字节的数据或者不带数据目的是触发接收方回复当前的窗口大小。如果接收方一直不回发送方会逐渐延长探测间隔避免频繁打扰。一旦接收方有了空间会回复一个非零窗口发送方就恢复发送。这个机制防止了死锁如果发送方一直等接收方通知而通知报文丢了双方就会僵住。窗口探测打破了这种僵局。注意零窗口在实际网络中不常见但在接收方应用读取慢、缓冲区小的情况下会出现。如果你看到大量零窗口探测说明接收方处理能力跟不上需要优化应用层读取逻辑或调大接收缓冲区。5.3 糊涂窗口综合症小包太多也是病滑动窗口有个副作用叫糊涂窗口综合症Silly Window Syndrome。如果接收方每次只腾出一点点空间就通告发送方就会发很小的报文段导致网络里充斥大量小包首部开销占比极高效率极低。同样如果发送方应用层每次只写几个字节TCP也可能发小包。解决办法有两个方向接收方方面采用Clark算法在缓冲区腾出足够空间比如一半或一个MSS之前不通告小窗口发送方方面采用Nagle算法把小数据攒起来等收到ACK或者攒够一个MSS再发。Nagle算法在交互式应用里可能增加延迟所以很多场景会设置TCP_NODELAY来关闭它。这两个算法是TCP可靠性设计中“效率与延迟权衡”的典型例子。没有绝对的好坏只有适不适合你的场景。比如SSH这种交互式应用延迟敏感通常会关闭Nagle而文件传输这种吞吐敏感的应用开启Nagle更划算。6. 拥塞控制网络堵了TCP怎么自救6.1 慢启动从零开始试探网络容量TCP连接刚建立时发送方不知道网络能承受多少所以采用慢启动策略初始拥塞窗口cwnd设为1个MSS现代实现通常初始为10个MSS每收到一个ACKcwnd就翻倍。这意味着每经过一个RTT发送速率就翻一番呈指数增长。慢启动的“慢”是相对而言的实际上它增长很快。它的目的是快速找到网络的承载上限。当cwnd达到**慢启动阈值ssthresh**时就进入拥塞避免阶段。ssthresh初始值通常设为一个很大的数第一次丢包后会被设置为当前cwnd的一半。我个人的经验是慢启动阶段对短连接影响很大。一个网页请求可能只有几十KB如果每次都要从1个MSS开始慢慢涨用户体验会很差。所以现代TCP实现会适当增大初始窗口Google甚至提出过初始窗口10个MSS的建议后来被广泛采纳。6.2 拥塞避免与快速恢复丢包后的优雅降速进入拥塞避免阶段后cwnd不再翻倍而是每个RTT增加1个MSS呈线性增长。这样做的目的是在接近网络容量时缓慢试探避免突然过载。当检测到丢包时根据是超时还是快速重传采取不同策略超时重传认为网络严重拥塞ssthresh设为当前cwnd的一半cwnd直接降到1个MSS重新进入慢启动。这是最保守的反应。快速重传认为网络轻度拥塞ssthresh设为当前cwnd的一半cwnd设为ssthresh而不是1然后进入快速恢复阶段。在快速恢复中每收到一个重复ACKcwnd增加1个MSS直到收到新数据的ACK再降回ssthresh。快速恢复的设计是为了避免像超时那样剧烈降速因为三个重复ACK说明网络还在传输数据只是个别包丢了没必要把速率打到最低。这种区分处理体现了TCP对网络状态的精细判断。6.3 现代拥塞控制算法CUBIC与BBR的取舍经典的TCP Reno/Tahoe在高带宽高延迟网络下表现不佳因为线性增长太慢丢包后降速太狠。于是出现了CUBIC它用三次函数代替线性增长在高带宽下增长更快在接近饱和时增长放缓。CUBIC是Linux默认的拥塞控制算法适合大多数场景。BBR是另一条路线它不把丢包当作拥塞的唯一信号而是主动测量带宽和RTT建立网络模型来调整发送速率。BBR在长肥管道高带宽高延迟和有一定丢包的链路上表现很好但在某些共享瓶颈链路上可能不够公平。选择哪个算法取决于你的网络特征和业务需求。我在实际调优中一般会先看链路特征如果是数据中心内部RTT低、丢包少CUBIC足够如果是跨地域传输RTT高、可能有随机丢包BBR往往更稳。当然最终还是要靠实测数据说话不能凭感觉选。7. 校验和与数据完整性最后一道防线7.1 校验和的计算与验证TCP首部有一个16位的校验和字段覆盖首部、数据和伪首部。发送方计算校验和并填入接收方收到后重新计算如果不一致就丢弃该报文段不发送确认。发送方超时后会重传。校验和用的是反码求和算法把数据按16位分组逐组相加溢出的位回卷加到低位最后取反。这个算法简单高效能检测出大多数单比特和双比特错误。但它不是密码学级别的校验不能防恶意篡改。如果需要更强的完整性保护得靠应用层的TLS或IPsec。提示现代网卡普遍支持校验和卸载计算由硬件完成CPU开销很低。但在虚拟化环境中如果卸载没配好可能导致校验和错误表现为大量丢包。排查时可以用ethtool查看卸载状态。7.2 校验和失效的场景与排查校验和失效通常有几个原因一是链路噪声大比特翻转概率高二是中间设备如某些老旧的NAT设备修改了报文但没更新校验和三是虚拟化环境中的卸载配置不一致。表现是接收方静默丢弃报文发送方超时重传抓包能看到发送方重传但接收方没有对应ACK。排查这类问题时我会先在两端同时抓包对比发送方发出的报文和接收方收到的报文是否一致。如果接收方根本没收到说明中间丢了如果收到了但没回ACK可能是校验和问题。还可以用网卡的统计信息看校验和错误计数Linux下用ethtool -S可以查看。7.3 校验和之外应用层还需要做什么TCP的校验和只能保证传输过程中的比特完整性不能保证应用层数据的语义正确。比如你传输一个文件TCP保证字节流没错但文件本身可能被恶意篡改或者传输过程中应用层逻辑有bug导致数据写错。所以关键业务还需要应用层自己做校验比如MD5、SHA256或者用TLS做端到端加密和完整性保护。我见过一些团队把TCP的可靠性当成万能药应用层不做任何校验结果遇到中间设备篡改或者磁盘写入错误时问题很难定位。TCP的可靠性是“传输层”的可靠性不是“端到端”的可靠性。这个边界要划清楚。8. 常见问题与排查技巧实录8.1 重传率高的常见原因速查表现象可能原因排查手段大量超时重传链路丢包严重、RTO过小抓包看RTT分布检查链路质量大量快速重传偶发丢包、乱序看重复ACK数量检查是否有乱序重传但接收方没收到中间设备丢弃、校验和错误两端同时抓包对比重传后仍失败链路中断、对端宕机检查路由、对端状态重传率低但吞吐差窗口太小、应用读取慢看窗口字段、零窗口探测这张表是我平时排查时的速查清单基本能覆盖八成以上的场景。实际排查时我一般先看重传率再看RTT再看窗口最后看应用层。从下往上排查效率最高。8.2 抓包分析实战一次典型的丢包定位某次线上服务出现间歇性超时应用日志显示请求偶尔要几秒才返回。我在客户端和服务端同时抓包发现客户端发出的某个报文段在服务端没抓到客户端在200ms后重传服务端才收到并回复。进一步看丢失的报文段都比较大接近MSS而且集中在特定时间段。结合链路监控发现那个时间段某条链路的带宽利用率接近饱和推测是中间路由器缓冲区溢出导致丢包。解决办法是调整TCP的拥塞控制算法为BBR同时优化应用层发送节奏避免突发大流量。调整后重传率明显下降超时问题缓解。这个案例的教训是TCP的可靠性机制能兜底但兜底是有代价的。与其等丢包后重传不如从源头减少丢包。拥塞控制算法和应用层发送策略的配合往往比单纯调TCP参数更有效。8.3 几个容易踩的坑和独家建议第一个坑是盲目调大接收缓冲区。有人觉得缓冲区越大越好其实不然。缓冲区太大会导致内存浪费而且可能掩盖应用层读取慢的问题。合理的做法是根据带宽延迟积BDP来计算BDP 带宽 × RTT。比如100Mbps带宽、100ms RTTBDP约1.25MB缓冲区设在这个量级就够了。第二个坑是忽略TIME_WAIT状态。主动关闭连接的一方会进入TIME_WAIT持续2MSL通常60秒。如果短连接很多会积累大量TIME_WAIT占用端口资源。解决办法是使用连接池、开启tcp_tw_reuse谨慎使用或者让客户端主动关闭。第三个坑是把Nagle算法和延迟确认一起用。Nagle算法攒小包延迟确认等大包两者叠加可能导致额外的延迟。交互式应用建议设置TCP_NODELAY关闭Nagle。提示调TCP参数前一定要先测量不要凭感觉改。用ss -i可以查看每个连接的详细统计包括RTT、重传、拥塞窗口等是排查问题的利器。9. 从机制到直觉我对TCP可靠性的一点体会把TCP的可靠性机制全部拆完你会发现它没有哪一项是特别高深的黑科技无非是确认、重传、排序、窗口、拥塞控制这些基本手段的组合。但组合的方式极其讲究每一处都体现了对网络不确定性的深刻理解。序列号解决了乱序和重复累积确认和SACK平衡了效率和抗丢包RTO动态计算适应了RTT变化快速重传缩短了恢复时间滑动窗口防止了接收方过载拥塞控制防止了网络崩溃校验和兜住了比特差错。这些机制不是孤立工作的而是相互配合、相互制约。比如重传会触发拥塞控制降速窗口会影响重传的触发时机校验和失败会导致静默丢弃进而触发超时重传。理解TCP的可靠性关键不是记住每个机制的细节而是理解它们之间的联动关系。我在实际工作中最大的体会是TCP的可靠性是“尽力而为”的可靠性不是“绝对”的可靠性。它能在大多数情况下保证数据正确送达但遇到极端情况如长时间断网、对端崩溃依然会失败。所以应用层不能完全依赖TCP该做的超时、重试、幂等、校验一个都不能少。TCP帮你解决了传输层的问题但端到端的可靠性终究是应用层和传输层共同的责任。最后分享一个我常用的排查思路遇到网络问题先分层。物理层看链路网络层看路由和丢包传输层看TCP统计应用层看业务日志。TCP的可靠性机制主要在传输层起作用但问题的根因往往在更下层或更上层。别一上来就盯着TCP参数调先搞清楚问题出在哪一层能省很多时间。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →