运输层核心机制详解:从UDP到TCP的可靠传输与拥塞控制

发布时间:2026/10/21 14:44:44
运输层核心机制详解:从UDP到TCP的可靠传输与拥塞控制
运输层这块内容我前前后后翻过不下五遍不同版本的教材每次重读都有新的体会。原因很简单——它是整个网络体系里最“接地气”的一层。你写代码时调用的 socket 接口、排查问题时抓的包、面试时被反复追问的 TCP 三次握手全都落在这层。但很多朋友学到这里会卡住概念一堆什么端口、复用、可靠传输、流量控制、拥塞控制单独看都懂串起来就乱。这篇我就按自己理解的路子把运输层从头到尾捋一遍重点讲清楚每个机制“为什么这么设计”而不是只告诉你“是什么”。不管你是正在啃计算机网络的学生还是工作后想补基础的开发者应该都能从中拿到点实在的东西。1. 运输层到底在解决什么问题1.1 从“两台主机通信”到“两个进程通信”的跨越网络层把数据从一台主机送到另一台主机靠的是 IP 地址。但一台主机上同时跑着浏览器、聊天软件、音乐播放器数据到了主机之后到底该交给哪个程序网络层管不了这件事它只负责把包裹送到楼下至于送到几零几房间是运输层的事。这就是运输层最核心的价值提供进程到进程的逻辑通信。物理上数据经过的是一条条链路和一堆路由器逻辑上两个进程就像直接连了一根管子你在这头写我在那头读。运输层用端口号来标识进程用复用和分用来让多个进程共享同一条网络层通道。我习惯用一个类比网络层是快递公司的干线运输把货从A城送到B城运输层是B城里的快递员负责把货分到具体的收件人手上。IP 地址是城市地址端口号是门牌号。少了门牌号货到了城市也没法投递。1.2 复用与分用一个容易被忽略但极其关键的概念复用发生在发送方多个应用进程的数据通过各自的端口交给运输层运输层再统一封装后交给网络层。分用发生在接收方网络层把数据交上来运输层根据端口号把数据分给正确的进程。这两个词听着抽象但你想一下你开着微信、浏览器、下载工具它们同时在收发数据网卡只有一块IP 只有一个凭什么互不干扰靠的就是端口号区分。每个进程绑定一个端口运输层在封装时把源端口和目的端口写进首部接收方一看目的端口就知道给谁。提示端口号是 16 位范围 0 到 65535。其中 0 到 1023 是熟知端口分配给 HTTP、FTP 这类标准服务1024 到 49151 是登记端口49152 以上是临时端口客户端发起连接时由系统动态分配。你自己写服务端程序时别去抢熟知端口容易和系统服务打架。1.3 运输层和网络层的分工边界很多人分不清这两层的职责。我总结成一句话网络层负责“主机到主机”运输层负责“进程到进程”。网络层提供的服务是不可靠的、尽最大努力交付的运输层则可以在不可靠的网络层之上构建出可靠的服务。这里有个关键点网络层不保证数据不丢、不乱序、不重复这些活儿全甩给了运输层。TCP 之所以复杂就是因为它要在一条本身就不可靠的通道上硬生生造出“可靠”的假象。而 UDP 干脆躺平网络层什么样它就什么样只加了一层端口复用别的啥也不管。理解了这个分工你就能明白为什么 TCP 有那么多机制而 UDP 简单到首部只有 8 个字节。复杂度不是凭空来的是为了弥补下层的不足。2. UDP简单到极致的“裸奔”协议2.1 UDP 首部结构拆解UDP 的首部只有 8 个字节四个字段每个 2 字节源端口、目的端口、长度、校验和。就这么点东西没有序号没有确认号没有窗口没有任何控制字段。字段长度作用源端口2 字节发送方进程端口不需要回复时可置 0目的端口2 字节接收方进程端口分用的关键长度2 字节UDP 首部加数据的总长度最小值是 8校验和2 字节检测传输过程中是否出错出错则丢弃校验和这块有个细节值得说UDP 计算校验和时会加一个 12 字节的伪首部里面包含源 IP、目的 IP、协议号、UDP 长度。伪首部不参与实际传输只是为了把 IP 层的信息拉进来一起校验防止数据被错误地送到别的主机。这个设计挺巧妙但也增加了计算量。2.2 UDP 适合什么场景UDP 不保证可靠那它凭什么还存在因为快。没有握手没有确认没有重传发出去就不管了。这种特性在下面这些场景里反而是优势实时音视频丢一两帧画面无所谓但如果为了重传卡住体验更差。直播、语音通话大多跑在 UDP 上。DNS 查询一次请求一个响应数据量小用 UDP 省去建立连接的开销快得多。游戏对战位置同步要求低延迟偶尔丢包可以靠客户端预测弥补重传反而添乱。广播和多播UDP 天然支持一对多TCP 只能一对一。我做过一个实时数据采集的小工具一开始用 TCP结果网络稍微抖动就卡顿因为 TCP 的重传和拥塞控制会把延迟拉高。换成 UDP 之后丢几个包无所谓整体流畅度反而上去了。这就是选对协议的威力。2.3 UDP 的“不可靠”到底意味着什么不可靠不是说出错概率高而是说协议本身不提供任何保证。数据可能丢、可能乱序、可能重复UDP 一概不管。应用层如果在意这些得自己实现。比如你想用 UDP 传文件就得自己在应用层加序号、加确认、加重传逻辑。这活儿干起来不比直接用 TCP 轻松所以除非有特殊需求传文件还是老老实实用 TCP。注意UDP 没有拥塞控制意味着它可以无节制地往网络里灌数据。如果大量应用都用 UDP 猛发网络容易拥塞崩溃。这也是为什么有些网络会限制 UDP 流量。用 UDP 时应用层最好自己做点速率控制别把网络当自家水管。3. TCP在不可靠之上造出可靠3.1 TCP 首部逐字段精讲TCP 首部最少 20 字节比 UDP 复杂得多。我把它拆成几块来看源端口、目的端口各 2 字节和 UDP 一样用于分用。序号4 字节标识本报文段数据第一个字节的编号。TCP 是面向字节流的每个字节都有编号。确认号4 字节期望收到对方下一个报文段的第一个字节的序号。数据偏移4 位指出首部长度因为首部有可选项长度不固定。保留6 位暂时没用留着以后扩展。六个标志位URG、ACK、PSH、RST、SYN、FIN每个都有特定用途。窗口2 字节告诉对方自己还能接收多少字节用于流量控制。校验和2 字节同样带伪首部。紧急指针2 字节配合 URG 使用。选项可变长度最常用的是 MSS最大报文段长度。序号和确认号是 TCP 可靠传输的基石。序号让接收方知道数据的位置确认号让发送方知道哪些数据已经被收到。这两个字段配合重传机制构成了可靠传输的核心。3.2 三次握手为什么是三次而不是两次三次握手的流程大家都背过客户端发 SYN服务端回 SYNACK客户端再发 ACK。但为什么非得三次两次行不行假设只有两次客户端发 SYN服务端回 SYNACK连接建立。问题在于服务端无法确认客户端收到了自己的 SYNACK。如果这个响应在路上丢了客户端以为没建立成功服务端却以为建立了就会一直等着客户端发数据白白浪费资源。三次握手的本质是双方都要确认对方的收发能力都正常。第一次让服务端知道客户端能发第二次让客户端知道服务端能收也能发第三次让服务端知道客户端能收。三次之后双方都确认了彼此的收发能力连接才算真正建立。还有一个经典问题已失效的连接请求。客户端发了一个 SYN网络延迟很久才到服务端此时客户端早就不等了。服务端收到后回 SYNACK如果没有第三次握手服务端就单方面建立了连接一直等数据浪费资源。有了第三次客户端不会回 ACK服务端就知道这个连接是无效的不会建立。3.3 四次挥手与 TIME_WAIT 状态关闭连接需要四次挥手因为 TCP 是全双工的每个方向要单独关闭。客户端发 FIN服务端回 ACK此时客户端到服务端的方向关了但服务端到客户端的方向还开着。等服务端也发 FIN客户端回 ACK连接才完全关闭。这里最让人头疼的是TIME_WAIT 状态。主动关闭的一方在发完最后一个 ACK 后要等 2MSL报文最大生存时间的两倍才能彻底关闭。为什么要等一是确保最后的 ACK 能到达对方。如果这个 ACK 丢了对方会重发 FIN此时如果客户端已经关闭就没法回应了。等 2MSL 就是为了能重发 ACK。二是让本次连接的所有报文在网络中消失。如果立即关闭旧连接的延迟报文可能被新连接误收造成数据混乱。等 2MSL足够让所有旧报文消亡。实操心得服务器上大量 TIME_WAIT 是常见现象尤其在高并发短连接场景。可以通过调整内核参数来缓解比如开启 TIME_WAIT 重用。但别盲目调理解清楚原理再动手否则可能引入更难排查的问题。4. 可靠传输的三大机制4.1 停止等待协议与连续 ARQ 协议最朴素的可靠传输是停止等待发一个报文段等确认收到确认再发下一个。简单是简单但效率极低。假设往返时间 RTT 是 20ms那每秒最多发 50 个报文段带宽利用率惨不忍睹。改进方案是连续 ARQ不等确认连续发多个报文段收到确认后滑动窗口往前移。这样信道利用率大幅提升。TCP 实际用的就是滑动窗口机制配合累积确认。累积确认的意思是确认号 N 表示 N 之前的所有字节都收到了。比如收到确认号 1001说明 1 到 1000 字节都到了。这样即使中间某个确认丢了后面的确认也能覆盖它减少重传。4.2 超时重传与自适应 RTT 计算发送方发了一个报文段多久没收到确认就重传这个时间叫超时重传时间 RTO。设太短会频繁重传浪费带宽设太长丢包后恢复慢延迟高。TCP 的做法是动态计算。它先测量每个报文段的往返时间 RTT然后算一个加权平均的平滑往返时间 RTTs再算一个RTT 偏差 RTTd最后 RTO RTTs 4 × RTTd。这样 RTO 能自适应网络状况的变化。具体公式是这样的第一次测量时RTTs 取测量值RTTd 取测量值的一半。之后每收到一个新确认就更新RTTs (1 - α) × 旧RTTs α × 新RTT测量值α 通常取 0.125RTTd (1 - β) × 旧RTTd β × |RTT测量值 - RTTs|β 通常取 0.25RTO RTTs 4 × RTTd这套算法让 TCP 能根据网络延迟的变化自动调整重传时机比固定超时时间聪明得多。4.3 快速重传不等超时就重传超时重传有个问题要等到 RTO 到了才重传太慢。快速重传解决了这个问题如果发送方连续收到三个重复的确认就认为后面的报文段丢了立即重传不用等超时。举个例子发送方发了 1、2、3、4、5 五个报文段2 丢了。接收方收到 1回确认 2收到 3还是回确认 2收到 4继续回确认 2收到 5依然回确认 2。发送方收到三个重复的确认 2就知道 2 丢了马上重传 2不用等 RTO 超时。这个机制把丢包恢复时间从“一个 RTO”缩短到“三个重复确认到达的时间”在高延迟网络里效果非常明显。5. 流量控制与拥塞控制5.1 滑动窗口与零窗口探测流量控制是为了防止发送方发得太快把接收方的缓冲区撑爆。TCP 用滑动窗口实现接收方在确认报文里带上自己的接收窗口大小发送方根据这个值调整发送速率。如果接收方缓冲区满了会通告一个零窗口发送方就停止发送。但这里有个死锁风险如果接收方后来腾出空间发了一个窗口更新的报文但这个报文丢了发送方还在等接收方也在等双方僵住。解决办法是零窗口探测发送方收到零窗口后启动一个定时器每隔一段时间发一个探测报文接收方收到后回复当前窗口大小。这样即使窗口更新报文丢了也能通过探测恢复。5.2 慢开始、拥塞避免、快重传、快恢复拥塞控制和流量控制不是一回事。流量控制看的是接收方能力拥塞控制看的是整个网络的状况。TCP 的拥塞控制有四个核心算法慢开始连接刚建立时拥塞窗口从 1 开始每收到一个确认就翻倍指数增长。虽然叫“慢开始”但增长其实很快。拥塞避免当拥塞窗口达到慢开始门限后改为线性增长每个 RTT 加 1避免增长过快导致拥塞。快重传收到三个重复确认就立即重传不等超时。快恢复快重传后把慢开始门限和拥塞窗口都减半然后执行拥塞避免而不是回到慢开始。这四个算法配合起来让 TCP 能在网络状况好时快速提速在出现拥塞时迅速降速整体表现相当稳健。阶段触发条件窗口变化慢开始连接建立或超时指数增长直到门限拥塞避免窗口达到门限线性增长每 RTT 加 1快重传三个重复确认立即重传丢失报文段快恢复快重传之后门限和窗口减半进入拥塞避免提示实际网络里拥塞控制算法有很多变种比如 CUBIC、BBR 等不同系统默认用的可能不一样。理解经典的四个算法是基础但别以为 TCP 就只有这一套。6. 常见问题与排查实录6.1 连接建立失败怎么查连接建不起来最常见的原因是端口没开、防火墙拦了、或者服务端没监听。排查顺序我一般是这样先用telnet或nc试一下目标端口通不通。服务端用netstat或ss看端口有没有在监听。检查防火墙规则看是否放行了对应端口。如果都正常抓包看 SYN 有没有发出去有没有收到 SYNACK。抓包是最直接的手段。如果 SYN 发出去了但没响应多半是网络路径或防火墙问题如果收到了 RST说明端口没开或服务没起。6.2 传输慢、卡顿的排查思路传输慢的原因很多我按经验列个速查表现象可能原因排查方法延迟高但带宽没用满网络拥塞或 RTT 大测 RTT看是否触发拥塞控制降速带宽用满但速度上不去窗口太小检查接收窗口和拥塞窗口频繁重传链路质量差抓包看重传率间歇性卡顿丢包导致超时重传看是否有大量超时事件我遇到过一次传输慢的问题最后发现是接收方窗口设得太小发送方一直填不满管道。调大窗口后速度立刻上去了。所以别只盯着带宽窗口大小同样是瓶颈。6.3 TIME_WAIT 过多怎么办高并发短连接场景下服务器上 TIME_WAIT 堆积是常态。处理方式有几种开启tcp_tw_reuse允许重用 TIME_WAIT 状态的连接但只对 outgoing 连接有效。调整tcp_max_tw_buckets限制 TIME_WAIT 数量超了就直接清理。从架构上改用长连接减少连接建立和关闭的频率。我个人更倾向于从架构层面解决比如用连接池复用连接。调内核参数是治标改架构才是治本。而且乱调参数可能引入新问题比如重用连接导致旧数据串进来。6.4 几个容易混淆的概念澄清流量控制 vs 拥塞控制前者管接收方后者管网络。一个是别撑爆对方缓冲区一个是别压垮网络。序号 vs 确认号序号标识自己发的数据确认号告诉对方自己期望收到什么。SYN vs FINSYN 用于建立连接FIN 用于关闭连接都占一个序号。RTO vs RTTRTT 是实际往返时间RTO 是重传超时时间RTO 根据 RTT 动态计算。这些概念初学容易混多抓几次包对着实际数据看很快就清楚了。我当初就是靠反复抓包把三次握手和四次挥手的每个字段都对照了一遍才真正记住。运输层的内容看着杂但主线很清楚UDP 简单直接TCP 复杂可靠所有机制都是为了在不可靠的网络层之上给应用层提供不同级别的服务。理解每个设计背后的“为什么”比死记硬背字段有用得多。我自己的习惯是学完一个协议就抓一次包看着真实的数据流动很多疑惑自然就解开了。