← 网络基础学习第 23 / 55 章高级35 分钟

模块二 · 运输层

TCP 效率机制:Nagle、延迟确认与 Keepalive

TCP 除了"可靠"和"别发太快",还面对三个"什么时候发"的选择:有数据就立刻发,还是攒一攒?收到数据立刻回确认,还是等一等?空闲的连接怎么知道对方还活着? 这三个问题各有机制,而且其中两个撞在一起会引发一个经典性能陷阱。

本章要解决的问题:TCP 除了"可靠"和"别发太快",还面对三个"什么时候发"的选择:有数据就立刻发,还是攒一攒?收到数据立刻回确认,还是等一等?空闲的连接怎么知道对方还活着? 这三个问题各有机制,而且其中两个撞在一起会引发一个经典性能陷阱。

本章的证据来源:历史实测记录(TCP 相关注册表项、netsh int tcp show global)。机制出自 RFC 896(Nagle)、RFC 1122(延迟确认)、RFC 9293(TCP Keepalive)。

先从一个例子理解概念

连续发送几个很小的片段,会反复付出头部开销;等一等合并可能省开销,又可能让交互变慢。这里讨论的就是这些时机取舍。

未确认时先攒小写入:Nagle

Nagle算法在有未确认数据时可能暂存新的小写入,等确认或达到一定大小再发。它是条件规则,不是固定每个包都等一段时间。

确认稍等一下:延迟ACK

延迟确认(延迟ACK)允许接收端短暂等一等,减少单独确认消息。与特定小写入时序相遇时可能产生停顿,但时长和发生条件不固定。

应用选择不用Nagle:TCP_NODELAY

TCP_NODELAY是应用可设置的socket选项名,用于禁用该socket的Nagle。它不保证每次应用写入都立刻变成一个独立线上包。

闲着时探测对方:Keepalive

Keepalive(保活探测)是TCP可选机制;应用心跳是业务自己周期交换的消息。两者目的相近但层次与启用方式不同,不能把一个注册表空值当成功能开关。

下面再用这些概念看原理、命令和结果。规范与官方资料列在本章末尾,类比只帮助理解,不代替协议规则。

三个"什么时候发"的问题

前面的章节解决了"可靠"和"别太快",但还有一类问题没讲:节奏。

问题机制代价
有数据就立刻发,还是攒一攒?Nagle 算法攒起来会增加延迟
收到数据立刻回 ACK,还是等一等?延迟确认等一等会增加延迟
空闲连接怎么知道对方还活着?Keepalive探测包占用额外流量

这三个机制体现了同一个权衡:用一点延迟换取更高的效率。 而这个权衡在某些情况下会反过来咬人。

Nagle 算法:别发那么多小包

先回顾一个实测数据(《全景与基础》模块算过):

本机 WLAN 平均每个包约 561 字节,而 MTU 是 1500

平均包只有 MTU 的三分之一。 说明网络里充满了"没装满"的小包。

小包的问题在哪? 《全景与基础》模块也算过:100 字节的数据实际要占 158 字节的线路容量,包头占 37%。大量小包会严重浪费带宽。

Nagle 算法(RFC 896)的规则很简单:

效果:

应用连续写了 3 次 1 字节:
  没有 Nagle:发出 3 个小包(每个都带 40 字节包头)
  有 Nagle:第 1 个立即发,第 2、3 个攒起来,等第 1 个的确认回来后合并成 1 个包发出

代价:如果应用是"写一个小请求,等一个响应"这种交互模式(典型的请求-响应协议),在存在未确认数据时,后续小写入可能等待ACK或达到MSS;延迟ACK、重传等会影响等待,不能给出无条件一个RTT的上界。

Nagle 是默认开启的。 要关掉它,应用需要设置 TCP_NODELAY 选项。

延迟确认:别回那么多 ACK

对称的问题在接收侧:如果每收到一个段就立刻回一个 ACK,全网会充满纯粹的 ACK 包。

延迟确认的做法是:收到数据后等一小段时间再回 ACK。

为什么要等? 因为它期望"这段时间里正好有数据要往回发"—— 那样 ACK 就可以搭在数据包上一起走(TCP 的 ACK 是捎带在数据包里的),省掉一个独立的小包。

经典陷阱:Nagle 撞上延迟确认

这两个机制各自看都很合理,但组合起来会产生一个很糟糕的结果。 这是网络编程里最有名的一个性能陷阱:

场景:请求-响应式的通信(客户端发一个小请求,等一个小响应)

客户端(开了 Nagle):
  ① 发出请求的最后一个片段的剩余部分
  ② 应用想再写一点数据,但 Nagle 说:"还有未确认的数据,先攒着"

服务端(开了延迟确认):
  ③ 收到了请求,本该回 ACK
  ④ 但延迟确认说:"等 200ms,看看有没有数据要顺便带回去"

结果:
  ② 在等服务端的 ACK(Nagle 才肯发)
  ④ 在等 200ms 计时(才肯发 ACK)
  → 双方都在等对方,每个来回都要多等 200 毫秒

在这类特定写入时序下可能出现等待,实际时长受接收端ACK策略和系统版本影响;不是每次请求必定增加200ms。对于一个需要多次往返的协议,累积起来就是几百毫秒到几秒的额外延迟。

这类问题在游戏、实时交易、以及自己实现的请求-响应协议里非常常见。

怎么解决

办法做法
关闭 Nagle应用设置 TCP_NODELAY,让数据立刻发出
关闭延迟确认Windows 上设置 TcpAckFrequency = 1(立刻回 ACK)
让应用一次写完再发避免"写一点、等一会、再写一点"的模式

实践中最常用的是第一种:需要低延迟的应用(游戏、实时通信)直接关掉 Nagle。

实测:本机的 TCP 调优项全是默认

原教程查询的若干注册表项为空。这只能说明这些查询位置未发现显式值,不能证明整机没调优,也不能证明某个socket的Nagle、ACK策略或Keepalive状态。

应用可逐socket设置TCP_NODELAY、SO_KEEPALIVE等选项,系统也有模板、接口和版本差异。Nagle与延迟ACK造成停顿还需满足特定写入与确认时序,不能由全局空值宣布本机每次请求多等200ms。Microsoft TCP socket选项

Keepalive:怎么知道对方还活着

最后一个问题:一条空闲的 TCP 连接,怎么判断对方还在?

问题的严重性在于:TCP 连接是双方各自维护的状态,没有一个"连接"的实体存在于网络上。如果:

  • 对方突然断电 / 强制重启
  • 中间的 NAT 设备超时清理了映射表(《网络层》模块讲过)
  • 网线被拔掉

这些情况下,本机是不知道的 —— 它会一直认为连接是好的。这叫半开连接(half-open)。

症状:应用往一条"其实已经死了"的连接写数据,写操作成功返回(数据进了本地缓冲区),可能暂时收不到应用响应,随后由重传、错误或应用超时发现失败。

Keepalive 做的事:空闲一段时间后,发一个探测包(实际上是一个"序号无效"的空包),看对方有没有正常回应。

实测:本机的 Keepalive 参数

KeepAliveTime     = (未设置,用系统默认)
KeepAliveInterval = (未设置,用系统默认)

按 Microsoft 文档记载,Windows 的默认值:KeepAliveTime = 7,200,000 毫秒 = 2 小时。

两个要点:

① Keepalive 默认是关闭的。 即使参数有默认值,连接也不会自动启用 Keepalive —— 需要应用主动设置 SO_KEEPALIVE 选项。所以"参数没设置"和"功能没启用"是两件事。

② 2 小时对应用来说太长了。 没有哪个应用能接受"连接死了两个小时才发现"。所以:

  • 大多数应用自己实现心跳(应用层定期发一个业务层的小包)
  • 这也是为什么你会看到各种协议都有自己的"ping/pong"消息

实测:另外几个效率相关的开关

Fast Open                           : enabled
Fast Open Fallback                  : enabled
Receive Segment Coalescing State    : enabled
机制作用
TCP Fast Open允许在三次握手的第一个 SYN 包里就携带数据,省掉一个 RTT。对于短连接(比如大量 API 调用)效果明显
Fast Open Fallback如果对方不支持 Fast Open,自动退回标准握手,不影响兼容性
接收段合并(RSC)网卡硬件把连续到达的多个小段合并后交给协议栈,减少中断和处理开销(上一章讲过)

常见误解

误解一:"Nagle 是为了省带宽,所以总是好的"

它确实省带宽,但代价是延迟。对于交互式应用(游戏、实时通信、请求-响应协议),这个代价往往不值得 —— 所以这些应用普遍设置 TCP_NODELAY 关掉它。

误解二:"延迟确认纯粹是坏事"

不是。它减少了 ACK 包的数量,而且如果恰好有数据要回,ACK 可以搭车一起走,反而更高效。它只是在和 Nagle 组合时才会变成问题。

误解三:"Keepalive 是默认开着的"

不是。 参数有默认值,但功能需要应用主动启用(SO_KEEPALIVE)。而且默认的 2 小时间隔对应用来说基本没有实用价值,所以实际心跳大多由应用层自己实现。

误解四:"注册表项没设置说明功能没启用"

要分清"参数"和"开关"。 比如 Keepalive:参数没设置 → 用默认值(2 小时);而功能启用与否由应用的代码决定,跟注册表无关。相反,Nagle 和延迟确认是默认启用的,注册表项是用来关闭或调整它们的。

本章小结

结论类型
Nagle 算法合并小包:有未确认数据时先攒着,减少包头浪费标准
Nagle可能等待未确认数据的ACK或达到MSS;等待不具有通用一个RTT上界协议机制
延迟确认合并 ACK,期望搭在回程数据上一同发送标准
特定分段写入与ACK策略可能产生Nagle/延迟确认停顿有条件机制
解决办法:应用设 TCP_NODELAY,或设 TcpAckFrequency = 1标准
实测:本机九项 TCP 相关注册表设置全部为未设置,即全默认实测
查询位置无注册表值,不能证明每个socket的Nagle和ACK状态观察限制
Keepalive 用于发现半开连接,但默认不启用,需应用设置 SO_KEEPALIVE标准
Windows 默认 KeepAliveTime 为 2 小时,对应用过长,故应用多自建心跳文档记载(本机为未设置)
实测:本机 TCP Fast Open、Fast Open Fallback、接收段合并均为 enabled实测
全局策略值不能单独证明收益、默认设计原因或逐连接行为推断限制

自测题

7 题 · 先自己答,再展开对照

  1. Nagle 算法解决什么问题?代价是什么?
    Nagle在有未确认数据时合并新小写入,通常减少小段;代价是可能等待ACK或达到MSS。等待受ACK与损失影响,不保证最多一个RTT。
  2. 一个应用连续调用三次 write() 各写 1 个字节。开了 Nagle 和没开,网络上分别会看到什么?
    不能只由三次write断定一定3包或2包。是否已收到ACK、系统调度、缓冲、MSS和卸载都会影响分段;可在明确时序下演示Nagle,真实包数需抓包。
  3. 延迟确认在等什么?它期望得到什么好处?
    它在等"这段时间里是否有数据需要往回发"。 因为 TCP 的 ACK 可以捎带在数据包上(piggyback),如果恰好有回程数据,ACK 就能搭车一起走,省掉一个独立的小 ACK 包。所以延迟确认的好处是减少纯 ACK 包的数量(具体定时与ACK策略依系统及连接,不能统一固定200ms)。
  4. 描述一下 Nagle 和延迟确认"互相等待"的完整过程。什么条件下可能产生停顿,为什么不能固定为每次200ms?
    请求分两次小写入、后一部分被Nagle暂存,而接收端延迟ACK等条件同时成立时,可能等待ACK定时器。不是每个请求都停顿,时间也不固定200ms。
  5. 你在一台服务器上发现所有 TCP 注册表项都是"未设置"。这说明哪些机制处于启用状态?
    只说明查询位置没有显式值;逐socket选项、TCP模板和系统版本仍影响行为,不能证明Nagle开启、机器未调优或已发生停顿。
  6. Keepalive 解决什么问题?为什么它的默认 2 小时间隔在实践中基本没用?
    解决"半开连接"问题:对方断电、重启、或者中间 NAT 设备清理了映射表时,本机是不知道的,会一直以为连接还好,往一条已死的连接写数据,之后可能由数据重传、连接错误或应用超时发现失败。Keepalive 通过定期发探测包来发现这种情况。
    2 小时太长的原因:没有应用能接受"连接死了两小时才发现"。而且Keepalive 默认是关闭的(需要应用主动设置 SO_KEEPALIVE),所以实践中心跳基本都由应用层自己实现(各种协议的 ping/pong 消息)。
  7. "注册表项没设置"和"功能没启用"是一回事吗?举一个例子说明。
    不是一回事。 要区分"参数"和"开关":
    - 参数没设置 → 使用系统默认值。比如 KeepAliveTime 没设置,就用默认的 2 小时。
    - 功能是否启用由别的地方决定。比如 Keepalive 即使参数存在,默认也不启用 —— 需要应用代码调用 SO_KEEPALIVE。
    - 反过来,Nagle 和延迟确认是默认启用的,注册表项是用来关掉或调整它们的。
    所以看到"未设置"时要具体分析:它可能是"默认开着",也可能是"默认关着"。

参考资料与适用范围

修订核对日期:2026-10-13。规则按所列协议与实现范围理解;文中的原始命令输出是历史观察,不表示本次重新实测。跨版本、代理/隧道及不同捕获位置需要另行核对。

正文源自 m2-08-TCP效率机制.md · 共 12 节 · 7 道自测题