TCP 协议:报文头、连接管理、可靠传输与窗口
课程:尚硅谷《嵌入式 Linux 应用层开发》第 6 章 Socket 编程
依据:2026-09-29 20:51 录音转写;课程 PDF 第 254—264 页。
本节边界:讲 TCP 原理和窗口机制;第 264 页末尾开始出现 Socket 常用函数,具体 API 留到下一节。
1. 本节知识路线
本节要回答四个核心问题:
- TCP 怎样区分连接和应用?
- TCP 怎样知道哪些字节已经收到、哪些需要重传?
- TCP 怎样防止压垮接收方?
- TCP 怎样避免向拥堵的网络继续高速发送?
2. TCP 的基本特征
TCP(Transmission Control Protocol)位于传输层,为应用提供面向连接、可靠、有序、双向的字节流服务。
| 特征 | 正确含义 |
|---|---|
| 面向连接 | 传输应用数据前先同步双方状态和初始序列号 |
| 可靠 | 检测丢失和错误,并通过确认、重传等机制恢复 |
| 有序 | 按字节序列号重排,按顺序交给应用 |
| 字节流 | 应用看到连续字节,不保留每次send()的消息边界 |
| 全双工 | 连接两端都有独立的发送序列空间,可以同时收发 |
| 端到端 | 两端 TCP 实体维护连接状态,中间网络主要负责转发 IP 数据包 |
2.1 “可靠”不等于永远成功
网络中的 TCP 报文段仍然可能丢失、重复、乱序或损坏。TCP 的可靠性来自发现问题并尝试恢复。
如果网线断开、对端进程退出、系统崩溃或重试超过限制,连接仍然可能失败,应用会收到错误或超时。
2.2 一条 TCP 连接怎样区分
TCP 连接通常由四元组标识:
源 IP + 源端口 + 目的 IP + 目的端口TCP 协议类型已经确定,因此通常不再写进这个四元组。服务端同一个监听端口可以接受多个客户端,因为每个连接的客户端 IP 或客户端端口不同。
3. TCP 首部总体结构
没有选项时,TCP 首部最小为 20 字节。存在选项时首部可以变长,Data Offset 字段的最大表示范围使 TCP 首部最大为 60 字节。
| 字段 | 宽度 | 主要作用 |
|---|---|---|
| 源端口、目的端口 | 各 16 位 | 标识两端应用 Socket |
序列号seq | 32 位 | 当前报文段中首个数据字节的编号 |
确认号ack | 32 位 | 下一次期望收到的字节编号 |
| Data Offset | 4 位 | TCP 首部长度,以 32 位字为单位 |
| 保留位与控制位 | 若干位 | 标志连接和传输状态 |
| Window | 16 位 | 通告接收窗口rwnd |
| Checksum | 16 位 | 检测 TCP 首部和数据中的传输错误 |
| Urgent Pointer | 16 位 | URG 有效时描述紧急数据位置 |
| Options | 可变 | MSS、窗口缩放、SACK、时间戳等 |
| Padding | 可变 | 把首部补齐到 32 位边界 |
| Data | 可变 | 应用字节流的一部分 |
3.1 Data Offset 怎样换算
Data Offset 记录的是首部包含多少个 32 位字:
Data Offset = 5 → 5 × 4 字节 = 20 字节 Data Offset = 8 → 8 × 4 字节 = 32 字节 Data Offset = 15 → 15 × 4 字节 = 60 字节填充只用于让 TCP 首部按 32 位边界结束,数据部分不要求补齐到 4 字节整数倍。
4. 需要重点认识的控制位
| 标志 | 作用 |
|---|---|
| SYN | 同步序列号,用于建立连接 |
| ACK | 表示确认号字段有效 |
| FIN | 发送方没有更多数据要发送,用于有序关闭一个方向 |
| RST | 立即复位或拒绝连接 |
| PSH | 提示接收端尽快把待交付数据推给应用 |
| URG | 表示紧急指针字段有效 |
| ECE、CWR | 用于显式拥塞通知 ECN |
教材沿用只强调六个传统标志位的入门图。现代 TCP 基本规范还包括 ECE 和 CWR,保留位数量也与旧图不同。
RST 表示连接被复位或拒绝,并不等于 TCP 自动重新建立连接。是否重连由应用程序决定。
5. 序列号与确认号
5.1 序列号按字节编号
TCP 不给“第几个报文段”简单编号,而是给字节流中的字节编号。
假设当前第一个待发送字节的编号为 1000,每个报文段携带 100 字节:
| 报文段 | 覆盖字节编号 | 首部中的seq |
|---|---|---|
| 1 | 1000—1099 | 1000 |
| 2 | 1100—1199 | 1100 |
| 3 | 1200—1299 | 1200 |
5.2 确认号表示“下一个想要的字节”
若接收方已经连续收到 1000—1299,就返回:
ack = 1300这表示 1300 之前的连续字节已经收到,下一步期待 1300。
基本关系:
ack = 已连续收到的最后一个字节序号 + 15.3 SYN 和 FIN 也占用序列号空间
SYN 和 FIN 即使不携带普通应用数据,也各消耗一个序列号。因此握手和挥手时经常出现:
ack = 对方 seq + 16. TCP 状态先建立最小框架
教材列出 11 种状态:
| 状态 | 含义 |
|---|---|
| CLOSED | 没有连接 |
| LISTEN | 服务端等待连接请求 |
| SYN_SENT | 主动方已经发送 SYN |
| SYN_RECEIVED | 收到 SYN 并回复 SYN+ACK |
| ESTABLISHED | 连接已经建立,可传输数据 |
| FIN_WAIT_1 | 主动关闭方已发送 FIN |
| FIN_WAIT_2 | 主动关闭方的 FIN 已被确认,等待对方 FIN |
| CLOSE_WAIT | 收到对方 FIN,等待本地应用关闭 |
| CLOSING | 双方几乎同时主动关闭 |
| LAST_ACK | 被动关闭方已发送 FIN,等待最后 ACK |
| TIME_WAIT | 主动关闭方等待旧报文消失并处理 FIN 重传 |
当前阶段先能认出LISTEN、ESTABLISHED、CLOSE_WAIT和TIME_WAIT。以后可用ss -tan对照实际连接状态。
7. 三次握手
假设客户端初始序列号为x,服务端初始序列号为y。
| 次数 | 发送方向 | 关键字段 | 作用 |
|---|---|---|---|
| 第一次 | 客户端 → 服务端 | SYN, seq=x | 请求连接并给出客户端初始序列号 |
| 第二次 | 服务端 → 客户端 | SYN+ACK, seq=y, ack=x+1 | 确认客户端并给出服务端初始序列号 |
| 第三次 | 客户端 → 服务端 | ACK, seq=x+1, ack=y+1 | 确认服务端的 SYN |
三次握手的核心作用是:
- 确认两个方向都能通信;
- 同步双方各自的初始序列号;
- 建立连接状态,减少旧报文造成错误连接的风险;
- 协商 MSS、窗口缩放、SACK 等 TCP 选项。
初始序列号不是教材图里固定的 0,实际实现会选择新的初始值。
8. 四次挥手
TCP 是全双工连接,两个发送方向要分别关闭。假设客户端先主动关闭:
第二次挥手后,主动关闭方已经不能再发送应用数据,但仍可以接收被动关闭方剩余的数据。这体现了 TCP 的半关闭能力。
8.1 为什么通常说四次
收到 FIN 后,内核可以马上确认;本地应用何时完成剩余发送并关闭,是另一个时间点,所以 ACK 和本端 FIN 通常分开发送。
实际通信中,如果时机合适,ACK 与 FIN 也可能合并到同一个报文段,抓包未必永远正好看到四个独立报文段。
8.2 TIME_WAIT 为什么存在
主动关闭方发送最后一个 ACK 后等待一段时间,主要用于:
- 对方没收到最后 ACK、重传 FIN 时,仍能再次回复 ACK;
- 让该连接的旧重复报文在网络中自然消失,避免干扰以后复用相同四元组的连接。
传统分析把等待时间写作2 × MSL。具体持续时间由操作系统实现决定,不要把它理解成所有系统都相同的固定秒数。
9. 可靠传输的几个基础机制
9.1 累计确认
ACK 确认的是某个序号之前连续收到的全部字节。
收到 0—99、100—199、200—299 → 可以只发送 ACK=300 → 表示 0—299 都已连续收到若 100—199 丢失,却先收到 200—299,累计确认通常仍指向 100,表示缺口尚未补上。
9.2 延迟确认
接收方可以短暂等待,将多个确认合并,或把 ACK 搭载到反向数据上,以减少纯 ACK 报文数量。
延迟确认不是无限等待。具体策略和定时由 TCP 实现决定。
9.3 超时重传
发送方为未确认数据维护重传计时。当重传超时 RTO 到期仍未得到确认时,重传最早的未确认数据。
RTO 不是随意固定的常量,它会根据往返时间 RTT 的测量动态计算。
9.4 快速重传
接收方发现序列号缺口时,会继续确认期望的下一个字节。发送方连续收到 3 个重复 ACK 后,可以推断某段可能丢失,不等待 RTO 就先重传。
发送:100、200、300、400 其中 200 丢失 接收方收到 300、400 后仍重复回复 ACK=200 发送方收到足够的重复 ACK → 快速重传 seq=200现代 TCP 还可能使用 SACK 精确说明已经收到的非连续区间。
10. 接收窗口rwnd:保护接收方
接收方读取应用数据的速度可能低于网络到达速度。TCP 把尚未被应用读取的数据暂存在接收缓冲区,并通过首部 Window 字段通告可用空间。
接收缓冲区总容量 - 当前已占用空间 = 可通告接收窗口 rwnd发送方必须限制未确认数据,避免超出接收方通告的能力。这是流量控制。
10.1 零窗口
当rwnd = 0时,发送方停止继续发送普通新数据。之后会通过零窗口探测等机制确认窗口是否重新打开,避免窗口更新丢失后双方永久等待。
Window 字段本身只有 16 位。高带宽高时延网络可以在连接建立时协商窗口缩放选项,使有效接收窗口超过 65535 字节。
11. 拥塞窗口cwnd:保护网络
接收方有足够缓冲区,不代表中间网络能承受同样的数据量。发送方还维护拥塞窗口cwnd,根据 ACK、丢包、ECN 等反馈估计路径承载能力。
| 窗口 | 谁维护或通告 | 保护对象 |
|---|---|---|
rwnd | 接收方通告 | 接收端缓冲区和处理能力 |
cwnd | 发送方计算 | 中间网络容量 |
11.1 慢启动
当路径能力未知时,发送方从较小拥塞窗口开始探测。每收到确认新数据的 ACK,窗口增加;从每个 RTT 的整体效果看,窗口大约呈指数增长。
“慢启动”指相对一次性高速注入数据更谨慎。它的增长阶段实际上可能很快。
11.2 拥塞避免
当cwnd达到慢启动阈值ssthresh后,增长变得保守。经典模型中,cwnd大约每个 RTT 增加一个 MSS,表现为近似线性增长。
11.3 重传超时后的处理
RTO 超时通常被视为较强的拥塞信号。经典模型会:
- 降低
ssthresh; - 把
cwnd降到很小的损失窗口; - 重传丢失数据;
- 重新进行慢启动,再转入拥塞避免。
11.4 快速恢复
三个重复 ACK 说明后续报文仍在到达,网络并非完全停滞。快速重传后可在降低窗口的同时保留 ACK 时钟,避免像超时那样完全从头探测。
不同拥塞控制算法处理细节不同。Linux 常见算法不只教材展示的经典 Reno 模型,因此课堂曲线用于理解“探测、发现拥塞、减速、再增长”的思想。
12. 教材窗口曲线怎样理解
教材用简化数值演示:
慢启动:1 → 2 → 4 → 8 拥塞避免:8 → 9 → 10 → 11 → 12 发生超时:降低阈值,cwnd 回到很小值 重新增长这是一张概念图,不应把它当成现代实现逐个 ACK 的固定计算公式。
更准确的经典描述是:
- 慢启动期间,每个确认新数据的 ACK 最多增加约一个 SMSS,因此每个 RTT 大约翻倍;
- 拥塞避免期间,每个 RTT 总增量大约为一个 SMSS;
- 初始窗口不一定是 1 MSS,现代规范允许更大的初始窗口;
- Linux 具体采用的拥塞控制算法可能是 CUBIC 等,而不是完全照搬 Reno 曲线。
13. 实际发送限制
发送方必须同时服从接收端和网络路径两个限制:
允许的在途数据上限 ≈ min(rwnd, cwnd)真正还能发送多少,还要减去已经发送但尚未确认的数据:
当前可继续发送量 ≈ min(rwnd, cwnd) - bytes_in_flight因此:
rwnd小:接收方来不及处理;cwnd小:网络被判断为拥堵或尚未探测出足够容量;- 两者都大:发送方仍受应用是否有数据、发送缓冲区、MSS 和协议实现等条件限制。
14. 流量控制与拥塞控制不要混淆
| 对比项 | 流量控制 | 拥塞控制 |
|---|---|---|
| 关注对象 | 接收方是否处理得过来 | 中间网络是否承载得住 |
| 主要变量 | rwnd | cwnd、ssthresh |
| 信息来源 | 接收方通告窗口 | ACK、丢包、超时、ECN 等网络反馈 |
| 调节主体 | 接收方通告,发送方遵守 | 发送方算法计算 |
教材第 262 页把cwnd也描述为“流量控制的基础”容易混淆。通常把rwnd对应的机制称为流量控制,把cwnd对应的机制称为拥塞控制。
15. 可以立即做的观察练习
15.1 查看 TCP 状态
ss-tan重点找:
LISTENESTABTIME-WAITCLOSE-WAIT
15.2 抓取握手和挥手
有管理员权限时,可以在访问某个测试服务期间抓包:
sudotcpdump-iany-nn'tcp port 8080'观察:
SYN;SYN, ACK;ACK;- 数据阶段的
seq、ack和win; - 关闭阶段的
FIN与ACK。
不要要求抓包中的序列号必须从 0 开始。抓包工具可能为了便于阅读显示相对序列号。
16. 录音与教材表述校正
- 转写中的
PCP、PCB、DCB、TCB等多数应为TCP。 - TCP 可靠不表示数据在网络中从不丢失,而是协议能检测并恢复部分丢失;连接仍可能最终失败。
- TCP 是字节流,没有固定应用消息长度,也不保留
send()边界。应用层必须自行设计消息边界。 - TCP 序列号对应字节位置,不是简单的报文段编号。
- 确认号表示下一期望字节,不是把对方序列号原样返回。
- SYN 和 FIN 各占用一个序列号,所以握手和挥手中确认号通常加一。
- 现代 TCP 首部包含 ECE 和 CWR 等控制位;教材六标志位图是简化或旧式表示。
- Checksum 用于错误检测,不提供加密或防篡改安全保证。
- RST 表示复位或拒绝连接,不会自动重新建立连接。
- 四次挥手是典型逻辑过程,实际 ACK 与 FIN 可能合并。
- TIME_WAIT 通常出现在主动关闭方,但同时关闭等路径会有所不同。
rwnd是接收方通告的流量控制窗口;cwnd是发送方维护的拥塞控制窗口。- 慢启动不是“每收到一个 ACK 就把整个 cwnd 翻倍”,而是每个 RTT 的整体效果近似翻倍。
- 拥塞避免不是“每个 ACK 都让窗口增加一个完整报文段”,经典目标是每个 RTT 约增加一个 MSS。
- 现代初始拥塞窗口不固定为 1 MSS;教材明确采用 1 只是为了画图讨论。
- 真实 TCP 实现存在 Reno、NewReno、CUBIC 等不同算法,教材曲线只表示经典基础思想。
17. 本节最低掌握标准
学完后应能回答:
- TCP 的面向连接、可靠、有序字节流分别表示什么?
- TCP 最小首部为什么是 20 字节?
seq与ack分别怎样计算?- SYN 和 FIN 为什么会让确认号加一?
- 三次握手每一步交换什么信息?
- 四次挥手为什么通常不能简单合成两次?
- TIME_WAIT 有什么作用?
- 累计确认、超时重传和快速重传有什么关系?
rwnd与cwnd分别保护谁?- 为什么发送限制取
min(rwnd, cwnd)? - 慢启动与拥塞避免的增长速度有什么区别?
- 为什么 TCP 应用仍然需要自行处理消息边界?
最低实践要求:
- 能画出三次握手和四次挥手;
- 能根据
seq和数据长度算出下一个 ACK; - 能解释零窗口时发送方为什么暂停;
- 能从
ss -tan中认出常见 TCP 状态; - 能看懂抓包中的
SYN、ACK、FIN、seq、ack和win。
18. PDF 页码与录音时间索引
| 主题 | PDF 页码 | 录音时间 |
|---|---|---|
| TCP 定义与特征 | 254—255 | 00:02—02:19 |
| TCP 首部与字段 | 255—257 | 02:20—08:17 |
| TCP 状态 | 257—258 | 08:18—09:33 |
| 三次握手 | 258—259 | 09:34—13:17 |
| 四次挥手与 TIME_WAIT | 259—260 | 13:18—17:07 |
| 累计确认、延迟确认、超时重传 | 260—261 | 17:08—20:49 |
| 接收窗口与流量控制 | 261—262 | 20:50—22:35 |
| 拥塞控制基础 | 262—263 | 22:36—27:59 |
发送窗口min(rwnd, cwnd) | 264 | 28:00—28:25 |
| Socket 开发函数 | 264 页末起 | 下一节内容 |
进一步核对资料:
- IETF Internet Standard:RFC 9293,Transmission Control Protocol
- IETF TCP 拥塞控制基础:RFC 5681
- IETF TCP 重传定时器:RFC 6298
- IETF 窗口缩放与时间戳扩展:RFC 7323
- IETF 较大初始窗口规范:RFC 6928
19. 下一步学习
下一节将把 TCP 原理映射到 Linux Socket API:
服务端:socket → bind → listen → accept → recv/send → close 客户端:socket → connect → send/recv → close继续前至少要能解释:
- 为什么服务端先进入
LISTEN; connect()对应三次握手的触发;- 为什么 TCP 的一次
recv()不保证得到一条完整业务消息; close()为什么不等于网络上立即完全消失。