从字节流到拥塞控制:TCP可靠传输与拥塞控制全解析

发布时间:2026/10/16 18:00:59
从字节流到拥塞控制:TCP可靠传输与拥塞控制全解析 第三章的笔记写到第三篇我的原计划是先把TCP整体的逻辑跑通再落笔因为这一章后半段的任何结论单独拿出来硬记都没用必须和前面的可靠传输原理串起来。教材用大量篇幅讲了UDP和rdt然后用几乎同样多的篇幅讲TCP和拥塞控制真正刷下来的感觉是前一半是基础框架后一半是前者的实战应用。很多同学看到拥塞控制就怕觉得又是算法又是状态机我一开始也这样后来才知道只要把每一个状态转换背后的动机问清楚这一章反而比第二章还好懂。这篇分享覆盖的是教材第三章从TCP报文段结构开始一直到TCP拥塞控制结束的内容。换句话说UDP、多路复用和多路分解、rdt那部分不在本篇范围如果你刚读到第三章中间可以先补一下前面的基础。本篇会用大量为什么来推送结论不堆定义目的是让你合上书之后能自己把TCP的工作过程从头到尾推出来。1. TCP报文段在字节流上画坐标而不是按包子打包1.1 序号和确认号字节流才是TCP的坐标系很多人刚接触TCP头时会被序号和确认号两个字段绕晕因为TCP明明是分段发送的为什么编号单位不是段而是字节这正是TCP和普通可靠传输协议最本质的区别。TCP站在应用层的角度把数据看成一条连续字节流所以seq表示的是这个报文段中第一个字节在整个字节流中的位置ack表示的是接收方期望收到的下一个字节位置。举个例子假设A要发送5000字节给B分成5个段每段1000字节。第一个段的seq0第二个段seq1000第三个seq2000依次类推。B收到前两个段后会回复ack2000意思是我已经收到字节0到1999期待你从字节2000开始发。这和很多协议里收到第几号包就回第几号包的语义完全不同也正因为编号落在字节上TCP天然支持累计确认哪怕只回一个确认号也能覆盖之前所有连续收到的数据。我刚开始学的时候有个误点总觉得ack2000就代表收到了1至1999也就是序号2000前面的所有后来发现自己默认把序号看成从0开始很顺但实际抓包时看到的seq是不固定的。教材第一章就提过双方握手时各自选择初始序号不一定是0所以真正要记的逻辑是ack 对方发来的初始seq 已连续收到的字节数。用字节作单位的好处是即使某个段丢了后面到达的段也不会被立刻丢掉接收方会把这些乱序但连续的段缓存起来等缺口补上后再一次性交付给应用层。1.2 标志位和握手为什么三次就够为什么挥手要四次TCP报文段头里有一组标志位最核心的是SYN、ACK、FIN。连接建立时双方要同步初始序号所以有了三次握手。第一次客户端发SYN1, seqx第二次服务器回SYN1, ACK1, seqy, ackx1第三次客户端再回ACK1, seqx1, acky1。这里有个我当年没想明白的问题为什么不能两次教材给出的核心原因是防止失效的连接请求突然到达服务器让服务器白白建立半开连接。如果是两次握手一个在网络里滞留很久的旧SYN可能导致服务器误以为客户端要建立新连接从而分配资源三次握手让客户端再确认一次服务器收不到第三次ACK就知道这次连接建立不成立不会把资源耗在幻觉连接上。至于挥手为什么需要四次要从全双工这个特性出发。TCP连接里两个方向的数据流是独立的关闭时必须各自独立结束。A发送FIN表示A方向上不再有数据要发了但B还可能继续向A发数据所以B先用ACK回应这个方向的关闭等自己的数据全部发完再向A发送FINA再确认。四次挥手的本质是两次单向关闭的组合。实战中还会遇到同时关闭、以及加TIME_WAIT状态等问题但理解到双向独立关闭这一层后面那些状态机细节就都顺了。我还想补充一个经常考但容易被忽略的语义TCP连接的标识是四元组——源IP、源端口、目的IP、目的端口。很多同学只记IP端口尤其在学Socket编程后会下意识认为是两个套接字一一对应一条连接。实际上服务端可以同时和多个客户端保持不同连接只要四元组里的元素不完全重复每条连接就是独立的。2. 超时与重传让TCP在不可靠信道里建立自己的节拍2.1 RTT估算为什么不能只用平均值TCP可靠传输的核心是发送数据、启动定时器、超时重传。但这个定时器设多少毫秒直接决定效率设太短网络稍波动就会盲目重传设太长一次丢包就让吞吐率断崖式下跌。教材给出的方案是追踪SampleRTT也就是从发出某个段到收到它对应ACK所经历的时间然后用指数加权移动平均来平滑EstimatedRTT (1 - α) × EstimatedRTT α × SampleRTTα默认是0.125。这个公式的本质和我们在性能监控里常说的EWMA一模一样给新样本的权重很小防止单次抖动带偏整体估计。只算平均还不够因为网络延迟的波动幅度是随机的平均偏移1毫秒和波动100毫秒对超时判断的影响完全不同。所以TCP在平均之外还维护一个偏差估计DevRTTDevRTT (1 - β) × DevRTT β × |SampleRTT - EstimatedRTT|β默认是0.25。最终超时间隔取TimeoutInterval EstimatedRTT 4 × DevRTT这个公式我很喜欢它不绕弯子用4倍偏差把不确定性兜住。你可以联想生活中约人见面如果只是算平均迟到5分钟那你提前5分钟到往往还是被放鸽子更好是平均迟到5分钟迟到波动4倍才有把握准时等到人。2.2 重传后的样本不能用来训练预测模型这里有个非常关键的工程细节如果一个报文段超时重传了TCP不会用这个重传段后来的ACK去更新SampleRTT。原因是我们收到ACK时根本没法确定这个ACK是对第一次发送还是第二次发送作出的响应。如果拿重传后的ACK去算RTT可能算出一个极其虚低或虚高的样本污染整个估计。这就是Karn算法想解决的核心问题。不过教科书也提到超时之后会启动指数退避连续超时则把超时时间翻倍而不是重传给一个固定值。原理和以太网退避类似防止网络已经拥塞时重传风暴继续踩踏已经脆弱的链路。2.3 快速重传不等超时的信号超时机制固然可靠但它有一个天然缺陷等待是昂贵的。一个段丢了发送方要憋到超时结束才重传这一个RTT的损失在高速网络里十分明显。TCP的设计者从接收方的行为里发现了一个信号如果接收方收到乱序段会立刻对已收到的最后一个连续字节回复一个重复ACK。也就是说发送方连续收到3个相同的ACK时基本可以断定某个段是真的丢了而不是简单的乱序。这时TCP立刻重传不走超时流程这个动作叫快速重传。这里我要强调一下为什么是3个重复ACK而不是2个或4个。教材的解释是一个重复ACK可能只是某个段乱序到达两个重复也仍然可能是乱序程度稍深连续3个就在乱序和丢失之间形成了足够强的置信度。可以理解成一次敲门可能是恶作剧两次敲门也可能是巧合连续三次一致的信号就该开门看看了。实际工程里快速重传能把丢包恢复时间从两个超时周期压缩到一个RTT以内在丢包率较高但延时不长的网络上对吞吐的影响是数量级的。3. 流量控制接收端视角的限速器和它的别名陷阱3.1 接收窗口rwnd从接收方的感受出发学完可靠传输后再看流量控制很容易误以为它和拥塞控制是一回事其实两者面对的对象完全不同。流量控制解决的是接收方来不及处理的问题和网络拥塞没有半点关系。接收方在TCP报文段头里有一个16位的接收窗口字段rwnd它告诉发送方我现在的接收缓冲区还能装下多少字节。发送方的基本约束是LastByteSent - LastByteAcked ≤ rwnd换句话说发送方把所有已发送未确认的数据量控制在接收方当前可接收的范围之内。接收方的rwnd不是固定值。当应用层慢慢从缓冲区读取数据时rwnd会逐渐变大当应用层读得慢、缓冲快满了rwnd就会变小。这个字段放在每个ACK里带回给发送方相当于接收端在每一条反馈里同时通告我已经收到哪里了以及我还能吃下多少。我当年学这块时最大的错觉是以为流量控制和可靠传输用的是同一个窗口。实际上可靠传输的滑动窗口说的是哪些数据可以发、哪些要等确认而流量控制只管别一次性塞爆接收方的缓冲区。它们一个管顺序一个管容量完全不冲突。发送方真正受的最终约束是这两个窗口的交集也就是后文要说的min(cwnd, rwnd)。3.2 零窗口和探测包一次容易被忽略的边界状况如果接收方连一个字节都收不下会通告rwnd0。此时接收方的确不能处理新数据但这不意味着发送方就可以彻底闭嘴。因为发送方一旦不再发包就可能收不到接收方后续发出的窗口更新如果接收方在窗口更新包中也丢了双方就死锁了。TCP的解法是发送方即使看到rwnd0也要隔一段时间发出一个只有1字节数据的探测段。接收方收到后会强制回复一个ACK并在ACK里带上当前最新的rwnd从而打破死锁。这个机制我在读书时几乎一眼带过后来在模拟拥塞场景时才发现它的价值。如果你用netstat观察长连接在接收缓冲区满时仍然能看到少量1字节的探测包周期性出现这不是协议出问题而是TCP的自救机制。理解零窗口后还能顺带解释一个面试里常见的问题为什么TCP首部里rwnd只有16位最大只有65535字节现代网络动不动几MB的窗口怎么办答案是TCP引入了窗口缩放选项在握手阶段协商一个缩放因子把字段实际能表达的范围扩大。能想到这一层说明你是真的在理解协议而不是背字段。这里还可以做一个小对比把流量控制和拥塞控制放在一张表里强迫自己分清对比项流量控制拥塞控制保护对象接收方应用进程的接收缓冲区路由器队列、整条网络路径作用范围发送端和接收端点对点发送端到网络全局反馈信号接收方在ACK里通告的rwnd超时、重复ACK控制窗口rwndcwnd这张表我一直保留到读第四章因为后面讲述的网络拥塞状态正是在流量控制之上再加一层约束没有这张对比表两个概念很容易滚进一个坑里。4. 拥塞控制网络没有红绿灯丢包就是唯一信号4.1 为什么不能只用流量控制拦住一切如果把流量控制当成唯一限制整个网络会遭遇一种典型的局部理性、全局崩溃。发送方和接收方之间的链路状态很好接收方缓冲区很空发送方就放开手脚猛发可中间经过的路由器可能早已排队溢出。路由器丢包后发送方看到的是超时或重复ACK却不知道丢包发生在哪一跳更不知道网络上还有多少其他连接在抢带宽。TCP面对这种黑箱没有中间节点给它发红绿灯信号只能通过端到端的观察去推断拥塞程度这就是所谓端到端拥塞控制。教材在第三章专门拿出一节讲拥塞控制原理核心结论是丢包并不可怕可怕的是当丢包率升高后发送方如果继续按原速率发送只能是让丢包更多、吞吐更差。一个在理想无拥塞情况下吞吐率随发送速率线性上升的网络一旦超过拐点继续增加负载反而会造成吞吐率下降。拥塞控制的本质就是让发送方在探测更多带宽和不要压垮网络之间维持平衡。4.2 AIMD加性增乘性减背后的动机TCP拥塞控制的基本动作被概括为AIMD加法增加(AI)和乘法减小(MD)。为什么会选择这个组合而不是按比例增加、按比例减小或线性增加、线性减小关键在不对称性。加法增加拥塞窗口cwnd每个RTT增加1个MSS。这样带宽探索是渐进式的网络状态好时慢慢抬高水位不会因为一次盲目暴涨把链路打崩溃。乘法减小一旦检测到丢包ssthresh立刻降为当前cwnd的一半甚至更强。之所以要乘性减是因为要快速退出危险的拥塞状态。乘性减在一次拥塞中就把流量砍去一半能迅速缓解网络压力给其他排队的数据让路。这就是典型的慢启动、快反应。现实中如果你用脚本去压测某个服务也会发现稳定状态下一段一段地加并发一旦出现超时错误立刻把并发腰斩往往比慢慢降更能挽救整体成功率。TCP的AIMD就是这种运维直觉的协议化。需要留意的是拥塞控制里两个重要丢包信号意味着不同级别的拥塞超时的信号更严重因为只有队列满溢路由器完全丢弃长期排队的数据TCP才会等不到ACK而三个重复ACK意味着仍然有后续数据能返回说明网络还能勉强消化流量。教材据此设计了两套恢复策略超时后要回到慢启动走最保守的路径重复ACK则可以走快速恢复保留一定带宽。这个差异在第五章里非常关键。5. 拥塞窗口三态切换从慢启动到快速恢复的完整手感5.1 慢启动指数试探和它的停止条件拥塞窗口cwnd初值通常只有1个MSS。慢启动的意思是每收到一个新的ACKcwnd就增加1个MSS。你可能会奇怪这哪里慢了在一个RTT里因为20个ACK会陆续回来所以cwnd大概在一个RTT内翻倍从1到22到44到8这是指数增长一点不慢。那为什么叫慢启动历史视角是相对于一开始就发送一大窗数据从1个MSS慢慢拉升已经是很有节制了。指数增长的目的是快速找出当前链路能承受的带宽大致范围因为刚开始连接时我们对该路径完全没有了解如果线性探测要花几十个RTT才能爬上去太不值得。慢启动一直增长到cwnd到达一个阈值ssthresh为止。第一次连接时ssthresh往往设得很大比如64KB所以慢启动会一直冲直到出现丢包信号才停下来。5.2 拥塞避免在谨慎和效率之间摆动一旦cwnd达到或超过ssthreshTCP进入拥塞避免阶段。此时每个RTTcwnd只增加1个MSS也就是从指数变成线性。教材里常把这个阶段画成一条平缓上升的直线配合慢启动的陡峭曲线形成先锐角拉升、再平缓爬坡的视觉印象。我建议大家不要只在图上看要亲手推一遍如果MSS是1000字节ssthresh是16000字节那么在慢启动阶段会经历1、2、4、8、16单位这样的翻倍进入拥塞避免后则变成17、18、19……每一步都只是上一个RTT加1。这里有个我当年反复犯错的点从慢启动切到拥塞避免并不是因为网络拥塞而只是到达了一个我们觉得可能危险的阈值。真正让流程发生跳变的永远是事件要么来了一个超时要么来了三个重复ACK。5.3 快速恢复丢包后不回到起点的折中当收到三个重复ACK时TCP先做两件事把ssthresh设为cwnd/2再用快速重传把丢失的段补发出去。至于cwnd本身Reno算法把它设为ssthresh 3×MSS然后进入快速恢复状态。之后的每个重复ACKcwnd再加1个MSS相当于网络还能往回传数据我就再挤一点带宽出来直到收到一个新的ACK确认丢失段已被补齐再把cwnd降到ssthresh回到拥塞避免继续线性增长。快速恢复的核心思路是多个重复ACK说明网络还没死透丢包可能仅仅因为偶发拥塞不必夸张地回到1个MSS重新慢启动。但如果是超时那说明网络状态严重得多TCP才会把cwnd重置为1然后重新进入慢启动。这两种策略的对比也正好呼应了第四章超时意味着路径大概率已经严重拥塞的结论。5.4 一个手推的完整例子把状态机变成直觉为了把三态跑通我常用一个手算例子ssthresh初始值设成16个MSScwnd从1开始。RTT记为1个回合第1回合cwnd1第2回合cwnd2第3回合cwnd4第4回合cwnd8第5回合cwnd16到达ssthresh进入拥塞避免第6回合cwnd17第7回合cwnd18假设第8回合开始时传来三个重复ACK说明数据在第7回合发送过程中丢了。按照Reno逻辑把ssthresh降为9cwnd先设为12ssthresh 3进入快速恢复如果在恢复期间又收到一个新的重复ACKcwnd继续加到13、14……等到新ACK到达确认丢失段恢复cwnd被降到9重新回到拥塞避免并从第9回合开始每回合加1。这个例子非常简单但我建议你一定自己动手按时间线把ACK数量算出来而不是只看数字结论。因为真正的难点不是状态转换表而是你能否意识到cwnd的每一步变化都对应着一个或一批真实ACK的到达而不是时间到了就自动加。很多同学在笔试里能背出状态转换图可一遇到如果网络延迟变成原来的3倍慢启动增速会怎么变化就懵就是因为脑子里只有状态图没有每条ACK都是changing trigger这个动态直觉。如果把这一章的TCP拥塞控制理解为一个人的行为准则那整章其实就讲了四句话带宽要一点点试探风险来了要果断收缩不同的丢包信号代表不同程度的危险每次调整都来自于对方的反馈而不是自己的猜测。第三章的最后一节还讨论了公平性多个TCP连接在带宽分配上如何达到近似公平这是AIMD机制的宏观结果。学完这一章再去翻第四章路由选择你会发现网络层解决的是包往哪走而传输层解决的是包应该以多快的速率走——两者一脉相承。我在实际操练中最大的感受是光靠读书是不够的必须自己搭一个小实验环境用两台虚拟机和一条限制带宽的网桥去观察cwnd的真实变化。当你在抓包工具里亲眼看到SYN之后cwnd按1、2、4、8的节奏爬升又在一个RTT里因为重复ACK被腰斩那种理解程度是任何文字都替代不了的。第三章内容多、公式多但只要抓住字节序号、RTT采样、接收窗口、丢包信号四条主线后面的拥塞控制也就是顺水推舟的事。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询