TCP 协议学习笔记:从报文格式到异常处理

本文整理自一次系统的 TCP 协议学习问答,覆盖报文格式、连接管理、可靠传输、流量控制、拥塞控制、缓冲区设计、粘包问题与异常处理。


一、TCP 报文段格式

TCP 头部固定 20 字节,加上可选选项,最大 60 字节。

各字段说明

字段 作用
源端口 / 目的端口 和 IP 一起构成四元组,标识连接
序号 本报文段数据第一个字节的编号,按字节递增
确认序号 期望收到对方的下一个字节序号,累计确认
数据偏移 头部长度,单位 4 字节,最小 5,最大 15
保留 保留将来使用
标志位 URG、ACK、PSH、RST、SYN、FIN、NS、CWR、ECE
窗口大小 接收方还能接收多少字节,用于流量控制
校验和 覆盖头部、数据和伪头部
紧急指针 URG=1 时有效,现代少用
选项 MSS、窗口缩放、SACK、Timestamps 等
数据 上层应用载荷

关键点

  • 序号是字节编号,不是报文编号。

  • 确认序号是累计确认,表示 N 之前全部收到。

  • SYN 和 FIN 各消耗一个序号,纯 ACK 不消耗。

  • 窗口大小是接收方通告的,不是发送方自己的。

为什么序号和确认序号分开

因为它们是两个方向、两个独立数据流的状态变量:

  • 序号:我发送的数据在我→你方向上的位置。

  • 确认序号:我期望你下一次发送的数据在你→我方向上的位置。

一个管发送方向,一个管接收方向,方向相反,必须分开。


二、三次握手与四次挥手

三次握手

为什么是三次:

逻辑上包含四个动作:A 的 SYN、B 的 ACK、B 的 SYN、A 的 ACK。但 B 的 ACK 和 SYN 没有时间差、方向一致,可以合并在同一个报文里,所以实际只传三个报文。

三次是保证双方都确认对方收发能力正常、且序号同步的最小次数。

四次挥手

复制代码
A                                           B
|  1. FIN, seq = u                          |   A: FIN_WAIT_1
|  ---------------------------------------> |   B: ESTABLISHED -> CLOSE_WAIT
|  2. ACK, ack = u+1                        |
|  <--------------------------------------- |   A: FIN_WAIT_2
|         (B 可能还有数据要发)             |
|  3. FIN, seq = w, ack = u+1               |   B: LAST_ACK
|  <--------------------------------------- |
|  4. ACK, ack = w+1                        |   A: TIME_WAIT
|  ---------------------------------------> |   B: CLOSED
|         (A 等待 2MSL 后关闭)             |   A: CLOSED

为什么是四次:

TCP 全双工,两个方向必须分别关闭。B 收到 A 的 FIN 后,ACK 可以立刻发,但 FIN 要等 B 自己的数据发完。ACK 和 FIN 之间存在时间差,不能合并。

四次挥手的本质:

ACK 是即时的,FIN 是延迟的,两者之间隔着"被动方可能还有数据要发"。如果 B 收到 FIN 时正好也没数据了,ACK 和 FIN 可以合并,变成三次挥手。

TIME_WAIT

主动关闭方发完最后一个 ACK 后进入 TIME_WAIT,等待 2MSL。

两个原因:

  1. 保证最后一个 ACK 能到达 B,如果丢了可以重发。

  2. 让旧连接的报文在网络中消亡,避免干扰新连接。

影响:

  • 连接逻辑上已关闭,但内核保留四元组。

  • 相同四元组的新连接不能被复用。

  • 如果是客户端主动关闭,临时端口在这段时间被占用。

  • SO_REUSEADDR 能让服务端在 TIME_WAIT 期间重新 bind 监听端口,但不能绕过四元组复用限制。


三、可靠传输:超时重传与快速重传

超时重传

发送方发出数据后,如果 RTO 内没收到 ACK,就重传。

RTO 计算:

复制代码
SRTT = (1 - α) * SRTT + α * RTT_sample     (α = 1/8)
RTTVAR = (1 - β) * RTTVAR + β * |SRTT - RTT_sample|  (β = 1/4)
RTO = SRTT + 4 * RTTVAR
RTO >= 1 秒(早期)或 200ms(现代)

指数退避:

每次重传失败,RTO 翻倍:

复制代码
第 1 次超时:RTO
第 2 次超时:2 * RTO
第 3 次超时:4 * RTO
...

Karn 算法:

重传报文的 ACK 不用于计算 RTT,因为无法判断确认的是第一次还是重传。

快速重传

接收方收到失序报文时,重复发送当前期望的 ACK。发送方连续收到 3 个重复 ACK,就认为该段丢了,立刻重传,不等 RTO。

触发条件:

  • 收到 3 个重复 ACK。

失效情况:

  • 丢的是最后一个报文。

  • 发送窗口太小,凑不够 3 个重复 ACK。

  • 重复 ACK 本身丢失。

  • 对端完全无响应。

两者关系

机制 角色 触发条件 速度
快速重传 优先机制 3 个重复 ACK
超时重传 兜底机制 RTO 超时

快速重传负责"能提前发现就快传",超时重传负责"发现不了也得传"。


四、流量控制

目标

让发送方别发太快,别把接收方缓冲区撑爆。

核心机制:滑动窗口

接收方在每个 ACK 里带窗口大小(rwnd),告诉发送方自己还能接收多少。

发送方约束:

复制代码
已发送未确认的字节数 <= rwnd

零窗口与窗口探测

接收方缓冲区满时通告 Window = 0,发送方停止发送,启动持续计时器,定期发窗口探测报文,防止死锁。

糊涂窗口综合征(SWS)

接收方每次只腾出一点空间就通告小窗口,发送方发小报文,效率极低。

解决:

  • 接收方:Clark 算法,窗口小于阈值时通告 0。

  • 发送方:Nagle 算法,攒够 MSS 或收到 ACK 再发。

Nagle 算法

  • 有已发送未确认数据时,不发送新的小报文。

  • 攒到 MSS 或收到 ACK 再发。

  • 可用 TCP_NODELAY 关闭。


五、滑动窗口

是什么

一个动态变化的区间,表示发送方当前可以发送多少字节,接收方当前还能接收多少字节。

窗口大小的决定因素

复制代码
发送窗口 = min(rwnd, cwnd)
窗口 含义 由谁维护
rwnd 接收方还能收多少 接收方通告
cwnd 网络还能承受多少 发送方维护

异常情况下的窗口变化

情况 窗口左边界 窗口右边界 关键机制
正常传输 随 ACK 右移 左边界 + min(rwnd,cwnd) 累计确认
数据丢失 冻结在空洞处 可能伸缩但受限 快速重传/超时重传
ACK 丢失 超时后随重传 ACK 右移 同上 重传
零窗口 不动 收缩到左边界 窗口探测
窗口恢复 收到新 ACK 后右移 扩大 ACK 或探测

为什么要设计滑动窗口

停等协议"一个 RTT 只能发一个报文",吞吐量受 RTT 限制。滑动窗口让发送方连续发送多个报文,同时用窗口边界区分已确认、未确认、可发送区域,支撑可靠传输、流量控制和拥塞控制。


六、三个窗口的关系

窗口 维护方 作用
rwnd 接收方 流量控制,防止压垮接收方
cwnd 发送方 拥塞控制,防止压垮网络
swnd 发送方 实际发送窗口 = min(rwnd, cwnd)
复制代码
swnd = min(rwnd, cwnd)

谁小听谁的。


七、拥塞控制

目标

别压垮网络,避免拥塞崩溃。

核心变量:cwnd

发送方自己维护,表示当前网络状况下还能发多少未确认数据。

四大核心算法

算法 作用
慢启动 cwnd 从 1 开始,指数增长
拥塞避免 cwnd 线性增长,缓慢逼近
快速重传 3 个重复 ACK,立刻重传
快速恢复 cwnd 减半,而不是降到 1

状态转换

复制代码
                 超时
                  ↓
              cwnd = 1
              ssthresh = cwnd/2
                  ↓
              ┌─────────┐
              │ 慢启动  │  cwnd 指数增长
              └────┬────┘
                   │ cwnd >= ssthresh
                   ↓
              ┌─────────────┐
              │ 拥塞避免    │  cwnd 线性增长
              └──────┬──────┘
                     │
          ┌──────────┴──────────┐
          │                     │
      3 个重复 ACK           超时
          │                     │
          ↓                     ↓
    ┌───────────┐         cwnd = 1
    │ 快速恢复  │         ssthresh = cwnd/2
    │ cwnd 减半 │         重新慢启动
    └───────────┘

演进

版本 特点
Tahoe 丢包一律 cwnd=1
Reno 加入快速恢复
NewReno 处理一次丢多个包
SACK 选择性确认,精准重传
CUBIC Linux 默认,三次函数增长
BBR 基于带宽和 RTT 建模,不靠丢包

八、缓冲区设计

为什么用环形缓冲区

TCP 是连续字节流,数据尾部追加、头部消费,是典型 FIFO。

环形缓冲区的优势:

  • 零搬移:只移动 head/tail 指针,O(1)。

  • 固定内存:预先分配,不扩容、不释放。

  • 空间即时复用:指针回绕,读过的空间立刻可写。

  • 缓存友好:连续内存,CPU 缓存命中率高。

  • 实现简单:只需 head、tail、size。

发送缓冲区

复制代码
┌─────────┬─────────────┬──────────┬────────┐
│已确认   │已发送未确认 │ 待发送   │ 空闲   │
└─────────┴─────────────┴──────────┴────────┘
          ↑                          ↑
        head                       tail

已发送未确认的数据必须保留,直到 ACK 到达。

接收缓冲区

复制代码
┌───────────┬──────────┬────────┐
│已收到未读 │ 可接收   │ 空闲   │
└───────────┴──────────┴────────┘
  ↑head                  ↑写入位置

rwnd 直接由剩余空间决定。

环形缓冲区与滑动窗口

  • 滑动窗口是逻辑视图,环形缓冲区是物理存储。

  • 窗口在环形缓冲区上滑动,就是滑动窗口的物理实现。


九、延迟应答

是什么

接收方收到数据后不立刻回 ACK,等一小段时间(通常 40ms~200ms),争取把 ACK 捎带在反向数据里。

为什么

  • 减少纯 ACK 报文数量。

  • 配合捎带确认,提高网络利用率。

  • 减少发送方处理负担。

代价

  • 增加 RTT 测量误差,导致 RTO 偏大。

  • 和 Nagle 算法叠加可能引入延迟。

  • 窗口小时影响吞吐。

不该延迟的情况

  • 收到失序报文。

  • 收到重复报文。

  • 接收缓冲区快满。

  • 收到 FIN。

  • 检测到丢包。


十、粘包问题

本质

TCP 是面向字节流的协议,不保留应用层的消息边界。所谓"粘包",是应用层把字节流当报文流用,没有自己划分边界。

现象

发送方连续 send("hello")send("world"),接收方可能读到:

  • "hello" / "world"

  • "helloworld"

  • "hel" / "loworld"

  • 等等

原因

  • TCP 是字节流,不保存写入边界。

  • Nagle 算法可能合并小报文。

  • 发送/接收缓冲区可能合并多次写入。

  • MSS 可能拆分消息。

解决方案

方案 说明
固定长度 每个消息固定 N 字节
长度字段 消息头带长度,最常用
特殊分隔符 \n,需转义
自描述格式 JSON、Protobuf 等

核心:应用层自己定义消息边界,不要假设一次 recv 等于一个消息。

和 UDP 对比

维度 TCP UDP
数据形式 字节流 数据报
边界 不保留 保留
粘包

十一、TCP 异常情况

进程终止

  • 进程正常终止、崩溃、被 kill -9,内核都会关闭 fd,发送 FIN。

  • 和正常关闭没有区别。

  • 例外:如果进程是被动关闭方且没来得及 close,可能停在 CLOSE_WAIT。

机器重启

  • 正常重启:先停止进程,关闭 fd,发送 FIN,和进程终止一样。

  • 强制重启/掉电:来不及发 FIN,连接静默失效。

机器掉电 / 网线断开

  • 本机瞬间消失,对端完全不知道,仍认为连接是 ESTABLISHED。

  • 对端有写入操作时:数据无 ACK,超时重传多次失败,最终发 RST。

  • 对端没有写入操作时:靠 TCP Keepalive 检测。

TCP Keepalive

参数 默认值 含义
tcp_keepalive_time 7200 秒 空闲多久开始探测
tcp_keepalive_intvl 75 秒 探测间隔
tcp_keepalive_probes 9 探测次数

总共约 2 小时 11 分钟才判定死亡。默认关闭,需要 SO_KEEPALIVE 开启。

局限: 时间太长,实际应用通常自己做心跳。

应用层检测

  • HTTP 长连接:Keep-Alive、定期请求。

  • WebSocket:Ping/Pong。

  • 数据库连接池:SELECT 1

  • IM(如 QQ):心跳 + 断线重连。

  • RPC 框架:心跳 + 超时 + 重连。

其他异常

  • 对端发送 RST:端口未监听、连接已关闭、半连接队列满等。

  • 半打开连接:一方已关闭,另一方不知道。

  • 中间设备超时:NAT、防火墙清除表项。

  • 网络分区:两端互相不可达。

异常情况汇总

异常 是否发 FIN 对端如何感知 检测机制
进程正常终止 收到 FIN 正常关闭
进程崩溃 是(内核代发) 收到 FIN 正常关闭
进程 kill -9 是(内核代发) 收到 FIN 正常关闭
进程死但 fd 未关 CLOSE_WAIT 超时
机器正常重启 收到 FIN 正常关闭
机器强制重启 Keepalive/写入触发 RST
机器掉电 Keepalive/写入触发 RST
网线断开 Keepalive/写入触发 RST
对端 RST 收到 RST 立即终止
中间设备超时 心跳/重连
应用层空闲 应用心跳

十二、核心总结

为什么TCP这么复杂? 因为要保证可靠性,同时由尽可能的提高性能.

可靠性:

• 校验和

• 序列号(按序到达)

• 确认应答

• 超时重发

• 连接管理

• 流量控制

• 拥塞控制

提高性能:

• 滑动窗口

• 快速重传

• 延迟应答

• 捎带应答

其他:

• 定时器(超时重传定时器, 保活定时器, TIME_WAIT定时器等)

  1. TCP 是面向字节流的可靠传输协议,不保留消息边界。

  2. 三次握手 同步双方序号,四次挥手分别关闭两个方向。

  3. 超时重传 是兜底,快速重传是优先,两者配合保证可靠。

  4. 滑动窗口是中枢,同时支撑可靠传输、流量控制和拥塞控制。

  5. 三个窗口:rwnd 管接收方,cwnd 管网络,swnd = min(rwnd, cwnd)。

  6. 环形缓冲区是滑动窗口的物理实现,零搬移、O(1)、固定内存。

  7. 延迟应答减少 ACK 数量,但可能增加 RTT 误差。

  8. 粘包不是 TCP 的 bug,是应用层没有定义消息边界。

  9. 异常情况分进程层、主机层、网络层、应用层,Keepalive 太慢,实际靠应用层心跳。


十三、一句话收尾

TCP 用序号和确认序号保证可靠,用滑动窗口支撑流量和拥塞控制,用三次握手和四次挥手管理连接,用超时和快速重传兜底丢包,用环形缓冲区高效存储,用应用层协议处理边界和异常------它不完美,但足够可靠、足够灵活,是互联网的基石。

相关推荐
傲世仙尊1 小时前
ELF与动态库进阶-链接真相与GOT表
linux
心易行者1 小时前
Python在线运行+SQLite数据库实战:0成本搭个人数据后台,5个场景直接套用
前端·网络·人工智能·python
M78佐菲2 小时前
ARM学习笔记(五)
linux·arm开发·笔记·嵌入式硬件·学习
终端安全笔记2 小时前
iOS 27 之后「策略空转」:设备升级不报错,但旧策略不再管它
android·网络·安全·ios·智能手机
星恒讯工业路由器2 小时前
配电DTU新国标落地:硬件加密成硬门槛,选型逻辑正在改变
网络·物联网·智能路由器·工业路由器·新国标·硬件加密·配电dtu
鹿鸣天涯2 小时前
护网专项工作 | 勒索病毒防护指南
网络·计算机网络·安全
H_oRIZoN_2 小时前
Linux入门DAY45 IMX6ULL 裸机开发笔记:汇编点灯、C 语言点灯、GIC 中断梳理
linux·单片机·嵌入式硬件
M哥支付3 小时前
商户池是什么?
服务器·网络·其他·微信·金融
捷烽3 小时前
光纤熔接机是什么?工作原理与马达结构(2026)
运维·网络·信息与通信