本文从零开始,用最通俗的语言解释TCP协议的每一个核心概念。
原文:RFC 9293,2022年8月,废弃了1981年的RFC 793。

第一章:TCP 是什么?为什么需要它?

1.1 一句话理解 TCP

想象你在寄快递:

  • IP协议 相当于快递公司:负责把包裹从A运到B,但不保证一定到,也不保证顺序。
  • TCP协议 相当于"可靠快递协议":在IP上面加了一层保证,确保包裹:
    1. 一定到达(丢了重发)
    2. 按顺序到达(乱序重排)
    3. 没有损坏(校验和验证)
    4. 不超过对方能接收的量(流量控制)
    5. 不把网络塞满(拥塞控制)

1.2 TCP 的核心特性

RFC 9293 Section 2.2 总结如下:

特性 说明
可靠传输 通过序列号检测丢包,通过重传修复
有序传输 字节流保证顺序,不会乱
字节流服务 对应用透明,不关心消息边界
面向连接 通信前必须先建立连接(三次握手)
全双工 两个方向独立传输数据
端口复用 用端口号区分同一主机上的不同应用
单播 一对一通信(不支持广播/多播作为目标)

第二章:TCP 头部格式——每个字段的含义

2.1 头部图示

0         1         2         3         (字节序号)
0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|        源端口号 (16位)         |       目标端口号 (16位)        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                      序列号 Sequence Number (32位)              |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    确认号 Acknowledgment Number (32位)          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 数据 |       |C|E|U|A|P|R|S|F|                               |
| 偏移 | 保留  |W|C|R|C|S|S|Y|I|         窗口大小 (16位)         |
|      |       |R|E|G|K|H|T|N|N|                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|          校验和 (16位)        |        紧急指针 (16位)          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                        选项 Options (可选)                      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                          应用数据                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

2.2 各字段逐一讲解

源端口 / 目标端口(各16位)

端口就是门牌号。IP地址定位到一台机器,端口定位到机器上的具体程序。
比如:Web服务器通常在80端口,HTTPS在443端口。
序列号 Sequence Number(32位)
每个字节都有编号,就像书的页码。序列号是"这段数据第一个字节的页码"。
特殊情况:SYN包里的序列号是初始序列号(ISN),第一个真正的数据字节是 ISN+1ISN + 1ISN+1
确认号 Acknowledgment Number(32位)
“我已经收到了这个编号之前的所有数据,现在期待收到编号为X的字节。”
只有ACK标志位为1时这个字段才有效。
例:确认号=301 意思是"300及之前的字节我都收到了,请发第301字节"。
数据偏移 Data Offset(4位)
告诉接收方"数据从哪里开始"。单位是32位字(4字节)。
最小值=5(即20字节,无选项时的标准头部大小)。
最大值=15(即60字节,选项最多40字节)。
控制位 / 标志位(8个)


标志 全称 作用
CWR Congestion Window Reduced 拥塞窗口已缩小(配合ECN使用)
ECE ECN-Echo 通知对方网络出现拥塞
URG Urgent 紧急指针字段有效
ACK Acknowledgment 确认号字段有效
PSH Push 请立即将数据推送给应用层,不要缓冲
RST Reset 强制断开连接(出错时使用)
SYN Synchronize 同步序列号,用于建立连接
FIN Finish 发送方没有更多数据了,请求关闭连接

窗口大小 Window(16位)

接收方告诉发送方:“我现在还能接收这么多字节的数据”。
这是流量控制的核心机制,防止发送方把接收方淹没。

重要:必须把它当作无符号整数处理(MUST-1),否则大窗口会被误认为负数。
建议在连接记录中用32位字段保存实际窗口值(使用窗口扩展选项时需要)。
校验和 Checksum(16位)
TCP的自我检测机制,检查数据在传输过程中有没有损坏。

计算方法:对整个TCP头部+数据+伪头部(包含IP地址信息)做反码求和

伪头部(IPv4版):

+--------+--------+--------+--------+
|         源IP地址  (32位)           |
+--------+--------+--------+--------+
|         目标IP地址 (32位)          |
+--------+--------+--------+--------+
|  零   |协议号=6|    TCP总长度      |
+--------+--------+--------+--------+

为什么要加IP地址?防止被路由到错误目标的数据包被误接受。

强制要求:发送方必须生成校验和(MUST-2),接收方必须验证(MUST-3)。
紧急指针 Urgent Pointer(16位)
只在URG标志位为1时有效。
值含义:相对于当前序列号的偏移量,指向紧急数据最后一个字节的下一个位置
现代应用程序不应该使用这个机制(SHLD-13),因为各实现行为不一致。

2.3 完整的 C++ 头部结构示例

#include <cstdint>
#include <cstring>
#include <arpa/inet.h>   // htons, ntohs
#include <iostream>
// TCP 头部结构(20字节固定部分,不含选项)
// 注意:网络字节序是大端(big-endian)
struct TcpHeader {
    uint16_t src_port;       // 源端口
    uint16_t dst_port;       // 目标端口
    uint32_t seq_num;        // 序列号
    uint32_t ack_num;        // 确认号(ACK标志为1时有效)
    uint8_t  data_offset;    // 高4位=数据偏移(单位:32bit字), 低4位=保留(置0)
    uint8_t  flags;          // 控制标志位(见下面的枚举)
    uint16_t window;         // 窗口大小(接收方的可用缓冲区字节数)
    uint16_t checksum;       // 校验和
    uint16_t urgent_ptr;     // 紧急指针(URG=1时有效)
    // 之后是可变长度的选项字段(0~40字节)
};
// 标志位枚举(对应flags字段的各个位)
enum TcpFlags : uint8_t {
    TCP_FIN = 0x01,  // bit 0: 结束
    TCP_SYN = 0x02,  // bit 1: 同步
    TCP_RST = 0x04,  // bit 2: 重置
    TCP_PSH = 0x08,  // bit 3: 推送
    TCP_ACK = 0x10,  // bit 4: 确认
    TCP_URG = 0x20,  // bit 5: 紧急
    TCP_ECE = 0x40,  // bit 6: ECN回显
    TCP_CWR = 0x80   // bit 7: 拥塞窗口缩小
};
// 辅助函数:解析头部并打印各字段(主机字节序显示)
void print_tcp_header(const TcpHeader& hdr) {
    // 从网络字节序转为主机字节序再显示
    uint16_t sport = ntohs(hdr.src_port);
    uint16_t dport = ntohs(hdr.dst_port);
    uint32_t seq   = ntohl(hdr.seq_num);
    uint32_t ack   = ntohl(hdr.ack_num);
    uint8_t  doff  = (hdr.data_offset >> 4) & 0x0F; // 高4位
    uint8_t  flags = hdr.flags;
    uint16_t win   = ntohs(hdr.window);
    std::cout << "=== TCP Header ===\n";
    std::cout << "  源端口:     " << sport << "\n";
    std::cout << "  目标端口:   " << dport << "\n";
    std::cout << "  序列号:     " << seq   << "\n";
    std::cout << "  确认号:     " << ack   << "\n";
    std::cout << "  数据偏移:   " << (int)doff << " x 4 = "
              << (int)doff * 4 << " 字节\n";
    std::cout << "  标志位:     "
              << ((flags & TCP_FIN) ? "FIN " : "")
              << ((flags & TCP_SYN) ? "SYN " : "")
              << ((flags & TCP_RST) ? "RST " : "")
              << ((flags & TCP_PSH) ? "PSH " : "")
              << ((flags & TCP_ACK) ? "ACK " : "")
              << ((flags & TCP_URG) ? "URG " : "")
              << ((flags & TCP_ECE) ? "ECE " : "")
              << ((flags & TCP_CWR) ? "CWR " : "")
              << "\n";
    std::cout << "  窗口大小:   " << win << " 字节\n";
    std::cout << "  校验和:     0x" << std::hex << ntohs(hdr.checksum)
              << std::dec << "\n";
}
int main() {
    // 模拟一个 SYN 包头部
    TcpHeader syn_pkt;
    std::memset(&syn_pkt, 0, sizeof(syn_pkt));
    syn_pkt.src_port    = htons(54321);  // 客户端随机端口
    syn_pkt.dst_port    = htons(80);     // 目标:HTTP
    syn_pkt.seq_num     = htonl(100);    // 初始序列号=100
    syn_pkt.ack_num     = htonl(0);      // SYN包无效确认号
    syn_pkt.data_offset = (5 << 4);      // 数据偏移=5(无选项,20字节头部)
    syn_pkt.flags       = TCP_SYN;       // 只有SYN标志
    syn_pkt.window      = htons(65535);  // 最大窗口(未使用窗口扩展时)
    syn_pkt.checksum    = 0;             // 实际发送前由内核计算
    syn_pkt.urgent_ptr  = 0;
    print_tcp_header(syn_pkt);
    return 0;
}

第三章:序列号与确认号——TCP 可靠性的基础

3.1 序列号的作用

每一个字节都有一个唯一的序列号,就像给每张纸编页码:

字节数据:   H  e  l  l  o     W  o  r  l  d
序列号:    101 102 103 104 105 106 107 108 109 110 111
  • 确认是累积的:ACK=106 意味着"101~105我全收到了,等106"
  • 重传通过序列号识别:如果105没收到,ACK一直停在105,发送方重传
    序列号空间范围:000232−12^{32} - 12321(约42亿),所有运算取模 2322^{32}232

3.2 关键状态变量(TCB中存储)

发送方的变量:
1⏟已确认∣2⏟已发未确认∣3⏟可发但未发∣4⏟还不能发 \underbrace{1}_{\text{已确认}} \quad | \underbrace{2}_{\text{已发未确认}} | \underbrace{3}_{\text{可发但未发}} | \underbrace{4}_{\text{还不能发}} 已确认 1已发未确认 2可发但未发 3还不能发 4

          SND.UNA        SND.NXT    SND.UNA + SND.WND
             |               |              |
 ----+-------+---------------+--------------+------>
  已确认  | 已发未确认    | 可发未发     | 不可发
 (区域1)  |   (区域2)    |  (区域3)    | (区域4)

变量 全名 含义
SND.UNA send unacknowledged 最老的未被确认的序列号(发送窗口左边界)
SND.NXT send next 下一个要发送的序列号
SND.WND send window 发送窗口大小(由接收方广告)
SND.UP send urgent pointer 发送方的紧急指针
SND.WL1 - 上次更新窗口时用的段序列号(防止用旧段更新窗口)
SND.WL2 - 上次更新窗口时用的确认号
ISS initial send sequence 初始发送序列号

接收方的变量:

          RCV.NXT    RCV.NXT + RCV.WND
             |              |
 ----+-------+--------------+------>
  已确认  | 期望接收区域  | 不接受
 (区域1)  |    (区域2)   | (区域3)

变量 全名 含义
RCV.NXT receive next 期望收到的下一个序列号(接收窗口左边界)
RCV.WND receive window 接收窗口大小(告诉发送方我能收多少)
RCV.UP receive urgent pointer 接收方紧急指针
IRS initial receive sequence 初始接收序列号

3.3 段的可接受性判断

一个到来的数据段,接收方如何判断是否接受?
接受条件(有数据时):RCV.NXT≤SEG.SEQ<RCV.NXT+RCV.WND \text{接受条件(有数据时):} \quad RCV.NXT \leq SEG.SEQ < RCV.NXT + RCV.WND 接受条件(有数据时):RCV.NXTSEG.SEQ<RCV.NXT+RCV.WND
或者(段的末尾落在窗口内也接受):
RCV.NXT≤SEG.SEQ+SEG.LEN−1<RCV.NXT+RCV.WND RCV.NXT \leq SEG.SEQ + SEG.LEN - 1 < RCV.NXT + RCV.WND RCV.NXTSEG.SEQ+SEG.LEN1<RCV.NXT+RCV.WND
四种特殊情况的完整表格:

段长度 接收窗口 接受条件
0 0 SEG.SEQ=RCV.NXTSEG.SEQ = RCV.NXTSEG.SEQ=RCV.NXT
0 >0> 0>0 RCV.NXT≤SEG.SEQ<RCV.NXT+RCV.WNDRCV.NXT \leq SEG.SEQ < RCV.NXT + RCV.WNDRCV.NXTSEG.SEQ<RCV.NXT+RCV.WND
>0> 0>0 0 不接受(窗口为零时不接受数据)
>0> 0>0 >0> 0>0 上面两个条件满足其一即可

3.4 初始序列号(ISN)的选择

为什么不能从0开始?
如果每次都从0开始,上一次连接遗留在网络中的旧数据包可能被新连接误认为有效数据。
RFC 9293 推荐的公式:
ISN=M+F(localip,localport,remoteip,remoteport,secretkey) ISN = M + F(\text{localip}, \text{localport}, \text{remoteip}, \text{remoteport}, \text{secretkey}) ISN=M+F(localip,localport,remoteip,remoteport,secretkey)
其中:

  • MMM 是一个每约4微秒递增一次的时钟计数器(绕回周期约4.55小时,远大于MSL=2分钟)
  • F()F()F() 是一个伪随机函数(PRF),通常用密码学哈希实现
  • secretkey 是只有本机知道的秘密,防止攻击者预测ISN
    F()F()F() 的作用:即使攻击者知道连接的四元组(两端IP+端口),也无法预测ISN,从而无法伪造RST攻击。

3.5 C++ 演示:序列号比较(模 2322^{32}232

#include <cstdint>
#include <iostream>
// TCP 序列号比较:考虑 2^32 回绕
// 由于序列号是模 2^32 的无符号整数,不能直接用 < 比较
// 判断 a < b(模 2^32 意义下)
// 方法:如果 (b - a) 在 (0, 2^31) 范围内,则 a < b
bool seq_less(uint32_t a, uint32_t b) {
    // 利用无符号减法自动取模
    return (int32_t)(b - a) > 0;
}
// 判断 a <= b
bool seq_leq(uint32_t a, uint32_t b) {
    return (int32_t)(b - a) >= 0;
}
// 判断 a 是否在 [left, right] 窗口内(闭区间,模 2^32)
bool in_window(uint32_t a, uint32_t left, uint32_t right) {
    // left <= a <= right(模 2^32)
    return seq_leq(left, a) && seq_leq(a, right);
}
// 判断一个ACK是否"可接受"(有效确认了新数据)
// 条件:SND.UNA < SEG.ACK <= SND.NXT
bool is_acceptable_ack(uint32_t seg_ack, uint32_t snd_una, uint32_t snd_nxt) {
    // SEG.ACK 必须在 (SND.UNA, SND.NXT] 范围内
    return seq_less(snd_una, seg_ack) && seq_leq(seg_ack, snd_nxt);
}
int main() {
    // 测试1:正常情况
    uint32_t snd_una = 100, snd_nxt = 200;
    uint32_t seg_ack = 150;
    std::cout << "ACK=150 是否有效: "
              << (is_acceptable_ack(seg_ack, snd_una, snd_nxt) ? "是" : "否")
              << "\n"; // 是
    // 测试2:回绕情况(序列号接近 2^32 边界)
    // 假设 SND.UNA 在 2^32-50 附近,SND.NXT 回绕到了 50
    uint32_t una_wrap = 0xFFFFFFD0u; // 2^32 - 48
    uint32_t nxt_wrap = 50;          // 已经回绕
    uint32_t ack_wrap = 30;          // 在回绕后的范围内
    std::cout << "回绕情况 ACK=30 是否有效: "
              << (is_acceptable_ack(ack_wrap, una_wrap, nxt_wrap) ? "是" : "否")
              << "\n"; // 是
    // 测试3:旧的 ACK(重复确认)
    uint32_t dup_ack = 50; // 等于 SND.UNA,不是 > SND.UNA
    std::cout << "重复 ACK=50 是否有效: "
              << (is_acceptable_ack(dup_ack, 50, 200) ? "是" : "否")
              << "\n"; // 否(不满足 SND.UNA < ACK 的严格小于)
    return 0;
}

第四章:连接建立——三次握手(3WHS)

4.1 为什么需要三次握手?

核心问题: 双方都需要知道对方的初始序列号,并且得到确认。
一问一答是两次,但两次不够:因为最后一个ACK也可能丢失,服务器不知道客户端是否收到了SYN-ACK。三次握手解决了这个问题。

4.2 正常三次握手流程

TCP 客户端 A                              TCP 服务器 B
CLOSED                                      LISTEN
  |                                            |
  |---  SYN, SEQ=100  ------------------->    |
  |     "我的初始序号是100"              SYN-RECEIVED
  |                                            |
SYN-SENT  <---  SYN+ACK, SEQ=300, ACK=101  --|
  |              "我的初始序号是300,           |
  |               收到你的了,等101"            |
  |                                            |
ESTABLISHED                                   |
  |---  ACK, SEQ=101, ACK=301  ----------->   |
  |     "收到你的了,等301"                 ESTABLISHED
  |                                            |
  |===  DATA 正常传输  ======================  |

步骤分解:

  1. A -> B:SYN(seq=100):A说"我要建立连接,我的起始序号是100"
  2. B -> A:SYN+ACK(seq=300, ack=101):B说"好的,我的起始序号是300,你的100我收到了,等101"
  3. A -> B:ACK(seq=101, ack=301):A说"你的300我收到了,等301,连接建立"
    注意:SYN本身占用一个序列号,所以ACK的确认号是 ISN+1ISN + 1ISN+1

4.3 同时发起连接(Simultaneous Open)

两端同时主动发起连接,TCP必须支持(MUST-10):

TCP 端A                                    TCP 端B
CLOSED                                      CLOSED
  |                                            |
  |--- SYN, SEQ=100 ------->    ...            |
  |                                            |--- SYN, SEQ=300 --->  ...
  |          <--- SYN, SEQ=300 ---------------|
  |    <SEQ=100><SYN> 到达B --> SYN-RECEIVED   |
SYN-RECEIVED                                  |
  |--- SYN+ACK, SEQ=100, ACK=301 ------> ...  |
  |    <--- SYN+ACK, SEQ=300, ACK=101 --------|
ESTABLISHED                               ESTABLISHED

路径:CLOSED → SYN-SENT → SYN-RECEIVED → ESTABLISHED(两端相同)

4.4 旧SYN包引起的问题及RST恢复

旧的重复SYN到达时,通过RST恢复:

A (客户端)                                  B (服务器)
  |                                            |
  |=== (旧的SYN=90 在网络中漂流) =============>|
  |--- 新 SYN, SEQ=100 -----------------> ... |
  |                                            |
  |        <---  SYN+ACK, SEQ=300, ACK=91 ----|  ← 响应了旧SYN
  |                                            |
  | A发现 ACK=91 与 ISS=100 不符               |
  |--- RST, SEQ=91 -----------------------> 回到LISTEN
  |                                            |
  |  (原来的新SYN终于到达)                     |
  |--- SEQ=100 SYN -------------------->       |
  |        <--- SYN+ACK, SEQ=400, ACK=101 ----|
  |--- ACK, SEQ=101, ACK=401 ----------->      |
ESTABLISHED                                ESTABLISHED

4.5 半开连接(Half-Open Connection)

A重启了,但B不知道,以为连接还在:

A (重启后)                                 B (以为连接还开着)
CLOSED                                  ESTABLISHED(send 300, rcv 100)
  |                                            |
  |--- SYN, SEQ=400 ----------------------->   |
  |    B处于synchronized状态,                 |
  |    SEQ=400不在期望窗口内                   |
  |          <--- ACK, SEQ=300, ACK=100 -------|
  |                                            |
  | A没有对应连接,发RST                       |
  |--- RST, SEQ=100 ----------------------->   |
  |                                       B中止!|
  |                                        CLOSED
  |--- 继续尝试建立连接 ------------------>    |

第五章:TCP 状态机

5.1 所有状态列表


状态 含义
CLOSED 无连接(虚构状态,表示没有TCB)
LISTEN 被动等待远端的连接请求
SYN-SENT 已发SYN,等对方的SYN-ACK
SYN-RECEIVED 已收并发了SYN,等最终的ACK
ESTABLISHED 连接建立,正常数据传输阶段
FIN-WAIT-1 已发FIN,等对方ACK
FIN-WAIT-2 己方FIN已被确认,等对方的FIN
CLOSE-WAIT 收到了对方的FIN,等本地应用调用Close
CLOSING 双方同时关闭,等对方的ACK
LAST-ACK 已发己方FIN,等最后一个ACK
TIME-WAIT 等待2倍MSL(最大段生存时间),确保旧包消散

5.2 状态转移图

被动打开(passive OPEN)
创建TCB

主动打开(active OPEN)
发送SYN

收到SYN
发送SYN+ACK

应用层SEND
发送SYN

CLOSE
删除TCB

收到SYN(同时打开)
发送SYN+ACK

收到SYN+ACK
发送ACK

超时或RST

收到ACK

收到RST(来自被动打开)

应用层CLOSE
发送FIN

应用层CLOSE
发送FIN

收到FIN
发送ACK

收到ACK

收到FIN(同时关闭)
发送ACK

收到FIN+ACK
发送ACK

收到FIN
发送ACK

应用层CLOSE
发送FIN

收到ACK

收到ACK
删除TCB

2MSL超时
删除TCB

CLOSED

LISTEN

SYN_SENT

SYN_RECEIVED

ESTABLISHED

FIN_WAIT_1

CLOSE_WAIT

FIN_WAIT_2

CLOSING

TIME_WAIT

LAST_ACK

5.3 状态转换的触发事件

触发TCP状态转换的三类事件:

事件类型
    |
    +-- 用户调用 (User Calls)
    |       OPEN / SEND / RECEIVE / CLOSE / ABORT / STATUS
    |
    +-- 到达的段 (Arriving Segments)
    |       含有 SYN / ACK / RST / FIN 标志的包
    |
    +-- 超时 (Timeouts)
            USER TIMEOUT(用户超时)
            RETRANSMISSION TIMEOUT(重传超时)
            TIME-WAIT TIMEOUT(2MSL超时)

第六章:连接关闭——四次挥手

6.1 为什么需要四次?

TCP是全双工的(两个方向独立)。关闭也是"半关闭":

  • 一方发FIN,表示"我不再发数据了"
  • 但另一方可以继续发数据,直到它也发FIN
    所以完整关闭需要两个方向各一个FIN+ACK,共4次交互。

6.2 正常关闭序列

A(主动关闭方)                              B(被动关闭方)
ESTABLISHED                                ESTABLISHED
  |                                            |
  |--- FIN+ACK, SEQ=100, ACK=300 -------->    |
  |                                       CLOSE-WAIT
FIN-WAIT-1                                    |
  |                                            |
  |          <--- ACK, SEQ=300, ACK=101 -------|
FIN-WAIT-2                                    |
  |                                            |
  |    (B的应用程序调用CLOSE)                  |
  |          <--- FIN+ACK, SEQ=300, ACK=101 --|
  |                                        LAST-ACK
TIME-WAIT                                     |
  |--- ACK, SEQ=101, ACK=301 ------------>    |
  |                                       CLOSED
  |  (等待 2×MSL = 4分钟)                     |
CLOSED

6.3 同时关闭(Simultaneous Close)

A                                              B
ESTABLISHED                                ESTABLISHED
  |                                            |
  |--- FIN+ACK, SEQ=100, ACK=300 -------->... |
  |   ...FIN+ACK, SEQ=300, ACK=100 <------    |
CLOSING                                    CLOSING
  |                                            |
  |--- ACK, SEQ=101, ACK=301 ------------>     |
  |         <--- ACK, SEQ=301, ACK=101 --------|
TIME-WAIT                                  TIME-WAIT
  |  (2MSL)                                    | (2MSL)
CLOSED                                      CLOSED

6.4 TIME-WAIT 状态的作用

TIME-WAIT 必须持续 2×MSL2 \times MSL2×MSLMSLMSLMSL = 最大段生存时间 = 2分钟,所以是4分钟):
目的1:确保最后一个ACK能到达B(如果ACK丢失,B会重发FIN,A在TIME-WAIT还能响应)
目的2:防止旧连接的延迟包影响新连接
特殊情况:如果有Timestamp选项,可以用改进算法减少TIME-WAIT时间(SHLD-4)。

6.5 RST 重置连接

RST 用于立即强制关闭连接,不等待对方确认。
三种触发RST的情况:

  1. 连接不存在(CLOSED状态):收到任何非RST包,回RST
  2. 非同步状态(LISTEN/SYN-SENT/SYN-RECEIVED):收到不合法的ACK,回RST
  3. 同步状态(ESTABLISHED等):收到不合法包,回ACK(不回RST!用于确认窗口)

第七章:分段与 MSS(最大段大小)

7.1 为什么要控制段的大小?

太大的问题:超过路径上某个链路的MTU(最大传输单元),会被分片或丢弃。
太小的问题:每段都有20字节TCP头 + 20字节IP头 = 40字节开销,数据太少则效率低。

7.2 MSS 选项(Maximum Segment Size)

在SYN包中协商,告诉对方"你最多可以给我发这么大的段":

格式:Kind=2, Length=4, MSS值(2字节)
 0         1         2         3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|       2       |     4         |       MSS 值 (16位)           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

默认值

  • IPv4:536字节(576−40576 - 4057640,576是IPv4最小重组大小,40是IPv4+TCP头)
  • IPv6:1220字节(1280−601280 - 60128060
    实际有效发送MSS公式
    Eff.snd.MSS=min⁡(SendMSS+20, MMS_S)−TCPhdrsize−IPoptionsize Eff.snd.MSS = \min(SendMSS + 20,\ MMS\_S) - TCPhdrsize - IPoptionsize Eff.snd.MSS=min(SendMSS+20, MMS_S)TCPhdrsizeIPoptionsize
    其中:
  • SendMSSSendMSSSendMSS:从对端收到的MSS选项值
  • MMS_SMMS\_SMMS_S:IP层允许的最大传输单元
  • TCPhdrsizeTCPhdrsizeTCPhdrsize:TCP头部大小(含选项,至少20字节)
  • IPoptionsizeIPoptionsizeIPoptionsize:IP选项/扩展头大小

7.3 路径 MTU 发现(PMTUD)

TCP自己并不知道路径上每条链路的MTU,PMTUD帮助动态发现:

客户端          路由器R1(MTU=1500)    路由器R2(MTU=576)     服务器
   |                   |                    |                  |
   |--- 大包(1400字) -->|                    |                  |
   |                   |--- 太大了 -------->|                  |
   |                   |                    X(丢弃)             |
   |                   |<-- ICMP "需要分片" |                  |
   |<-- ICMP 报告 ------|                   |                  |
   |                                        |                  |
   | (调小MSS,重发)                         |                  |
   |--- 小包(556字) --->|--- 可以通过 ------>|--- 到达 -------->|

7.4 Nagle 算法——避免小包泛滥

问题背景:SSH等交互应用,每次只发1个字符,但每个字符都有40字节TCP+IP头,效率极低("糊涂窗口综合症"的一种)。
Nagle算法规则

如果 有未确认的数据在路上 (SND.NXT > SND.UNA):
    则 缓冲新数据,直到:
        (a) 可以发一个满尺寸段(>= Eff.snd.MSS),或
        (b) 所有之前的数据都被确认了

简单说:要么发大包,要么等确认
注意:应用程序可以通过 TCP_NODELAY socket选项关闭Nagle算法(MUST-17),比如需要低延迟的游戏或实时交互。

第八章:数据传输与可靠性机制

8.1 重传超时(RTO)

数据发出后,如果在RTO时间内没有收到ACK,就重传。
RTO必须动态计算,因为网络RTT(往返时延)变化很大。
RFC 9293 要求使用 RFC 6298 的算法(MUST-18):
SRTT=(1−α)×SRTT+α×RTT_sample(α=1/8) SRTT = (1 - \alpha) \times SRTT + \alpha \times RTT\_sample \quad (\alpha = 1/8) SRTT=(1α)×SRTT+α×RTT_sample(α=1/8)
RTTVAR=(1−β)×RTTVAR+β×∣SRTT−RTT_sample∣(β=1/4) RTTVAR = (1 - \beta) \times RTTVAR + \beta \times |SRTT - RTT\_sample| \quad (\beta = 1/4) RTTVAR=(1β)×RTTVAR+β×SRTTRTT_sample(β=1/4)
RTO=SRTT+4×RTTVAR RTO = SRTT + 4 \times RTTVAR RTO=SRTT+4×RTTVAR
其中:

  • SRTTSRTTSRTT:平滑化的RTT估计(指数移动平均)
  • RTTVARRTTVARRTTVAR:RTT的方差估计(衡量抖动)
  • 重传时RTO翻倍(指数退避)
    Karn算法:重传后不用重传包的ACK来更新RTT样本(因为不知道ACK对应哪次发送)。

8.2 拥塞控制

TCP必须(MUST-19)实现基本拥塞控制算法:
慢启动(Slow Start)

连接初始时,拥塞窗口(cwnd)很小(通常1~10个MSS)
每收到一个ACK,cwnd += 1 个MSS
→ 每个RTT,cwnd翻倍(指数增长)
→ 直到 cwnd >= ssthresh(慢启动阈值)

拥塞避免(Congestion Avoidance)

当 cwnd >= ssthresh 时:
每收到一个ACK,cwnd += MSS²/cwnd
→ 每个RTT,cwnd += 1 个MSS(线性增长)

快速重传(Fast Retransmit)

收到3个重复ACK → 不等RTO超时,立即重传丢失的段
→ ssthresh = max(FlightSize/2, 2*MSS)
→ cwnd = ssthresh(快速恢复)

图示:

cwnd
  ^
  |         /(慢启动,指数)
  |        /
ssthresh  / \___(拥塞避免,线性)
  |      /      \____
  |     /              \___(检测到丢包,cwnd骤降)
  |    /
  +--+--+--+--+--+--+--+--+-> 时间(RTT)
     |           |
   连接开始    首次丢包

8.3 流量控制——滑动窗口

接收方控制
接收方在每个ACK中告诉发送方当前窗口大小(RCV.WND),发送方不能超过这个量。
“糊涂窗口综合症”(Silly Window Syndrome, SWS)
当接收方缓冲区快满时,可能广告一个很小的窗口(比如1字节),导致发送方只能发微小的段,效率极低。
解决方案——双端都要配合
接收方:只有在可以广告足够大的窗口时才更新窗口:
ΔW=RCV.BUFF−RCV.USER−RCV.WND≥min⁡(12×RCV.BUFF, Eff.snd.MSS) \Delta W = RCV.BUFF - RCV.USER - RCV.WND \geq \min\left(\frac{1}{2} \times RCV.BUFF,\ Eff.snd.MSS\right) ΔW=RCV.BUFFRCV.USERRCV.WNDmin(21×RCV.BUFF, Eff.snd.MSS)
发送方:只有满足以下条件之一才发送:

  1. 可以发一个满尺寸段:min⁡(D,U)≥Eff.snd.MSS\min(D, U) \geq Eff.snd.MSSmin(D,U)Eff.snd.MSS
  2. Push了且所有队列数据都能发
  3. 可以发至少 12\frac{1}{2}21 最大窗口的数据
  4. 超时强制发送(override timeout,0.1~1秒)

8.4 零窗口探测(Zero-Window Probing, ZWP)

当接收方广告窗口=0时,发送方必须定期发送探测包(MUST-36),防止死锁:

发送方                              接收方
   |--- 数据 ------------------->   |
   |   <--- ACK, WIN=0 ------------|  ← 接收缓冲满了,窗口关闭
   |                               |
   |   (等待 RTO 时间)             |  ← 接收方处理数据,释放了缓冲区
   |                               |
   |--- 探测包(1字节数据)-------> |  ← 但窗口更新的ACK可能丢了!
   |   <--- ACK, WIN=4096 ---------|  ← 接收方在探测回应中带上新窗口
   |                               |
   |--- 正常数据传输继续 ---------->|

第一个探测应在RTO后发送(SHLD-29),后续探测指数退避(SHLD-30)。

8.5 Keep-Alive(TCP保活)

当连接长时间空闲时,发送探测包检测对端是否还活着。
默认关闭(MUST-25),应用程序可以开启(MUST-24)。
默认间隔:至少2小时(MUST-28)。
注意:不能因为某次探测没有响应就认为连接断了(MUST-29),因为ACK本身不保证可靠传输。

8.6 延迟ACK(Delayed ACK)

为提高效率,接收方不必每收到一个段就立即发ACK,可以稍微等一下,把ACK和数据一起发,或者把多个段的ACK合并成一个。
限制

  • 延迟不能超过 500ms(MUST-40)
  • 至少每收到2个满尺寸段就要发一次ACK(SHLD-19)

第九章:TCP 选项

9.1 必须支持的三个选项


Kind 长度 名称 作用
0 1字节 EOL(End of Option List) 选项列表结束标志
1 1字节 NOP(No Operation) 填充,用于对齐后续选项到字边界
2 4字节 MSS(Maximum Segment Size) 协商最大段大小,仅在SYN中出现

9.2 常用但非必须的选项


选项 RFC 作用
SACK(选择确认) RFC 2018 精确告知收到了哪些不连续的段,减少不必要重传
Timestamp RFC 7323 精确RTT测量 + PAWS(防序列号回绕)
Window Scale RFC 7323 将窗口大小扩展到最大约1GB(16位×最大14位移位)

9.3 选项格式

单字节选项(Kind 0 和 Kind 1):
+--------+
|  Kind  |
+--------+
多字节选项(Kind 2 及其他):
+--------+--------+--------+--------+...
|  Kind  | Length |      Data...    |
+--------+--------+--------+--------+...
         ↑ Length包含Kind和Length本身

重要规则(MUST-6):实现必须忽略不认识的选项(不能报错),以保证前向兼容。

第十章:TCP 安全性

10.1 TCP 本身没有加密

TCP头部是明文传输的,路径上的任何节点都能看到:

  • 源端口、目标端口
  • 序列号、确认号
  • 所有标志位
  • 数据内容

10.2 主要安全威胁


攻击类型 原理 防御措施
SYN洪泛 发送大量SYN包耗尽服务器TCB资源 SYN Cookie
RST注入 伪造RST包强制断开连接 RFC 5961:RST只接受SEQ=RCV.NXT的
序列号预测 猜测ISN,伪造数据 RFC 6528:ISN包含密码学PRF
ICMP攻击 用ICMP错误包干扰TCP连接 RFC 5927:严格验证ICMP包中的TCP信息

10.3 加密保护方案

完整保护(包括TCP头部):
    → IPsec (网络层加密)
仅数据完整性+认证:
    → TCP-AO (TCP Authentication Option, RFC 5925)
    → 替代已废弃的TCP-MD5
仅数据内容保护:
    → TLS (在TCP之上的应用层加密)
    → tcpcrypt (实验性TCP扩展)

第十一章:完整的 C++ TCP 状态机演示

#include <iostream>
#include <string>
#include <cstdint>
#include <functional>
// ============================================================
// TCP 状态枚举
// ============================================================
enum class TcpState {
    CLOSED,
    LISTEN,
    SYN_SENT,
    SYN_RECEIVED,
    ESTABLISHED,
    FIN_WAIT_1,
    FIN_WAIT_2,
    CLOSE_WAIT,
    CLOSING,
    LAST_ACK,
    TIME_WAIT
};
// 将状态转为字符串(方便打印)
const char* state_name(TcpState s) {
    switch (s) {
        case TcpState::CLOSED:        return "CLOSED";
        case TcpState::LISTEN:        return "LISTEN";
        case TcpState::SYN_SENT:      return "SYN-SENT";
        case TcpState::SYN_RECEIVED:  return "SYN-RECEIVED";
        case TcpState::ESTABLISHED:   return "ESTABLISHED";
        case TcpState::FIN_WAIT_1:    return "FIN-WAIT-1";
        case TcpState::FIN_WAIT_2:    return "FIN-WAIT-2";
        case TcpState::CLOSE_WAIT:    return "CLOSE-WAIT";
        case TcpState::CLOSING:       return "CLOSING";
        case TcpState::LAST_ACK:      return "LAST-ACK";
        case TcpState::TIME_WAIT:     return "TIME-WAIT";
        default:                      return "UNKNOWN";
    }
}
// ============================================================
// 简化的 TCP 端点(仅演示状态机)
// ============================================================
struct TcpEndpoint {
    TcpState state = TcpState::CLOSED;
    std::string name;           // 端点名称,用于打印
    uint32_t iss = 0;           // 初始发送序列号
    uint32_t snd_nxt = 0;       // 下一个发送序列号
    uint32_t snd_una = 0;       // 最老的未确认序列号
    uint32_t rcv_nxt = 0;       // 期望收到的下一个序列号
    explicit TcpEndpoint(const std::string& n, uint32_t initial_seq)
        : name(n), iss(initial_seq), snd_nxt(initial_seq), snd_una(initial_seq)
    {}
    // 打印当前状态
    void print_state() const {
        std::cout << "  [" << name << "] 状态 -> " << state_name(state) << "\n";
    }
    // ---- 用户操作 ----
    // 被动打开(服务器调用,进入LISTEN)
    void passive_open() {
        if (state == TcpState::CLOSED) {
            state = TcpState::LISTEN;
            std::cout << name << ": 被动OPEN, 进入LISTEN\n";
            print_state();
        }
    }
    // 主动打开(客户端调用,发送SYN)
    // 返回要发送的SYN包信息
    uint32_t active_open() {
        if (state == TcpState::CLOSED) {
            state = TcpState::SYN_SENT;
            // ISN已在构造时设置
            snd_nxt = iss + 1;  // SYN占一个序号
            std::cout << name << ": 主动OPEN, 发送SYN(SEQ=" << iss << "), 进入SYN-SENT\n";
            print_state();
            return iss;  // 返回ISN,供对端使用
        }
        return 0;
    }
    // 调用CLOSE(应用程序请求关闭)
    void close_call() {
        if (state == TcpState::ESTABLISHED) {
            state = TcpState::FIN_WAIT_1;
            std::cout << name << ": 调用CLOSE, 发送FIN(SEQ=" << snd_nxt << "), 进入FIN-WAIT-1\n";
            snd_nxt++;  // FIN占一个序号
            print_state();
        } else if (state == TcpState::CLOSE_WAIT) {
            state = TcpState::LAST_ACK;
            std::cout << name << ": 调用CLOSE(从CLOSE-WAIT), 发送FIN, 进入LAST-ACK\n";
            snd_nxt++;
            print_state();
        }
    }
    // ---- 接收到的段处理 ----
    // 收到 SYN(服务器端处理)
    void receive_syn(uint32_t seg_seq, uint32_t server_iss) {
        if (state == TcpState::LISTEN) {
            rcv_nxt = seg_seq + 1;  // 期望收 SYN 后的第一个数据
            iss = server_iss;
            snd_nxt = iss + 1;
            snd_una = iss;
            state = TcpState::SYN_RECEIVED;
            std::cout << name << ": 收到SYN(SEQ=" << seg_seq << "), "
                      << "发送SYN+ACK(SEQ=" << iss << ",ACK=" << rcv_nxt << "), "
                      << "进入SYN-RECEIVED\n";
            print_state();
        }
    }
    // 收到 SYN+ACK(客户端处理)
    void receive_syn_ack(uint32_t seg_seq, uint32_t seg_ack) {
        if (state == TcpState::SYN_SENT) {
            // 检查ACK是否合法:SND.UNA < SEG.ACK <= SND.NXT
            if (seg_ack == snd_nxt) {  // 简化:ack应该等于 ISS+1
                rcv_nxt = seg_seq + 1;
                snd_una = seg_ack;
                state = TcpState::ESTABLISHED;
                std::cout << name << ": 收到SYN+ACK(SEQ=" << seg_seq
                          << ",ACK=" << seg_ack << "), "
                          << "发送ACK(ACK=" << rcv_nxt << "), 进入ESTABLISHED\n";
                print_state();
            }
        }
    }
    // 收到对SYN的ACK(服务器端:完成三次握手)
    void receive_ack_of_syn(uint32_t seg_ack) {
        if (state == TcpState::SYN_RECEIVED) {
            if (seg_ack == snd_nxt) {
                snd_una = seg_ack;
                state = TcpState::ESTABLISHED;
                std::cout << name << ": 收到ACK(ACK=" << seg_ack
                          << "), 三次握手完成, 进入ESTABLISHED\n";
                print_state();
            }
        }
    }
    // 收到 FIN
    void receive_fin(uint32_t seg_seq) {
        if (state == TcpState::ESTABLISHED) {
            rcv_nxt = seg_seq + 1;  // FIN占一个序号
            state = TcpState::CLOSE_WAIT;
            std::cout << name << ": 收到FIN(SEQ=" << seg_seq << "), "
                      << "发送ACK(ACK=" << rcv_nxt << "), 进入CLOSE-WAIT\n";
            print_state();
        } else if (state == TcpState::FIN_WAIT_1) {
            rcv_nxt = seg_seq + 1;
            state = TcpState::CLOSING;
            std::cout << name << ": 收到FIN(同时关闭), 进入CLOSING\n";
            print_state();
        } else if (state == TcpState::FIN_WAIT_2) {
            rcv_nxt = seg_seq + 1;
            state = TcpState::TIME_WAIT;
            std::cout << name << ": 收到FIN, 发送ACK, 进入TIME-WAIT(等2MSL)\n";
            print_state();
        }
    }
    // 收到 ACK(通用)
    void receive_ack(uint32_t seg_ack) {
        if (state == TcpState::FIN_WAIT_1 && seg_ack == snd_nxt) {
            snd_una = seg_ack;
            state = TcpState::FIN_WAIT_2;
            std::cout << name << ": FIN被确认, 进入FIN-WAIT-2\n";
            print_state();
        } else if (state == TcpState::CLOSING && seg_ack == snd_nxt) {
            state = TcpState::TIME_WAIT;
            std::cout << name << ": 收到ACK, 进入TIME-WAIT\n";
            print_state();
        } else if (state == TcpState::LAST_ACK && seg_ack == snd_nxt) {
            state = TcpState::CLOSED;
            std::cout << name << ": 最后的ACK收到, 进入CLOSED, 连接终止\n";
            print_state();
        }
    }
    // 2MSL超时(模拟)
    void time_wait_timeout() {
        if (state == TcpState::TIME_WAIT) {
            state = TcpState::CLOSED;
            std::cout << name << ": 2MSL超时, 进入CLOSED\n";
            print_state();
        }
    }
};
// ============================================================
// 演示:完整的三次握手 + 正常关闭
// ============================================================
int main() {
    std::cout << "========== 三次握手演示 ==========\n";
    TcpEndpoint client("客户端", 100);  // 客户端ISN=100
    TcpEndpoint server("服务器", 300);  // 服务器ISN=300
    // 服务器先进入LISTEN
    server.passive_open();
    std::cout << "\n--- 握手第1步:客户端发SYN ---\n";
    uint32_t client_isn = client.active_open();
    std::cout << "\n--- 握手第2步:服务器回SYN+ACK ---\n";
    server.receive_syn(client_isn, 300);
    std::cout << "\n--- 握手第3步:客户端发ACK ---\n";
    // 客户端收到服务器的SYN+ACK:SEQ=300, ACK=101
    client.receive_syn_ack(300, 101);  // SEQ=300(服务器ISN), ACK=101(client_isn+1)
    // 服务器收到最终的ACK
    server.receive_ack_of_syn(301);  // ACK=server_ISN+1=301
    std::cout << "\n========== 数据传输(略) ==========\n";
    std::cout << "  两端都处于 ESTABLISHED,正常发送数据...\n";
    std::cout << "\n========== 四次挥手演示 ==========\n";
    std::cout << "\n--- 挥手第1步:客户端发FIN ---\n";
    client.close_call();  // 客户端主动关闭
    std::cout << "\n--- 挥手第2步:服务器回ACK,服务器还有数据可发 ---\n";
    server.receive_fin(101);  // 服务器收到FIN(SEQ=101)
    std::cout << "\n--- 挥手第3+4步:服务器发FIN,客户端回ACK ---\n";
    // 服务器应用程序也调用CLOSE
    server.close_call();  // 服务器发FIN,进入LAST-ACK
    // 客户端收到服务器的FIN(SEQ=301,因为SYN占了300,FIN占了301)
    client.receive_fin(301);  // 客户端进入TIME-WAIT
    // 服务器收到最后的ACK
    server.receive_ack(302);  // 服务器进入CLOSED
    // 客户端等待2MSL后关闭
    std::cout << "\n--- 客户端等待2MSL(约4分钟),然后关闭 ---\n";
    client.time_wait_timeout();
    std::cout << "\n========== 连接完全关闭 ==========\n";
    return 0;
}

第十二章:重要的 MUST/SHOULD 要求汇总

RFC 9293 对各功能打了标签(MUST = 必须实现,SHOULD = 强烈建议,MAY = 可选)。
以下是最关键的几条:

编号 要求 级别
MUST-1 窗口必须作为无符号数处理 MUST
MUST-2 发送方必须计算校验和 MUST
MUST-3 接收方必须验证校验和 MUST
MUST-8 使用时钟驱动的ISN生成器 MUST
MUST-9 ISN的PRF分量不能从外部计算 MUST
MUST-10 支持同时打开 MUST
MUST-13 TIME-WAIT持续2MSL MUST
MUST-14 实现发送和接收MSS选项 MUST
MUST-17 应用程序可以关闭Nagle算法 MUST
MUST-18 实现Karn算法(RTO计算) MUST
MUST-19 实现慢启动、拥塞避免、指数退避 MUST
MUST-36 支持零窗口探测 MUST
MUST-40 延迟ACK不超过500ms MUST
SHLD-7 实现Nagle算法 SHOULD
SHLD-8 实现ECN(显式拥塞通知) SHOULD
SHLD-18 实现延迟ACK SHOULD
MAY-5 可以实现Keep-Alive MAY

附录:关键术语中英对照


英文术语 中文 简释
TCB 传输控制块 保存一条TCP连接所有状态的数据结构
ISN/ISS/IRS 初始序列号 连接建立时选择的起始序号
MSL 最大段生存时间 一个TCP段在网络中存活的最长时间(2分钟)
MSS 最大段大小 单个TCP段可携带的最大数据量
MTU 最大传输单元 链路层一次能传输的最大字节数
RTT 往返时延 一个包从发出到收到ACK的时间
RTO 重传超时 等待ACK的最长时间,超时则重传
cwnd 拥塞窗口 拥塞控制算法维护的发送限制
SRTT 平滑RTT RTT的指数移动平均值
SWS 糊涂窗口综合症 由于小窗口导致的效率极低的稳定状态
PMTUD 路径MTU发现 动态发现路径上最小MTU的机制
PAWS 防序列号回绕 用Timestamp选项防止高速连接序列号回绕
SACK 选择确认 精确报告收到了哪些不连续的数据
ECN 显式拥塞通知 路由器在包头打标记通知拥塞,避免丢包
ZWP 零窗口探测 窗口为0时定期发送探测,防止死锁

Logo

免费领 200 小时云算力,进群参与显卡、AI PC 幸运抽奖

更多推荐