本文从零开始,用最通俗的语言解释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个)


标志全称作用
CWRCongestion Window Reduced拥塞窗口已缩小(配合ECN使用)
ECEECN-Echo通知对方网络出现拥塞
URGUrgent紧急指针字段有效
ACKAcknowledgment确认号字段有效
PSHPush请立即将数据推送给应用层,不要缓冲
RSTReset强制断开连接(出错时使用)
SYNSynchronize同步序列号,用于建立连接
FINFinish发送方没有更多数据了,请求关闭连接

窗口大小 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,发送方重传
    序列号空间范围:000 到 232−12^{32} - 1232−1(约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.UNAsend unacknowledged最老的未被确认的序列号(发送窗口左边界)
SND.NXTsend next下一个要发送的序列号
SND.WNDsend window发送窗口大小(由接收方广告)
SND.UPsend urgent pointer发送方的紧急指针
SND.WL1-上次更新窗口时用的段序列号(防止用旧段更新窗口)
SND.WL2-上次更新窗口时用的确认号
ISSinitial send sequence初始发送序列号

接收方的变量:

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

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

3.3 段的可接受性判断

一个到来的数据段,接收方如何判断是否接受?
接受条件(有数据时):RCV.NXT≤SEG.SEQ<RCV.NXT+RCV.WND \text{接受条件(有数据时):} \quad RCV.NXT \leq SEG.SEQ < RCV.NXT + RCV.WND 接受条件(有数据时):RCV.NXT≤SEG.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.NXT≤SEG.SEQ+SEG.LEN−1<RCV.NXT+RCV.WND
四种特殊情况的完整表格:

段长度接收窗口接受条件
00SEG.SEQ=RCV.NXTSEG.SEQ = RCV.NXTSEG.SEQ=RCV.NXT
0>0> 0>0RCV.NXT≤SEG.SEQ<RCV.NXT+RCV.WNDRCV.NXT \leq SEG.SEQ < RCV.NXT + RCV.WNDRCV.NXT≤SEG.SEQ<RCV.NXT+RCV.WND
>0> 0>00不接受(窗口为零时不接受数据)
>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×MSL(MSLMSLMSL = 最大段生存时间 = 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 - 40576−40,576是IPv4最小重组大小,40是IPv4+TCP头)
  • IPv6:1220字节(1280−601280 - 601280−60)
    实际有效发送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)−TCPhdrsize−IPoptionsize
    其中:
  • 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+β×∣SRTT−RTT_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.BUFF−RCV.USER−RCV.WND≥min(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长度名称作用
01字节EOL(End of Option List)选项列表结束标志
11字节NOP(No Operation)填充,用于对齐后续选项到字边界
24字节MSS(Maximum Segment Size)协商最大段大小,仅在SYN中出现

9.2 常用但非必须的选项


选项RFC作用
SACK(选择确认)RFC 2018精确告知收到了哪些不连续的段,减少不必要重传
TimestampRFC 7323精确RTT测量 + PAWS(防序列号回绕)
Window ScaleRFC 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-9ISN的PRF分量不能从外部计算MUST
MUST-10支持同时打开MUST
MUST-13TIME-WAIT持续2MSLMUST
MUST-14实现发送和接收MSS选项MUST
MUST-17应用程序可以关闭Nagle算法MUST
MUST-18实现Karn算法(RTO计算)MUST
MUST-19实现慢启动、拥塞避免、指数退避MUST
MUST-36支持零窗口探测MUST
MUST-40延迟ACK不超过500msMUST
SHLD-7实现Nagle算法SHOULD
SHLD-8实现ECN(显式拥塞通知)SHOULD
SHLD-18实现延迟ACKSHOULD
MAY-5可以实现Keep-AliveMAY

附录:关键术语中英对照


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

Logo

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

更多推荐