打开一个网页时,浏览器可能同时下载 HTML、图片和脚本;后台的聊天软件仍在收消息,系统也可能正在做域名查询。网络层能把数据报送到目标主机,但“送到哪一个应用”“丢了怎么办”“发多快才不会压垮接收方或网络”,都需要传输层回答。
这里沿着一条清晰主线展开:先把主机到主机的交付扩展成进程到进程的交付,再从一个几乎不做额外保证的 UDP 出发,逐步构造可靠传输,最后进入 TCP 的连接、流量控制和拥塞控制。学完后,你不仅能说出 TCP 与 UDP 的差别,还能解释每个机制为什么存在、它解决了什么问题,以及它付出了什么代价。

传输层提供的是不同主机上应用进程之间的逻辑通信。所谓“逻辑”,是指应用看起来像在直接把消息交给远端进程,虽然数据实际上会经过协议栈封装、许多链路和路由器。发送端把应用消息切分为适合传输的部分,加上传输层首部形成报文段,再交给网络层;接收端执行相反过程,把载荷交给正确的应用。
传输协议主要运行在端系统。中间路由器通常只依据网络层首部转发数据报,不替端系统维护 TCP 序号,也不替 UDP 应用重传。这个边界非常重要:网络核心负责“把包往目标主机送”,端系统负责“把数据交给正确进程并实现所选的端到端语义”。
上层能得到什么服务,受下层能力限制。若网络层可能丢包、乱序和延迟,传输层不能让物理链路凭空不丢包,却可以借助确认、序号、缓存和重传,在端系统之间构造可靠交付。相反,若底层不能保证时延上限,TCP 也无法向应用承诺每个字节必在固定毫秒内到达。
互联网中最经典的两个选择是 UDP 和 TCP。二者都提供端口复用、分用和校验和;UDP 基本止步于此,而 TCP 继续提供可靠、有序的字节流、流量控制、拥塞控制和连接管理。

“端到端”不意味着数据不经过中间设备,而是关键通信状态由两端维护。可靠性、序号、接收窗口和拥塞窗口都属于端点之间的协议状态。
一台主机上往往同时有许多套接字。发送时,传输层从多个套接字收集数据,为每份数据添加源端口、目的端口等首部字段,再交给网络层,这叫多路复用。接收时,传输层检查到达报文段的字段,把载荷送入正确套接字,这叫解复用。
端口号占 16 位,取值范围是 0~65535。服务器通常在约定端口上监听,客户端通常由操作系统选择临时端口。端口只在一台主机的协议上下文中有意义;“目的 IP + 目的端口”才能表达要送到哪台主机上的哪个服务入口。
UDP 套接字通常由本地 IP 与目的端口标识。两个 UDP 数据报只要目的 IP、目的端口相同,通常就会进入同一个接收套接字,即使它们来自不同源 IP 和源端口。应用仍可从每个数据报附带的源地址得知发送者,并把响应发回去。
TCP 服务器可以让许多客户端同时连接同一个监听端口,因为每条已建立连接由四元组区分:
(源 IP, 源端口, 目的 IP, 目的端口)例如,两个客户端都访问服务器的 443 端口,只要源 IP 或源端口不同,它们就是两条不同连接。监听套接字接收新连接,操作系统为每条连接建立独立的已连接套接字,分别维护序号、缓存和计时器。
不要把“端口号”理解为进程永久身份证。一个进程可以拥有多个套接字,多个进程也可能通过端口复用选项共享本地端口;真正的分用规则由协议、地址、端口和套接字状态共同决定。
UDP 是无连接协议:发送数据前不握手,端点不维护连接状态,每个应用数据块作为独立数据报发送。它不承诺到达、不承诺按序、不去重,也不内置重传、流量控制和拥塞控制。它提供的核心增量是端口复用/分用和端到端差错检测。
为什么仍然需要 UDP?第一,没有连接建立时延,应用想发就发;第二,首部固定为 8 字节,状态和处理开销小;第三,应用可自行决定发送时机、丢包后的处理方式以及是否需要局部可靠;第四,一次发送对应一个完整数据报,保留消息边界。DNS 查询、实时语音、在线游戏状态、测量上报以及基于 UDP 构建的现代安全传输都能利用这些特征。

UDP 首部包含四个 16 位字段:源端口、目的端口、长度、校验和。长度包括首部与载荷,因此最小值是 8。接收方依据长度判断 UDP 报文段边界,依据目的端口分用,依据校验和检查传输中是否出现比特变化。
计算时,把参与校验的内容视为一串 16 位字。先做反码加法:若最高位产生进位,就把进位回卷加到低 16 位;最后对结果逐位取反,写入校验和字段。接收方把所有 16 位字(包括校验和)相加,理想结果应为全 1。UDP 校验还覆盖包含源/目的 IP、协议号和长度的伪首部,从而发现“报文送错端点”等问题。

UDP 的简洁不等于可以无节制发送。没有内置拥塞控制的应用若持续高速灌入网络,会造成队列溢出并挤压其他流量。负责任的 UDP 应用仍应做速率限制、拥塞响应、超时与必要的安全保护。
可靠数据传输的目标,是让上层看到一条不丢失、不损坏、不重复且按序的交付通道,即使下层信道可能损坏或丢失分组。理解可靠机制最有效的方法,是逐步增加故障条件,并观察协议必须增加哪些状态。
若分组永不损坏也不丢失,发送方收到上层数据后直接发送,接收方收到后直接交付即可。此时没有确认、序号和计时器。
接收方用校验和检测损坏,并用 ACK 表示正确收到、NAK 表示需要重传。发送方发出一个分组后必须等待反馈,这种方式叫停等协议。问题随之出现:如果 ACK 或 NAK 自身损坏,发送方不知道对方的真实状态。
解决办法是给数据分组加入序号。停等场景只需 0 和 1 两个序号:接收方看到期望序号才交付并切换期待值;看到重复序号则不再次交付,只重发对上一份数据的确认。这样,即使发送方因反馈损坏而重传,也不会让上层收到重复数据。进一步还可以不用 NAK,只用带序号的 ACK;对旧序号的重复 ACK 就等价于告诉发送方“当前分组还没被正确接收”。
仅靠序号仍不够,因为发送方可能永远等不到反馈。于是引入倒计时计时器:发送一个分组时启动计时器,超时前收到正确 ACK 就停止;超时则重传。计时器可能过早到期,造成不必要的重复,但序号与去重规则能保证正确性。
一个完备的停等可靠协议通常包含四类工具:

可靠传输不是某个单独字段带来的,而是一组相互配合的状态机规则。校验和只负责“发现可见错误”,真正恢复依靠确认、序号、重传和接收端去重。
停等协议的正确性很直观,但长时延链路上利用率很差。假设发送一个分组只需 1 毫秒,而往返时延是 30 毫秒,那么发送方绝大多数时间都在等待。流水线允许多个尚未确认的分组同时在途,以窗口填满“带宽 × 往返时延”形成的管道。
流水线带来三个变化:序号空间要扩大;发送方和接收方要缓存更多分组;协议必须规定乱序分组、确认和重传范围。两种经典方案在这里分岔。
发送方维护一个最多容纳 N 个未确认分组的窗口。窗口左边是已确认分组,窗口内是已发送未确认与可立即发送的序号,右边暂不可发送。接收方只接受当前期望序号,乱序分组直接丢弃,并重复发送最近按序分组的累计 ACK。
发送方通常只为最老未确认分组维护一个计时器。若它超时,就从该分组开始,把所有已发送但未确认的分组重新发送。GBN 状态简单,但一个分组丢失可能连带重传许多已经到达的后续分组。
接收方对窗口内每个正确分组单独确认,并缓存乱序分组。发送方为每个未确认分组维护状态与计时器,只重传真正超时的分组。当缺口补齐后,接收方才把连续数据按序交付并滑动窗口。
SR 节省带宽,却需要更复杂的缓存与计时管理。它还有一个容易忽略的约束:若序号空间为 K,窗口通常不能超过 K/2,否则序号回绕后,接收方可能无法区分“很久以前的旧分组”和“新一轮的分组”。
TCP 提供的是面向连接、全双工、可靠、有序的字节流。它不是消息协议:应用连续写入的两块数据,接收端可能一次读出,也可能分多次读出。TCP 会把发送缓存中的字节切成报文段,单段应用数据的上限常用 MSS 描述;MSS 通常根据路径允许的 IP 数据报大小来选取,以避免不必要的 IP 分片。
TCP 连接是点到点的,一条连接只关联两个端点,不支持在一条连接上原生广播或多播。两端都维护发送缓存、接收缓存、序号、确认状态和多个计时器。

TCP 按字节编号,而不是按报文段编号。若某报文段序号为 1000,携带 500 字节,那么下一个连续报文段序号是 1500。接收方返回 ACK=1500,意思是 1500 之前的字节已按序收到,下一字节应从 1500 开始。
若收到高于期望值的报文段,说明字节流中出现缺口。TCP 规范允许实现选择缓存乱序段;无论是否缓存,累计确认号都不能越过缺口。重复 ACK 因此能够成为丢包信号。

“ACK=5000”不是“只确认了编号 5000 的字节”,而是累计确认到 4999,并声明下一步期待 5000。把 ACK 理解成“下一个期望字节”最不容易出错。
超时值若太短,会引起大量不必要的重传;若太长,真实丢包后恢复又会很慢。TCP 不直接拿一次采样 RTT 当超时值,而是用指数加权移动平均平滑网络抖动。
EstimatedRTT = (1 - α) × EstimatedRTT + α × SampleRTT
DevRTT = (1 - β) × DevRTT + β × |SampleRTT - EstimatedRTT|
Timeout = EstimatedRTT + 4 × DevRTT常用权重是 α = 0.125、β = 0.25。平滑 RTT 表示中心趋势,偏差项为波动留出安全余量。发生超时后,重传计时常采用指数退避,避免拥塞时过于激进地再次发送。对已重传的报文段,单凭 ACK 无法判断它确认的是原始发送还是重传副本,因此 RTT 采样应避开这种歧义。
TCP 的发送方维护最老未确认字节的重传计时器。出现以下信号时会采取动作:
接收方通常会使用延迟 ACK:在较短时间内等待第二个连续报文段,以减少纯 ACK 数量;若乱序段到达,则应立即重复确认当前期望字节,帮助发送方尽快识别缺口。SACK 选项还能告诉发送方哪些不连续区间已收到,从而避免重传已缓存数据。
可靠传输解决“数据能否正确到达”,流量控制解决“接收应用来不来得及读取”。接收方为连接分配接收缓存,已到达但尚未被应用读取的字节会占用空间。它通过 TCP 首部的接收窗口 rwnd 告诉发送方当前剩余容量。
可把接收方状态写成:
rwnd = RcvBuffer - (LastByteRcvd - LastByteRead)发送方要保证未确认在途字节不超过对方通告的 rwnd。当应用读取数据后,接收窗口会增大;当应用停止读取而缓存逐渐填满,接收方可通告零窗口。发送方不能从此永久沉默,因为窗口更新报文也可能丢失;它会周期性发送很小的窗口探测,让接收方有机会重新通告非零窗口。
rwnd 与 cwnd 不要混淆rwnd 反映接收端缓存压力,属于流量控制;cwnd 反映发送方对网络承载能力的估计,属于拥塞控制。发送方实际可用窗口还要扣除已经在途的数据:
允许在途数据 ≤ min(rwnd, cwnd)
可继续发送量 = min(rwnd, cwnd) - 已发送未确认量
TCP 在传数据前必须让双方确认彼此可达并同步初始序号。最典型的建立过程是三次握手:
SYN=1, seq=x,声明自己的初始序号并请求建立连接。SYN=1, ACK=1, seq=y, ack=x+1,既确认客户端,也声明自己的初始序号。ACK=1, ack=y+1。服务器收到后,双方进入已建立状态;第三步报文可以携带应用数据。为什么不是两次?两次握手无法让服务器确认客户端确实收到了服务器的初始序号,也更难排除网络中滞留的旧 SYN 触发半开连接。第三次确认把双方的发送与接收方向都闭合起来。

TCP 是全双工的,两条发送方向可以独立关闭。一方发送 FIN,表示自己不会再发送新字节;对方确认后仍可继续发送剩余数据,直到它也发送 FIN。于是常见序列是 FIN → ACK → FIN → ACK,中间也可能把 ACK 与 FIN 合并。
主动关闭方在发送最后 ACK 后通常进入 TIME_WAIT。等待一段时间有两个目的:若最后 ACK 丢失,仍能响应对方重发的 FIN;让旧连接中的迟到报文在网络中消散,避免污染相同四元组的新连接。
若服务器收到 SYN 就立刻为每个半开连接分配大量状态,攻击者可伪造源地址耗尽队列。SYN Cookie 的核心思路,是把必要状态编码进服务器选择的初始序号,先不分配完整连接状态;只有客户端返回合法 ACK 时才恢复信息并建立连接。
流量控制只观察接收端;即使接收端缓存很大,中间路由器的输出链路仍可能成为瓶颈。多个发送方把数据注入网络的总速率超过某处容量时,队列增长、时延升高,缓存最终溢出,这就是拥塞。
拥塞代价不只是“丢几个包”:
端到端拥塞控制不依赖路由器明确告知原因,发送方从丢包、重复 ACK、RTT 增长等现象推断拥塞。网络辅助拥塞控制则由路由器直接反馈,例如在队列趋于拥塞时标记分组,接收方再把标记回显给发送方。
“没有丢包”不等于“没有拥塞”。当队列正在持续增长时,RTT 已经恶化,只是缓存尚未溢出。只追求把路由器缓存填满,往往会得到高延迟而不是更高的有效吞吐。
TCP 发送方用拥塞窗口 cwnd 限制在途未确认数据。若近似认为窗口中的数据每个 RTT 都能得到确认,则平均发送速率约为:

发送速率 ≈ cwnd / RTT经典控制呈现“探测—反馈—收缩”的闭环:顺畅时增加窗口,出现拥塞信号时减小窗口。核心阶段如下。
连接开始时对可用容量几乎一无所知,因此从较小 cwnd 起步。每收到一个确认新数据的 ACK,cwnd 增加约一个 MSS。一个 RTT 内大约有 cwnd/MSS 个 ACK,所以窗口每个 RTT 近似翻倍。名称中的“慢”是相对旧式一次性猛发而言,实际增长是指数级的。
当 cwnd 达到慢启动阈值 ssthresh 后,继续翻倍太激进,于是改为每个 RTT 约增加一个 MSS。实现上可让每个新 ACK 增加 MSS²/cwnd,累积一个窗口的 ACK 后总增量约为一个 MSS。
超时通常表示拥塞更严重:发送方把 ssthresh 设为丢包前窗口的一半附近,把 cwnd 降到很小并重新慢启动。三个重复 ACK 表明部分后续分组仍能到达,网络仍在转发,于是可执行快速重传,并用快速恢复在减半后的窗口附近继续,而不必完全回到起点。
这种“加性增大、乘性减小”会形成锯齿状窗口:逐步探索容量,遇到拥塞明显收缩。它既追求高利用率,也努力让竞争连接趋向公平。
Tahoe 与 Reno 的主要分界发生在重复 ACK 触发的丢包上:Tahoe 无论何种丢包都把 cwnd 降到很小并重新慢启动;Reno 判断重复 ACK 说明仍有分组在流动,因此进入快速恢复,只把窗口降到拥塞前的一半附近。若发生重传超时,二者都采取更保守的回退。
对长连接做一个宏观近似:假设拥塞发生时窗口为 W,乘性减小后窗口约为 W/2,随后每 RTT 线性增长到 W。那么一个锯齿周期内的平均窗口约为 3W/4,平均吞吐量约为 3W/(4RTT)。这也解释了为什么相同丢包窗口下,RTT 更短的连接往往增长更快。
在高带宽、大 RTT 路径上,每 RTT 只增加一个 MSS 可能过于缓慢。CUBIC 把拥塞避免阶段的窗口写成“距上次拥塞后经过时间”的三次函数:丢包后先快速回到上次最大窗口 Wmax 附近;接近 Wmax 时增长变缓,谨慎观察旧瓶颈是否仍在;越过该点仍无拥塞时再次加速,探索更高容量。它保留慢启动和快速恢复的基本框架,但比纯线性增长更适合大带宽时延积路径。
另一类方法不等队列溢出,而是观察 RTT。发送方记录接近无排队时的最小 RTT,并比较当前吞吐与 cwnd / 最小RTT 所代表的理想吞吐。如果实际 RTT 上升,说明瓶颈队列正在积累,发送方可以提前减速;如果差距很小,则继续加速。Vegas 属于典型的时延型思路,BBR 则尝试估计瓶颈带宽与传播时延,用二者乘积附近的在途数据填满路径而不过度堆积队列。时延信号更早,却也要面对反向路径排队、测量噪声以及与激进丢包型流竞争等问题。
支持显式拥塞通知时,路由器可在队列拥塞但尚未丢包前设置 IP 首部标记;接收方用 TCP 标志回显,发送方像处理拥塞事件一样降低窗口,并确认自己已响应。这样可以不等到真正丢包才控制速率。
若两条 RTT 相近的长连接共享同一瓶颈,且都采用加性增大、乘性减小,它们的吞吐量会倾向于靠近公平点。但“每条连接公平”不等于“每个应用公平”:一个应用可建立多条并行 TCP 连接,从而获得更大份额;UDP 流若不响应拥塞,也可能挤压响应式流量。RTT 不同的连接还会以不同频率收到 ACK,短 RTT 流往往更快增加窗口。
现代传输功能持续演进。新的拥塞控制可能依据带宽和最小 RTT 建模,而不只把丢包当信号;多路径传输可让一条逻辑连接利用多个网络接口;面向数据中心的方案可以结合显式标记,让交换机浅队列保持低时延。所谓“TCP”在工程中其实是共享报文格式与互操作规则的一系列实现选择,而不是只有一种固定拥塞算法。
QUIC 展示了另一条演进路径:它用 UDP 承载,却在端点实现连接、加密、可靠传输、流量控制和拥塞控制。连接建立与安全握手合并,可减少串行往返;一条连接内可以创建多条独立可靠流,每条流分别按序交付。一条流丢失数据时,其他流若不依赖该丢失分组,仍可继续向应用交付,从而避免 TCP 单字节流跨应用对象的队头阻塞。连接 ID 还把连接身份与固定四元组适度解耦,移动设备从 Wi-Fi 切换到蜂窝网络时可以验证新路径并迁移连接。由于主要逻辑位于用户态,协议更新不必等待操作系统内核普遍升级。
关键结论是:可靠性、拥塞控制和连接语义并不专属于某个固定首部,它们是端点通过协议状态机共同实现的服务。运行在 UDP 之上也绝不代表可以绕开拥塞责任;承载协议简单,上层就更需要完整实现网络友好的控制。
排查 TCP 问题时,不要只看“连接慢”,而要沿时间线寻找证据:
一次有条理的观察可以这样做:先用四元组过滤单条连接;定位 SYN、SYN-ACK、ACK 计算握手 RTT;检查首个数据段序号和累计确认;标注重传与重复 ACK;比较接收窗口、在途字节和 RTT 随时间的变化;最后再判断瓶颈来自发送端、接收端还是网络路径。
rwnd 保护接收方,cwnd 保护网络,实际发送同时受二者限制。