技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTNET

TCP 协议灵魂之问,巩固你的网络底层基础

所属馆藏
NET Protocol
文件格式
Markdown
原始路径
NET Protocol/08-TCP协议灵魂之问,巩固你的网路底层基础
本文目录14 个章节
  1. 001. 能不能说一说 TCP 和 UDP 的区别?
  2. 002. 说说 TCP 三次握手的过程?为什么是三次而不是两次、四次?
  3. 003. 说说 TCP 四次挥手的过程
  4. 004. 说说半连接队列和 SYN Flood 攻击的关系
  5. 005. 介绍一下 TCP 报文头部的字段
  6. 006. 说说 TCP 快速打开的原理(TFO)
  7. 007. 能不能说说 TCP 报文中时间戳的作用?
  8. 008. TCP 的超时重传时间是如何计算的?
  9. 009. 能不能说一说 TCP 的流量控制?
  10. 010. 能不能说说 TCP 的拥塞控制?
  11. 011. 能不能说说 Nagle 算法和延迟确认?
  12. 012. 如何理解 TCP 的 keep-alive?
  13. 总结
  14. 参考资料

TCP 协议灵魂之问,巩固你的网络底层基础

Category(分类): NET Protocol Status: 未知

本文尽量保留原文的面试题结构、比喻、公式和图片,同时依据 RFC 9293(TCP)、RFC 6298(RTO)、RFC 5681/RFC 9438(拥塞控制)、RFC 2018(SACK)、RFC 7323(TCP 时间戳)、RFC 7413(TCP Fast Open)等资料修正过时或不准确的说法。

先亮出这篇文章的思维导图:

TCP 知识体系思维导图

TCP 作为传输层协议,是软件工程师网络基础素养的重要组成部分,也是面试中经常被问到的知识点。本文梳理 TCP 的核心问题,并补充现代实现中常见的 SACK、RACK、TFO、拥塞控制、时间戳和连接保活等内容。需要注意,HTTP/1.1 和 HTTP/2 常运行在 TCP 之上,而 HTTP/3 运行在 QUIC 之上,不能把所有现代 HTTP 都简单等同于 TCP。

001. 能不能说一说 TCP 和 UDP 的区别?

首先概括基本区别:

TCP 是面向连接、可靠、基于字节流的传输层协议。

**UDP 是无连接的数据报传输协议。**不过“UDP 没有 TCP 的其他特性”只能作为入门记忆,不能理解为 UDP 完全没有校验、状态或任何可靠能力:UDP 有校验和,应用也可以在 UDP 之上自行实现确认、重传和拥塞控制,QUIC 就是典型例子。

TCP 的三个核心特性

  1. 面向连接:通信双方在传输应用数据前通常通过三次握手交换初始序列号并建立 TCP 状态。UDP 不需要 TCP 式的连接建立过程,但 UDP Socket 也可以调用 connect() 记录默认对端,这不等于建立了 TCP 连接。
  2. 可靠性:TCP 通过序列号、确认、重传、校验和、接收窗口和拥塞控制等机制,尽量把字节按序交付给对端应用。TCP 会记录哪些字节已经确认、哪些仍在飞行中,并在检测到丢失时重传。
  3. 面向字节流:TCP 不保留应用层消息边界。应用连续写入两次数据,对端可能一次读到,也可能分多次读到;应用必须使用固定长度、分隔符、长度前缀或其他 framing 方案自行划分消息。

UDP 的特点是:

  • 每次发送一个独立数据报,接收端保留数据报边界;
  • 不保证送达、不保证顺序、不保证不重复;
  • 协议本身不提供 TCP 那样的连接状态、重传、流量控制和拥塞控制;
  • 具有 UDP 校验和,但校验和只用于发现部分传输错误,不等于可靠传输;
  • 适合由应用自行权衡可靠性、时延和开销的场景。

TCP 的“可靠”也不是业务成功保证:连接中断时,尚未被确认的数据可能丢失;即使数据已经送到对端,业务是否落库仍需要应用层确认。

TCP 和 UDP 对比

对比项TCPUDP
连接面向连接无连接数据报
可靠性有序、确认、重传不保证送达、顺序和不重复
数据边界字节流,无消息边界保留数据报边界
流量/拥塞控制协议提供应用或上层协议自行负责
广播/组播通常一对一可一对一、一对多、多对一、多对多
首部最小长度20 字节8 字节
常见用途HTTP/1.1、HTTP/2、SSH、数据库DNS、NTP、实时媒体、QUIC 底层

002. 说说 TCP 三次握手的过程?为什么是三次而不是两次、四次?

恋爱模拟

以谈恋爱为例,两个人能够在一起,首先要确认各自“爱”和“被爱”的能力。下面的比喻只是为了帮助理解 TCP 的双向确认,不代表任何价值判断。

第一次:

男:我爱你。
女方收到。

由此只能证明男方发出的消息到达了女方。

第二次:

女:我收到了你的爱,我也爱你。
男方收到。

男方确认自己能收到女方的消息,但女方还不知道男方是否收到了第二句话。

第三次:

男:我收到了你的爱。
女方收到。

女方终于确认男方收到了自己的消息。TCP 三次握手的核心也是交换双方初始序列号,并让服务端确认客户端收到了服务端的 SYN+ACK。

真实握手

从最开始双方都处于 CLOSED 状态。服务端调用 listen() 后进入 LISTEN,客户端主动发起连接:

TCP 三次握手过程

设客户端初始序列号为 x,服务端初始序列号为 y

  1. 第一次握手:客户端发送 SYN=1, SEQ=x,客户端进入 SYN-SENT
  2. 第二次握手:服务端收到 SYN,发送 SYN=1, ACK=1, SEQ=y, ACK=x+1,服务端进入 SYN-RECEIVED
  3. 第三次握手:客户端验证服务端的 SYN+ACK 后发送 ACK=1, ACK=y+1,客户端进入 ESTABLISHED;服务端收到该 ACK 后也进入 ESTABLISHED

简化表示:

客户端                                  服务端
  SYN, SEQ=x       ───────────────────>
                   <───────────────────  SYN+ACK, SEQ=y, ACK=x+1
  ACK=y+1          ───────────────────>

原文中的 SYN-REVD 是笔误,正确状态名是 SYN-RECEIVED

SYN 会占用一个序列号,所以下一个确认号需要加 1;纯 ACK 不占用序列号。更完整的规则是:SYN 和 FIN 各占用一个序列号,携带数据的报文按数据长度占用序列号,纯 ACK 不占用序列号。

为什么不是两次?

根本原因之一是:两次握手无法让服务端确认客户端收到了自己的 SYN+ACK。

另一个重要原因是避免旧的、延迟到达的连接请求造成半开连接。假设客户端曾发送一个 SYN,但该报文在网络中滞留;客户端以为连接失败而重新发起或放弃连接。若只需要两次握手,服务端收到这个过期 SYN 后发送响应就可能直接认为连接建立,并为一个实际上不存在的客户端分配资源、等待数据。

三次握手让服务端必须等待客户端的第三个 ACK,才能确认客户端收到自己的初始序列号和 SYN+ACK。TCP 规范也要求通过 SYN 交换并确认双方的初始序列号,以降低旧连接报文干扰新连接的风险。

为什么不是四次?

四次当然可以,只是没有必要。三次已经完成:

  • 客户端初始序列号传给服务端并被确认;
  • 服务端初始序列号传给客户端并被确认;
  • 服务端确认客户端收到了第二次握手。

额外的 TCP 控制报文只会增加时延和开销。应用层在 TCP 建连后继续协商协议版本、认证或能力,那不属于 TCP 的“第四次握手”。

三次握手过程中可以携带数据吗?

在经典 TCP 语义中:

  • SYN 报文通常不携带普通应用数据;
  • 第三次握手的 ACK 可以与第一段应用数据合并发送,RFC 9293 的握手示例也展示了第三个报文可以携带数据;
  • TCP Fast Open(TFO)允许客户端在后续连接的 SYN 中携带数据,但这是单独的扩展,必须双方支持并正确处理重放风险。

原文把“前两次绝对不能携带数据、第三次一定安全”说得过于绝对。SYN 阶段的数据处理受到 TCP 扩展、操作系统和应用协议限制;服务端不能因为收到 SYN 就信任并处理任意大量应用数据,仍需防御资源消耗攻击。

同时打开(Simultaneous Open)会怎样?

TCP 允许双方几乎同时主动发起连接。状态通常为:

TCP 同时打开状态变化

  1. 双方从 CLOSED 进入 SYN-SENT,各自发送 SYN;
  2. 双方收到对方不带 ACK 的 SYN 后,进入 SYN-RECEIVED,并回复 SYN+ACK;
  3. 双方收到对方的 SYN+ACK 并发送最终 ACK 后,进入 ESTABLISHED

同时打开在实际公网应用中不常见,但它是 TCP 规范要求支持的状态转换。

003. 说说 TCP 四次挥手的过程

过程拆解

TCP 是全双工连接,两个方向可以独立关闭,因此常见的正常关闭需要四个控制报文,但不是所有关闭都严格产生四个报文。ACK 和 FIN 在某些情况下可以合并,同时关闭也会走不同状态。

TCP 关闭流程概览

假设客户端主动关闭:

  1. 客户端发送 FIN,表示自己不再发送数据,进入 FIN-WAIT-1
  2. 服务端收到 FIN,发送 ACK,进入 CLOSE-WAIT。此时服务端仍可继续发送剩余数据;
  3. 服务端应用完成发送后,TCP 发送 FIN,进入 LAST-ACK
  4. 客户端收到服务端 FIN,发送 ACK,进入 TIME-WAIT;服务端收到 ACK 后通常进入 CLOSED;客户端等待约 2×MSL 后释放连接状态。

原文的 CLOSED-WAIT 是笔误,正确状态名是 CLOSE-WAIT。客户端发送 FIN 后不是“完全不能接收”,而是关闭了客户端到服务端的发送方向,仍然可以接收服务端的数据,这叫半关闭(half-close)。

TCP FIN 报文头部示意

等待 2MSL 的意义

TIME-WAIT 主要有两个目的:

  1. 如果最后一个 ACK 丢失,服务端会重传 FIN;主动关闭方在 TIME-WAIT 中可以再次发送 ACK;
  2. 让旧连接的延迟报文在网络中消失,避免相同四元组快速复用时,旧报文被误认为新连接的数据。

传统解释通常把 2×MSL 拆成两个方向的传播时间:一个 MSL 让最后 ACK 到达对端,另一个 MSL 让对端可能重传的 FIN 到达主动关闭方。更准确地说,TIME-WAIT 是为了同时覆盖最后 ACK 重传和旧连接报文消失这两类风险。

MSL(Maximum Segment Lifetime)是报文段在网络中允许存在的最大寿命。2MSL 是 TCP 规范中的经典等待要求,但现代实现还会结合时间戳、连接复用和系统策略优化,并不意味着所有系统都用一个可观察且固定的 MSL 常数。

为什么是四次挥手而不是三次?

服务端收到客户端 FIN 时,可能还有数据没有发送完,所以通常先回复 ACK 表示已收到关闭请求,等剩余数据发送完后再发送 FIN。

如果服务端当时已经没有数据要发送,ACK 和 FIN 可以合并,抓包中可能表现为三段,而不是严格四段。四次挥手是典型流程,不是 TCP 关闭的唯一报文数量。

同时关闭会怎样?

如果客户端和服务端几乎同时发送 FIN,双方可能经历:

FIN-WAIT-1 → CLOSING → TIME-WAIT

双方分别确认对方 FIN 后进入 TIME-WAIT。这与一方先进入 CLOSE-WAIT、另一方进入 FIN-WAIT-2 的正常关闭路径不同。

TCP 同时关闭状态变化

004. 说说半连接队列和 SYN Flood 攻击的关系

服务端从 CLOSED 进入 LISTEN 后,操作系统通常需要维护握手请求和已完成连接的队列。Linux 等系统常用“半连接队列/请求队列”和“全连接队列/accept 队列”的概念,但队列名称、容量、溢出策略和实现细节不是 TCP 协议本身统一规定的,不能简单认为所有操作系统都严格只有两个队列。

半连接队列

客户端发送 SYN,服务端收到后回复 SYN+ACK,并为后续确认保存必要的请求状态。此时连接通常处于 SYN-RECEIVED,尚未完成三次握手。

全连接队列

客户端返回最终 ACK 后,握手完成。已建立的连接通常进入监听 Socket 关联的 accept 队列,等待应用调用 accept() 取走。应用迟迟不调用 accept()、线程池耗尽或 backlog 太小,都可能让该队列堆积。

SYN Flood 攻击原理

SYN Flood 是 DoS/DDoS 攻击的一种形式。攻击者可以使用伪造源地址或大量真实来源发送 SYN,使服务端持续处理握手请求:

  1. 半连接请求大量积压,正常连接可能被丢弃或延迟;
  2. 服务端需要发送 SYN+ACK 并保留请求状态,重传和超时会消耗 CPU、内存、带宽以及队列容量;
  3. 攻击流量也可能由分布式真实主机发出,不能只理解为“伪造不存在 IP”。

如何应对 SYN Flood

  1. 合理配置监听 backlog、SYN 重传和连接超时;
  2. 启用并正确配置 SYN Cookies。SYN Cookie 不是浏览器 Cookie,也不是完全“不分配任何资源”,它是服务端把必要状态编码到序列号等信息中的防护机制,可能限制部分 TCP 选项;
  3. 使用防火墙、负载均衡、连接限速、上游清洗、Anycast 或云 DDoS 防护分散攻击;
  4. 监控 SYN-RECEIVED、accept 队列、丢包率和 CPU,而不是盲目把队列调得很大;
  5. 减少 SYN+ACK 重传可以降低资源消耗,但会牺牲高延迟网络下的连接成功率,应结合业务和网络环境设置。

005. 介绍一下 TCP 报文头部的字段

TCP 基本首部最小 20 字节,携带选项时最长通常为 60 字节(Data Offset 为 4 位,以 32 位字为单位)。

TCP 报文头部结构

源端口、目标端口

TCP 首部记录源端口和目标端口,IP 首部记录源 IP 和目标 IP。一个 TCP 连接通常由以下四元组和传输协议共同标识:

源 IP、源端口、目标 IP、目标端口、TCP

监听 Socket 只绑定本地地址/端口,accept 得到的连接 Socket 还具有远端地址/端口。IPv4、IPv6、NAT 和多网卡场景下,不能只用一个 IP 和端口描述所有连接。

序列号

Sequence Number 是本报文段中第一个数据字节的序列号;如果设置 SYN,序列号是 ISN,首个数据字节从 ISN+1 开始。TCP 序列号是 32 位,会在 2^32 处回绕。

序列号主要用于:

  1. 在 SYN 中交换双方初始序列号;
  2. 对字节流排序、检测重复和确认已收到的范围;
  3. 配合窗口判断报文是否仍属于当前连接。

它不是“第几个数据包”的编号,也不能把一个 TCP Segment 直接等同于一个应用消息。

ISN

Initial Sequence Number 是连接一方选择的初始序列号。原文所说“每 4ms 固定加一”是对早期 RFC 中时钟组件的过度简化。RFC 9293 保留了大约每 4 微秒递增的时钟思想,同时要求使用连接参数和秘密密钥生成不可由外部直接计算的伪随机偏移,现代实现还会考虑 RFC 6528 的安全建议。

ISN 的目的包括:

  • 降低旧连接延迟报文干扰新连接的可能;
  • 让连接双方同步各自的序列空间;
  • 提高攻击者猜测有效序列号并伪造 RST 或数据的难度。

ISN 不是密码学密钥,不能只依赖“随机”来替代 TCP 的序列号验证、时间戳和 TLS 认证。

确认号

ACK(Acknowledgment Number) 表示发送方期望从对端收到的下一个序列号。通常可以理解为小于 ACK 的字节已经按序被 TCP 接收并确认,但这不等于应用程序已经读取或业务已经处理完成。

控制位

TCP 当前常见控制位包括 CWRECEURGACKPSHRSTSYNFIN,前面还存在扩展的 NS 位。常用含义如下:

  • SYN:同步序列号并请求建立连接;
  • ACK:确认号字段有效;
  • FIN:发送方在该方向没有更多数据,开始有序关闭发送方向;
  • RST:立即中止或拒绝连接,具体行为依赖当前状态;
  • PSH:历史上用于请求接收方尽快把已缓存数据交给应用。它不是应用消息边界,现代 Socket API 通常不会把它暴露给应用,不能据此判断“一个包就是一条消息”;
  • URG:紧急指针相关机制,存在实现差异和中间设备问题,现代新应用通常不应依赖;
  • ECE/CWR:与 ECN(显式拥塞通知)协商和拥塞反馈有关。

原文把 PSH 解释成“收到后马上交给上层且不能缓存”过于绝对;TCP 仍以字节流和接收窗口为核心,PSH 也不是刷新应用消息的可靠方法。

窗口大小

TCP 首部中的 Window 字段为 16 位,表示接收方在当前确认号基础上愿意接收的窗口。窗口缩放选项在 SYN 阶段协商,缩放值通常为 0~14,实际窗口可按 Window field × 2^scale 解释。

窗口缩放只能在连接建立时协商,不能在连接中途直接改变。发送方实际可发送的数据还受到接收窗口 rwnd、拥塞窗口 cwnd、已发送未确认数据和协议实现限制共同影响。

校验和

TCP 校验和占 2 字节,计算时还包含 IP 伪首部,用于发现传输中的部分错误。接收方发现校验和错误通常丢弃报文,发送方因收不到有效确认而可能根据重传机制重新发送;TCP 并不是收到错误报文后由接收方直接发一个“请重传”消息。

可选项

TCP 选项由 kindlength 和具体信息组成,受首部长度限制。常见选项如下:

TCP 选项格式

  • Timestamp(Kind=8,Length=10):携带 TSval 和 TSecr,用于 RTT 测量和 PAWS;
  • MSS:声明本端愿意接收的最大 TCP 数据段长度,通常只在 SYN 中协商;
  • SACK Permitted:在 SYN 中表示支持 SACK;
  • SACK Block:连接建立后报告已收到的非连续数据区间;
  • Window Scale:在 SYN 中协商窗口缩放因子;
  • TFO:TCP Fast Open 使用的 Cookie 选项。

006. 说说 TCP 快速打开的原理(TFO)

TCP Fast Open(TFO)由 RFC 7413 定义,属于 TCP 的可选扩展。它优化的是后续连接发送第一段数据的时机,并不是把 TCP 三次握手完全删除。

TFO Cookie 与 SYN Cookie 不是同一个东西:

  • SYN Cookie 主要是服务端应对 SYN Flood 的防护机制;
  • TFO Cookie 用于证明客户端曾经从某个服务端获得过 TFO 凭据,让服务端决定是否接受 SYN 中的早期数据。

首轮连接

  1. 客户端发送 SYN,并请求 TFO Cookie;
  2. 服务端在 SYN+ACK 中返回 TFO Cookie;
  3. 客户端缓存 Cookie,并正常完成 TCP 三次握手。

首轮连接通常不能因为没有 Cookie 就发送可被服务端立即处理的早期数据。

后续连接

客户端可以在新的 SYN 中携带 TFO Cookie 和数据。服务端验证 Cookie 后,可以在 TCP 三次握手最终 ACK 到达前把这部分数据交给应用处理,同时发送 SYN+ACK。若 Cookie 无效、服务端关闭 TFO 或中间设备不支持,连接通常回退到普通 TCP 流程。

TCP Fast Open 流程

TFO 的早期数据可能被重放,服务端不能把它用于不可重复的副作用操作。HTTPS 场景还要区分:TFO 携带的是 TCP 层早期字节,通常可能是 TLS ClientHello;它不等同于 TLS 1.3 的 0-RTT,也不代表明文 HTTP 请求可以直接在 TLS 建立前处理。

TFO 的优势和限制

  • 对已经获得 Cookie 的客户端,TFO 可以把 TCP 数据发送提前,理想情况下节省约一个 TCP RTT;
  • 实际收益还会受到 TLS、HTTP/2/3、代理、中间盒、服务端策略和丢包影响;
  • TFO 需要客户端、服务端和内核支持,现代部署中是否开启、是否被中间设备剥离并不一致;
  • 早期数据必须考虑重放、幂等和拒绝服务风险。

007. 能不能说说 TCP 报文中时间戳的作用?

TCP Timestamp 是一个可选项,格式为:

Kind(1 字节) + Length(1 字节) + TSval(4 字节) + TSecr(4 字节)

其中 Kind=8Length=10。时间戳由双方在 SYN 阶段协商,未协商时连接中不能直接启用。

主要作用有两个:

  • 辅助计算 RTT;
  • PAWS(Protection Against Wrapped Sequence numbers)防止高速连接中旧报文因序列号回绕而被误认为新数据。

计算 RTT

TCP 时间戳与 RTT 测量示意

没有时间戳时,如果一个数据段经历重传,ACK 到达时很难判断该 ACK 对应第一次发送还是重传,直接计算会得到错误 RTT。现代 TCP 使用 Karn 算法原则:被重传报文通常不用于 RTT 采样,并结合时间戳提高测量精度。

启用时间戳后:

  1. A 发送报文,在 TSval 中放入自己的时间戳 ta1
  2. B 回复报文时,在 TSecr 中回显最近收到的 A 的 TSval,并在 TSval 中放入自己的时间戳;
  3. A 收到包含回显值的 ACK 后,可以用本地当前时间减去被回显的发送时间估算 RTT。

这里的时间戳是每个端点自己的单调递增计数,不要求双方时钟同步,也不一定是操作系统“当前墙上时间”。如果中间有多个报文,TSecr 回显的是协议规定的最近有效 TSval,不应简化成永远只对应某一个固定报文。

防止序列号回绕

TCP 序列号只有 32 位,高带宽连接可能在连接生命周期内很快使用完序列空间。旧数据段和新数据段可能出现相同或相近序列号,接收端需要额外信息判断新旧。

PAWS 在时间戳已经协商、时间戳值满足有效性条件时,使用时间戳拒绝明显过旧的报文。时间戳不是“每个包都有全球唯一编号”,也不是所有 TCP 连接都一定启用;窗口、序列号比较、TIME-WAIT 等机制仍然重要。

008. TCP 的超时重传时间是如何计算的?

TCP 具有超时重传机制:发送方在预期时间内没有收到有效确认时,可能重传未确认的数据。这个时间称为 RTO(Retransmission Timeout)。RTO 不是固定的 3 秒或某一个常数,而是根据 RTT 和 RTT 波动动态计算,并受实现、时钟粒度、拥塞控制和重传退避影响。

经典方法(历史内容)

早期资料曾使用平滑 RTT:

SRTT = α × SRTT + (1 - α) × RTT
RTO  = β × SRTT

其中 α 常取 0.8~0.9。这种方法对 RTT 波动反应不足,今天主要作为理解“平滑平均”概念的历史算法,不应当作为实现 TCP RTO 的完整公式。

RFC 6298 的标准方法

现代 TCP 通常参考 RFC 6298 的 Jacobson/Karels 算法。初次取得 RTT 样本 R 时,典型初始化为:

SRTT   = R
RTTVAR = R / 2
RTO    = SRTT + max(G, 4 × RTTVAR)

后续取得新的 RTT 样本 R' 时,必须先更新 RTTVAR,再更新 SRTT:

RTTVAR = (1 - 1/4) × RTTVAR + (1/4) × |SRTT - R'|
SRTT   = (1 - 1/8) × SRTT + (1/8) × R'
RTO    = SRTT + max(G, 4 × RTTVAR)

其中 G 是时钟粒度。RFC 6298 还规定初始 RTO、RTO 下限和超时后的指数退避等行为,实际系统可能依据后续标准和实现细节调整。

还需要注意

  • 发生 RTO 后通常会把 RTO 加倍,避免在拥塞期间持续过快重传;
  • Karn 算法要求不要直接使用重传报文的 ACK 计算普通 RTT 样本;时间戳可改善部分测量问题;
  • RTO 是判断重传的计时器,不是应用请求超时。应用仍应设置自己的连接、读取和业务超时;
  • TCP 可能通过快速重传、SACK、RACK、Tail Loss Probe 等更早发现丢包,不必总等到 RTO。

009. 能不能说一说 TCP 的流量控制?

流量控制解决的是接收端处理能力有限的问题:发送端不能无限向接收端发送数据,否则接收缓存可能被填满。接收端通过 TCP 首部的 advertised window(接收窗口)告诉发送端当前还能接收多少字节。

TCP 滑动窗口

TCP 中常见的窗口概念包括:

  • 接收窗口 rwnd:接收端根据接收缓存和应用读取速度公布的窗口;
  • 拥塞窗口 cwnd:发送端根据网络拥塞状态计算的发送限制;
  • 发送窗口/可用发送量:通常受 min(rwnd, cwnd) 限制,还要扣除已经发送但未确认的数据。

发送窗口

发送端的窗口通常包含:

  • 已发送且已确认;
  • 已发送但未确认;
  • 未发送但允许发送;
  • 未发送且暂时不能发送。

TCP 发送窗口

TCP 发送窗口变量

常见变量:

  • SND.UNA:最早未确认的序列号;
  • SND.NXT:下一个要发送的序列号;
  • SND.WND:发送方向可用窗口的相关状态。

接收窗口

接收端根据自己的缓存容量和应用读取进度公布窗口:

TCP 接收窗口

RCV.NXT 表示按序期待的下一个序列号,RCV.WND 表示从该位置开始愿意接收的范围。若应用迟迟不读取,窗口可能逐渐缩小到 0,发送方会进入零窗口处理并使用 persist probe 防止死锁。

流量控制示例

假设接收缓存容量为 200 字节,发送端发送 100 字节,接收端 TCP 已经把这 100 字节放入接收缓存,但应用只读取了 40 字节,剩余 60 字节仍在缓存中:

  • TCP 仍可以确认已经按序收到的 100 字节,ACK 不等于应用已经读取 100 字节;
  • 接收端剩余可用缓存为 140 字节,因此后续 ACK 可能公布 rwnd=140
  • 发送端的 SND.UNA 应根据 TCP 实际收到并确认的字节前移 100 字节,而不是只前移应用读取的 40 字节;
  • 发送端新的可发送量还要取 rwndcwnd 的较小值。

原文把 ACK 的前移量写成 40 字节,这是把“应用读取”与“TCP 确认”混淆了,已修正。

流量控制和拥塞控制需要同时满足:接收端窗口足够大,不代表网络允许发送这么多;网络不拥塞,也不代表接收端缓存有空间。

010. 能不能说说 TCP 的拥塞控制?

流量控制关注发送端和接收端,拥塞控制关注整个网络路径。发送得太快会让路由器队列溢出、丢包和时延升高,因此 TCP 需要根据 ACK、丢包、ECN 和时延等信号调节发送速率。

拥塞窗口

  • rwnd 是接收端给出的限制;
  • cwnd 是发送端根据网络状态维护的限制;
  • 实际在途数据和可发送数据通常受 min(rwnd, cwnd) 限制。

cwnd 的单位在不同实现中可能按字节或 MSS 计量。它不是“当前自己还能传输的所有数据量”这一固定值,而是允许在网络中保持的未确认数据上限之一。

慢启动

慢启动用于连接开始或拥塞后重新探测可用容量。经典描述是:在慢启动阶段,每收到一个 ACK,cwnd 约增加一个 MSS,因此经过一个 RTT 后可能近似翻倍,但实际增长会受到 ACK 聚合、延迟 ACK、初始窗口、接收窗口和实现算法影响。

慢启动不是无止境增长,达到 ssthresh 或检测到拥塞后,通常转入拥塞避免或其他控制阶段。初始窗口也不是固定为某一个值,应以当前实现和 RFC 为准。

拥塞避免

经典 Reno 语义下,拥塞避免阶段大致每个 RTT 线性增加约一个 MSS,而不是简单地说每收到一个 ACK 固定增加一个字节。实现通常使用按 ACK 分摊的增量,使一个 RTT 的总增长接近线性。

丢包检测和快速重传

当接收端发现数据缺口时,通常继续发送对缺口位置的累计 ACK。传统快速重传常以收到三个重复 ACK 为触发条件,尽快重传疑似丢失的段,不必等待 RTO。

“三个重复 ACK”是经典 Reno 规则,不是所有现代 TCP 实现唯一的丢包检测方式。ACK 频率、ACK thinning、中间设备和 RACK/TLP 等机制都可能改变检测时机。

选择性确认 SACK

SACK 需要在 SYN 阶段协商 SACK Permitted,建立后接收端可以在 ACK 的 SACK Block 中告知已经收到的非连续数据区间。发送端可以只重传缺失的区间,避免重复发送已到达的数据。

SACK 不是“一个独立可靠的重传协议”,它仍然依赖累计 ACK、序列号和 TCP 重传机制;接收端也不一定在每一个 ACK 中都携带 SACK 块。

快速恢复和现代算法

经典 Reno 快速恢复通常会降低 ssthresh,调整 cwnd,再逐渐增加。真实实现可能使用 Reno、CUBIC、BBR 或其他算法:

  • CUBIC 在 Linux 等系统中很常见,并有 RFC 9438 的规范描述;
  • BBR 依据带宽和 RTT 模型控制发送速率,不完全按 Reno 的“丢包即拥塞”方式工作;
  • RACK、ECN、拥塞控制和尾部丢包探测会影响现代 TCP 的表现。

因此“发生三次重复 ACK 后 cwnd 一定减半并线性增加”应标记为经典算法模型,而不是所有现代系统的必然行为。

011. 能不能说说 Nagle 算法和延迟确认?

Nagle 算法

如果发送端不停地发送很小的字节,例如每次只写 1 字节,就可能产生大量小 TCP 段,增加首部开销和网络处理成本。Nagle 算法用于合并小数据,经典规则可以概括为:

  • 如果没有未确认数据,第一次小数据可以立即发送;
  • 如果已经有未确认数据,后续数据通常等到积累到 MSS,或此前数据得到确认后再发送。

Nagle 并不是把应用的多次写入合并成可靠消息边界,也不是所有小包都一定被延迟。对交互式低延迟应用,可以使用 TCP_NODELAY 禁用 Nagle,或者在应用层批量写入;是否禁用应通过延迟和吞吐测试决定。

延迟确认

Delayed ACK 允许接收端稍等片刻,把确认与后续数据或多个收到的段合并,以减少纯 ACK 数量。经典 TCP 要求延迟通常小于 500ms,并建议至少每两个完整数据段发送 ACK;实际 Linux、Windows 和其他系统往往使用更短的延迟。

收到乱序数据时,接收端通常会尽快发送重复 ACK 或 SACK,帮助发送端发现缺口;具体是否立即确认、如何触发 quickack 取决于实现。tcp_in_quickack_mode 是 Linux 内部实现细节,不是通用 TCP 标准接口。

两者一起使用会怎样?

Nagle 可能延迟发送,延迟 ACK 可能延迟确认,二者组合会在某些请求/响应模式中产生额外延迟,经典上称为“TCP 小包交互问题”。解决方式包括:

  • 合并应用层小写入;
  • 对低延迟交互协议评估 TCP_NODELAY
  • 调整应用消息设计和批处理策略;
  • 不要仅凭经验关闭 Nagle,应结合抓包、RTT 和吞吐测试。

012. 如何理解 TCP 的 keep-alive?

TCP 层 keep-alive 与 HTTP 的持久连接(persistent connection)不是同一个概念:

  • HTTP 持久连接表示同一个应用层连接可以承载多个 HTTP 请求/响应;
  • TCP keep-alive 是在长时间空闲时发送探测段,确认对端或路径是否仍然可达。

如果一方因为宕机、网络断开或 NAT 状态过期而失联,而本地没有要发送的数据,TCP 可能暂时无法知道连接已经不可用。TCP keep-alive 可以帮助发现这类空闲连接,但它不能证明对端应用健康,也不能确认业务请求已经处理。

在 Linux 上可以查看系统级配置:

sudo sysctl -a | grep keepalive

# 常见 Linux 默认值,实际以系统、内核和应用配置为准
net.ipv4.tcp_keepalive_time = 7200
net.ipv4.tcp_keepalive_probes = 9
net.ipv4.tcp_keepalive_intvl = 75

原文注释挤在同一行,已整理。这里的 7200 秒、9 次和 75 秒是常见 Linux 默认值,不是 TCP 协议强制的统一值。RFC 9293 规定 TCP keep-alive 是可选能力,若实现支持,默认空闲探测间隔通常不应小于两小时,但现代应用、容器和云平台经常通过 Socket 选项覆盖它。

为什么很多应用不用默认 TCP keep-alive?

  • 两小时的默认空闲检测对 HTTP API、数据库连接池和消息系统通常太慢;
  • 中间 NAT、防火墙、负载均衡的空闲超时可能短于 TCP keep-alive;
  • 应用层心跳可以携带业务状态、鉴权和版本信息,通常更适合判断服务是否真正健康;
  • keep-alive 探测本身也会增加流量,不能设置得过于频繁。

实际系统通常组合使用:连接/读取超时、应用层心跳、连接池健康检查、TCP keep-alive 和重连退避。

总结

TCP 的可靠性来自一组相互配合的机制,而不是简单的“有连接所以可靠”:

  • 三次握手同步双方的初始序列号并确认双向可达性;
  • FIN 挥手实现全双工方向的独立关闭,TIME-WAIT 防止旧报文干扰和保证最后 ACK 重传;
  • 序列号、ACK、重传和 SACK 负责可靠、有序交付;
  • 接收窗口负责流量控制,拥塞窗口负责网络拥塞控制;
  • 时间戳可以帮助 RTT 测量和 PAWS,窗口缩放解决大带宽时延积下的窗口限制;
  • Nagle、延迟确认、TFO 和 keep-alive 都是可选或实现相关机制,应结合应用场景和抓包结果理解;
  • TCP 是字节流,不是消息队列,应用必须自行设计消息边界;
  • UDP 不保证可靠性,但 QUIC 等协议可以在 UDP 上构建可靠、有拥塞控制的传输。

参考资料

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS