本文基于1981年9月发布的 RFC 791(互联网协议规范),从零开始用中文讲解 IP 协议的设计思想与工作机制。

一、背景与动机

IP协议是什么?

IP(Internet Protocol,互联网协议)是整个互联网的"地址+投递"系统。
一句话理解: 就像快递公司,IP负责把一包数据从发送方送到接收方,至于路上丢没丢、顺序对不对,它不管。
RFC 791 是 IP 协议的官方规范文档,1981年由 Jon Postel 编写,定义了我们今天仍在使用的 IPv4 协议基础。
这套系统最初叫做 catenet(互联网络),设计目标是让不同类型的网络能够互相连通,就像不同国家的邮政系统通过标准信封格式协作投递信件一样。

二、IP协议的范围与定位

IP只做两件事

IP协议的职责非常明确,只做两件核心事情

  1. 寻址(Addressing):知道数据从哪来、到哪去
  2. 分片与重组(Fragmentation & Reassembly):把太大的数据切成小块传输,到达后再拼回来

IP协议不做什么

IP是一个"尽力而为"的协议,它刻意不提供以下功能:

不提供的功能 谁来负责
端到端可靠性(丢了不重传) TCP负责
流量控制(防止发太快) TCP负责
数据排序(保证顺序) TCP负责
数据错误校验(只校验头部) 上层协议负责

设计哲学: IP保持简单,把复杂的可靠性需求交给上层协议(如TCP)处理。这种分层设计让整个系统更灵活。

三、IP在协议栈中的位置

IP协议处于协议栈的中间层,上接应用层协议,下接本地网络协议。

协议层次结构(从上到下):
+--------+ +-------+ +-------+     +-------+
| Telnet | |  FTP  | | TFTP  | ... | 其他  |   <- 应用层
+--------+ +-------+ +-------+     +-------+
      |       |           |            |
    +-------+         +-------+     +-------+
    |  TCP  |         |  UDP  | ... | 其他  |   <- 传输层
    +-------+         +-------+     +-------+
        |                 |            |
    +---------------------------------------+
    |         IP协议  +  ICMP             |   <- 网络层(IP层)
    +---------------------------------------+
                        |
            +-----------------------+
            |    本地网络协议        |   <- 数据链路层/物理层
            +-----------------------+
                (以太网、Wi-Fi等)

调用关系举例(TCP发送数据的流程):

TCP模块
  |  把TCP段(头部+用户数据)作为"内容"交给IP
  v
IP模块
  |  在外面加上IP头(含源/目标IP地址)
  v
本地网络接口(如以太网驱动)
  |  再在外面加上本地网络头(如MAC地址)
  v
物理传输(网线/无线电波)

这就像套娃信封:数据被一层层包裹,每层负责自己那部分信息。

四、IP协议的四个关键机制

IP头部通过四个字段来控制数据传输行为:

4.1 服务类型(Type of Service,ToS)

用来告诉路由器"我希望以什么质量传输这个数据包"。
例如:

  • 视频通话 → 要求低延迟
  • 文件下载 → 要求高吞吐量
  • 普通浏览 → 默认即可
    路由器根据 ToS 值选择最合适的传输路径或参数。

4.2 生存时间(Time to Live,TTL)

TTL是一个倒计时计数器,防止数据包在网络里无限循环。
工作原理:

发送方设置 TTL = 64(举例)
每经过一个路由器(网关):TTL = TTL - 1
                 TTL=64         TTL=63         TTL=62
发送方 ---------> 路由器A -------> 路由器B -------> 目的地
如果 TTL 减到 0 还没到达目的地:
  数据包被丢弃,路由器向发送方发送 ICMP "超时" 错误消息

比喻: 就像快递单上写着"最多中转5次,超过就销毁",防止快递在仓库间无限流转。
TTL剩余=TTL初始−经过的路由器数量TTL_{剩余} = TTL_{初始} - \text{经过的路由器数量}TTL剩余=TTL初始经过的路由器数量
TTL剩余=0TTL_{剩余} = 0TTL剩余=0 时,数据包被丢弃。

4.3 选项(Options)

提供一些可选的控制功能,正常通信不需要用到:

  • 时间戳(记录经过哪些节点)
  • 安全标记
  • 特殊路由指定(指定必须经过某个路由器)

4.4 头部校验和(Header Checksum)

用于验证 IP 头部在传输过程中是否发生了损坏

  • 如果校验失败 → 立即丢弃该数据包
  • 注意:只校验头部,不校验数据内容

重要: IP不提供重传机制。数据包丢了就丢了,错误只能通过 ICMP(互联网控制消息协议) 上报,由上层协议决定是否重发。

五、数据包传输的完整流程

以"两台电脑通过一个网关通信"为例,演示数据包从发送到接收的全过程:

发送方(主机A)                网关                接收方(主机B)
+------------+           +---------+           +------------+
| 应用程序   |           |         |           | 应用程序   |
|     |      |           |         |           |     ^      |
|     v      |           |         |           |     |      |
| IP模块     |           | IP模块  |           | IP模块     |
|     |      |           |  /   \  |           |     ^      |
|     v      |           | /     \ |           |     |      |
| 本地网络   |           |本地网络1 本地网络2   | 本地网络   |
| 接口(LNI-1)|           |(LNI-1)(LNI-2)      | 接口(LNI-2)|
+-----+------+           +---^---+----+--------+-----^------+
      |                      |        |               |
      +----[本地网络1]--------+        +--[本地网络2]--+

逐步拆解:
第1步:应用程序发起发送

应用程序准备好数据
  -> 调用本地IP模块
  -> 传入:目标IP地址 + 数据内容

第2步:IP模块封装数据包

IP模块创建IP头部(含源IP、目标IP、TTL等)
  -> 数据包 = IP头部 + 应用数据
  -> 查询路由表,发现需要先发给网关

第3步:本地网络接口发送

本地网络接口(如以太网)在IP数据包外再加一层本地头部
  -> 最终帧 = 以太网头部 + IP数据包
  -> 通过网线/Wi-Fi发送出去

第4步:网关处理

网关收到数据帧
  -> 剥掉本地网络头部(以太网头部)
  -> IP模块查看目标IP地址
  -> 发现目标在另一个网络(本地网络2)
  -> 重新封装,加上本地网络2的头部
  -> 转发到目标主机

第5步:目标主机接收

目标主机收到数据帧
  -> 剥掉本地网络头部
  -> IP模块识别出这是给本机应用程序的数据
  -> 把数据交给对应的应用程序

六、IP地址结构

名称、地址、路由的区分

RFC 791 明确区分了三个概念:

概念 含义 类比
名称(Name) 你要找的是什么 人名:“张三”
地址(Address) 它在哪里 家庭住址:“北京市XX路XX号”
路由(Route) 怎么到达那里 导航路线

IP协议主要处理地址,名称到地址的转换由上层协议(如DNS)负责。

IPv4地址格式

IP地址是**32位(4字节)**的固定长度数值。
IP地址=32 bits=4 bytes(字节)=4 组0-255的十进制数\text{IP地址} = 32\text{ bits} = 4\text{ bytes(字节)} = 4\text{ 组0-255的十进制数}IP地址=32 bits=4 bytes(字节)=4 0-255的十进制数
例如:192.168.1.1

IPv4的三种地址类别(RFC 791时代)

RFC 791 定义了三种地址格式,通过最高位来区分:

A类地址(Class A):
位:  0 | NNNNNNN | LLLLLLLLLLLLLLLLLLLLLLLL
      ^    7位       24位本地地址
      高位=0
网络数量:$2^7 = 128$ 个网络
每网络主机数:$2^{24} = 16,777,216$ 台
B类地址(Class B):
位:  10 | NNNNNNNNNNNNNN | LLLLLLLLLLLLLLLL
      ^     14位             16位本地地址
      高位=10
网络数量:$2^{14} = 16,384$ 个网络
每网络主机数:$2^{16} = 65,536$ 台
C类地址(Class C):
位:  110 | NNNNNNNNNNNNNNNNNNNNN | LLLLLLLL
      ^          21位               8位本地地址
      高位=110
网络数量:$2^{21} = 2,097,152$ 个网络
每网络主机数:$2^8 = 256$ 台

N = 网络号位,L = 本地地址位
A类最大主机数=224=16,777,216\text{A类最大主机数} = 2^{24} = 16,777,216A类最大主机数=224=16,777,216
B类最大主机数=216=65,536\text{B类最大主机数} = 2^{16} = 65,536B类最大主机数=216=65,536
C类最大主机数=28=256\text{C类最大主机数} = 2^{8} = 256C类最大主机数=28=256

七、分片与重组机制

为什么需要分片?

不同网络对数据包大小有不同限制,这个上限叫做 MTU(最大传输单元)

问题场景:
网络A(MTU=4000字节)        网络B(MTU=1500字节)
+-------------------+        +-------------------+
|                   |        |                   |
|  发送一个3000字节  |------> |  最大只能接收     |
|  的大数据包       |  网关  |  1500字节的包     |
|                   |        |                   |
+-------------------+        +-------------------+
解决方案:在网关处把3000字节的包切成多个小包(分片)

分片相关的四个关键字段


字段 作用
标识(Identification) 同一原始数据包的所有分片共用同一个ID
分片偏移(Fragment Offset) 这个分片在原始数据包中的位置(从第几个字节开始)
更多分片标志(More Fragments Flag) =1表示后面还有分片,=0表示这是最后一片
不分片标志(Don’t Fragment Flag) =1表示禁止分片,若路径中必须分片则直接丢弃

分片过程示例

假设原始数据包数据部分为 3000 字节,途经 MTU=1500 的网络(IP头部占20字节,数据最多 1500−20=14801500 - 20 = 1480150020=1480 字节):
分片必须在8字节边界切割:
每片最大数据量=⌊14808⌋×8=1480 字节\text{每片最大数据量} = \lfloor \frac{1480}{8} \rfloor \times 8 = 1480 \text{ 字节}每片最大数据量=81480×8=1480 字节

原始数据包(数据3000字节,ID=12345):
+--IP头--+-----数据[0~2999]共3000字节-----+
  MF=0, Offset=0
分片后:
分片1:
+--IP头--+--数据[0~1479]共1480字节--+
  ID=12345, MF=1(后面还有), Offset=0
分片2:
+--IP头--+--数据[1480~2959]共1480字节--+
  ID=12345, MF=1(后面还有), Offset=185
  (185 = 1480/8,表示从第1480字节开始)
分片3:
+--IP头--+--数据[2960~2999]共40字节--+
  ID=12345, MF=0(最后一片), Offset=370
  (370 = 2960/8)

偏移值计算(以8字节为单位):
分片2偏移=14808=185\text{分片2偏移} = \frac{1480}{8} = 185分片2偏移=81480=185
分片3偏移=29608=370\text{分片3偏移} = \frac{2960}{8} = 370分片3偏移=82960=370

重组过程

接收方根据以下四个字段识别同属一个原始数据包的所有分片:
匹配条件={标识ID+源IP+目标IP+协议号}\text{匹配条件} = \{\text{标识ID} + \text{源IP} + \text{目标IP} + \text{协议号}\}匹配条件={标识ID+IP+目标IP+协议号}
重组步骤:

  1. 收集所有 ID 相同的分片
  2. 按 Fragment Offset 排序
  3. MF=0 的那片是最后一片
  4. 拼接所有分片数据,还原原始内容
重组示意:
收到分片1(Offset=0,   MF=1):[数据0~1479]
收到分片3(Offset=370, MF=0):[数据2960~2999]
收到分片2(Offset=185, MF=1):[数据1480~2959]
按偏移排序后拼接:
[0~1479] + [1480~2959] + [2960~2999] = 原始3000字节数据

八、网关(Gateway)的角色

网关就是今天所说的路由器,是连接不同网络的中间设备。

网关内部结构:
              +-----------------------------+
              |  IP协议 + ICMP + GGP协议   |
              +-----------------------------+
                    /               \
        +-----------+           +-----------+
        |  本地网络A |           |  本地网络B |
        +-----------+           +-----------+

网关需要实现:

  • IP协议:转发数据包
  • ICMP:发送控制消息(如TTL超时通知)
  • GGP(网关到网关协议):与其他网关协调路由信息

注意:网关不需要实现 TCP、FTP 等上层协议,它只关心 IP 层的转发。

九、IP协议特性总结


特性 说明
无连接(Connectionless) 每个数据包独立处理,不维护连接状态
尽力而为(Best-effort) 不保证送达,不保证顺序,不保证不重复
无确认(No ACK) 发出去不管收没收到
无重传(No Retransmission) 丢了就丢了
无流控(No Flow Control) 不控制发送速率
头部校验 只验证头部完整性,不验证数据内容
错误报告 通过ICMP上报,不自动处理

十、C++ 演示:IP分片偏移计算

以下代码演示如何计算 IP 分片的偏移值和分片结构:

#include <iostream>    // 标准输入输出
#include <vector>      // 动态数组
#include <string>      // 字符串
#include <cmath>       // 数学函数(floor等)
#include <iomanip>     // 格式化输出(setw等)
// IP分片结构体,模拟一个分片的关键字段
struct IPFragment {
    int  identification;    // 标识符:同一原始包的所有分片共享同一个ID
    int  fragmentOffset;    // 分片偏移(单位:8字节),表示本片数据在原始包中的起始位置
    bool moreFragments;     // 更多分片标志:true=后面还有分片,false=这是最后一片
    int  dataStart;         // 本片数据的起始字节索引(用于展示,非IP头字段)
    int  dataLength;        // 本片数据的长度(字节)
};
// 计算分片函数
// 参数:
//   totalDataSize  - 原始数据总字节数
//   mtu            - 路径MTU(最大传输单元,含IP头)
//   ipHeaderSize   - IP头部大小(通常为20字节)
//   identification - 本次传输的数据包标识ID
// 返回值:所有分片的列表
std::vector<IPFragment> fragment(int totalDataSize,
                                  int mtu,
                                  int ipHeaderSize,
                                  int identification) {
    std::vector<IPFragment> fragments; // 存储所有分片
    // 每个分片最多能携带的数据量
    // 必须是8的倍数(IP协议规定,偏移字段以8字节为单位)
    int maxDataPerFragment = ((mtu - ipHeaderSize) / 8) * 8;
    int offset = 0; // 当前处理到原始数据的哪个字节
    while (offset < totalDataSize) {
        IPFragment frag;
        frag.identification = identification; // 所有分片共用同一个ID
        frag.dataStart = offset; // 本片数据起始位置
        // 计算本片实际数据长度
        // 最后一片可能不足 maxDataPerFragment
        int remaining = totalDataSize - offset;
        frag.dataLength = (remaining > maxDataPerFragment)
                          ? maxDataPerFragment
                          : remaining;
        // 分片偏移 = 当前起始字节 / 8(单位换算为8字节块)
        frag.fragmentOffset = offset / 8;
        // 判断是否还有后续分片
        frag.moreFragments = (offset + frag.dataLength < totalDataSize);
        fragments.push_back(frag); // 加入分片列表
        offset += frag.dataLength; // 移动到下一片的起始位置
    }
    return fragments;
}
// 打印分片信息
void printFragments(const std::vector<IPFragment>& frags, int mtu, int headerSize) {
    std::cout << "原始数据包分片结果(MTU=" << mtu
              << "字节,IP头=" << headerSize << "字节)\n";
    std::cout << std::string(65, '-') << "\n";
    std::cout << std::setw(6)  << "分片号"
              << std::setw(10) << "ID"
              << std::setw(10) << "偏移值"
              << std::setw(8)  << "MF标志"
              << std::setw(12) << "数据起始"
              << std::setw(10) << "数据长度"
              << "\n";
    std::cout << std::string(65, '-') << "\n";
    for (int i = 0; i < (int)frags.size(); ++i) {
        const auto& f = frags[i];
        std::cout << std::setw(6)  << (i + 1)
                  << std::setw(10) << f.identification
                  << std::setw(10) << f.fragmentOffset
                  << std::setw(8)  << (f.moreFragments ? "1(有)" : "0(无)")
                  << std::setw(12) << f.dataStart
                  << std::setw(10) << f.dataLength
                  << "\n";
    }
    std::cout << std::string(65, '-') << "\n";
    std::cout << "共产生 " << frags.size() << " 个分片\n\n";
}
// 模拟重组:验证所有分片能否正确还原
// 通过检查偏移量是否连续、最后一片MF是否为0来验证
bool reassemble(const std::vector<IPFragment>& frags, int totalDataSize) {
    if (frags.empty()) return false;
    int expectedOffset = 0; // 期望的下一个偏移值
    for (int i = 0; i < (int)frags.size(); ++i) {
        const auto& f = frags[i];
        // 检查偏移是否连续(无空隙)
        if (f.fragmentOffset != expectedOffset / 8) {
            std::cout << "重组失败:分片" << (i+1) << "偏移不连续!\n";
            return false;
        }
        expectedOffset += f.dataLength; // 更新期望偏移
        // 最后一片的MF应为false
        if (i == (int)frags.size() - 1 && f.moreFragments) {
            std::cout << "重组失败:最后一片MF标志不为0!\n";
            return false;
        }
    }
    // 检查总数据量是否匹配
    if (expectedOffset != totalDataSize) {
        std::cout << "重组失败:数据总量不匹配!\n";
        return false;
    }
    return true;
}
int main() {
    // 场景1:3000字节数据包经过MTU=1500的网络
    {
        int totalData    = 3000;   // 原始数据大小(字节)
        int mtu          = 1500;   // 网络MTU限制
        int ipHeaderSize = 20;     // IP头部固定大小(字节)
        int id           = 12345;  // 数据包标识ID
        std::cout << "=== 场景1:" << totalData << "字节数据,MTU=" << mtu << " ===\n\n";
        auto frags = fragment(totalData, mtu, ipHeaderSize, id);
        printFragments(frags, mtu, ipHeaderSize);
        bool ok = reassemble(frags, totalData);
        std::cout << "重组验证:" << (ok ? "成功!" : "失败!") << "\n\n";
    }
    // 场景2:500字节数据包无需分片
    {
        int totalData    = 500;
        int mtu          = 1500;
        int ipHeaderSize = 20;
        int id           = 9999;
        std::cout << "=== 场景2:" << totalData << "字节数据,MTU=" << mtu << "(无需分片)===\n\n";
        auto frags = fragment(totalData, mtu, ipHeaderSize, id);
        printFragments(frags, mtu, ipHeaderSize);
        bool ok = reassemble(frags, totalData);
        std::cout << "重组验证:" << (ok ? "成功!" : "失败!") << "\n\n";
    }
    // 场景3:大包经过很小MTU的网络
    {
        int totalData    = 4000;
        int mtu          = 576;   // 早期互联网最小MTU
        int ipHeaderSize = 20;
        int id           = 42;
        std::cout << "=== 场景3:" << totalData << "字节数据,MTU=" << mtu << "(小MTU)===\n\n";
        auto frags = fragment(totalData, mtu, ipHeaderSize, id);
        printFragments(frags, mtu, ipHeaderSize);
        bool ok = reassemble(frags, totalData);
        std::cout << "重组验证:" << (ok ? "成功!" : "失败!") << "\n";
    }
    return 0;
}

十一、整体知识结构图

IP协议 RFC 791

两大核心功能

四个关键机制

地址体系

分片机制

寻址
Addressing

分片与重组
Fragmentation

服务类型 ToS
控制传输质量

生存时间 TTL
防止无限循环

选项 Options
时间戳/安全/路由

头部校验和
验证头部完整性

A类地址
7位网络+24位主机

B类地址
14位网络+16位主机

C类地址
21位网络+8位主机

标识 ID
区分不同数据包

分片偏移
记录位置

更多分片标志 MF
标记是否最后一片

不分片标志 DF
禁止分片则丢弃

十二、关键术语速查


术语 全称 含义
IP Internet Protocol 互联网协议
Datagram - 数据报,IP传输的基本单位
TTL Time to Live 生存时间,每经过一跳减1
ToS Type of Service 服务类型,指定传输质量偏好
MTU Maximum Transmission Unit 最大传输单元,网络能传输的最大包大小
Fragment - 分片,大包被切割后的小块
Fragment Offset - 分片偏移,以8字节为单位记录位置
MF Flag More Fragments 更多分片标志
DF Flag Don’t Fragment 不分片标志
Gateway - 网关,即路由器
ICMP Internet Control Message Protocol 互联网控制消息协议(报告错误用)
GGP Gateway to Gateway Protocol 网关间协议(路由协调)
Catenet - 互联网络,多个网络互联构成的系统

RFC 791:IP 协议头部规范详解(第3章)

本文从零讲解 IPv4 数据包头部的每一个字段,包含分片/重组的完整伪代码解析、校验和算法、Options 选项详解,以及完整 C++ 实现。

一、IP 头部总览

IP 头部是每个数据包最前面的一段"控制信息",就像快递单上的收发件人、重量、特殊说明等信息。
下面是 IP 头部的完整布局(每行 32 位 = 4 字节):

比特位编号(0 = 最高位,31 = 最低位):
 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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Version |  IHL  |Type of Service|         Total Length        |
|  (4位)  | (4位) |    (8位)      |           (16位)            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|         Identification        |Flags|    Fragment Offset      |
|           (16位)              |(3位)|        (13位)           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  Time to Live |   Protocol    |       Header Checksum         |
|    (8位)      |    (8位)      |           (16位)              |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                        Source Address                         |
|                           (32位)                              |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                     Destination Address                       |
|                           (32位)                              |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                   Options(可变长度)        |   Padding      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

最小头部 = 5行 × 4字节 = 20字节(无Options时)
最大头部 = 60字节(IHL最大值15,即 15×4=6015 \times 4 = 6015×4=60 字节)

二、每个字段逐一详解

2.1 Version(版本)— 4 位

表示 IP 协议的版本号。

  • RFC 791 描述的是版本 4,即 IPv4
  • 二进制值:0100
    比喻: 就像文件格式版本号,接收方先看版本,决定如何解读后续数据。

2.2 IHL(Internet Header Length,头部长度)— 4 位

表示 IP 头部有多少个 32位字(4字节块)
头部实际字节数=IHL×4\text{头部实际字节数} = IHL \times 4头部实际字节数=IHL×4

IHL值 头部字节数 含义
5 20 字节 最小头部(无Options)
6 24 字节 含4字节Options
15 60 字节 最大头部

最小合法值为 5,即 5×4=205 \times 4 = 205×4=20 字节。
IHL 同时也指向数据的起始位置:数据从第 IHL×4IHL \times 4IHL×4 个字节开始。

2.3 Type of Service(服务类型,ToS)— 8 位

告诉路由器"我希望以什么方式传输这个包",是一种传输质量偏好声明
8位的布局:

位编号:  0   1   2   3   4   5   6   7
        +---+---+---+---+---+---+---+---+
        | P | P | P | D | T | R | 0 | 0 |
        +---+---+---+---+---+---+---+---+
          优先级(3位)  延迟 吞吐 可靠  保留

3位优先级(Precedence)含义:

二进制值 优先级名称 说明
111 Network Control 网络控制(最高)
110 Internetwork Control 网间控制
101 CRITIC/ECP 关键
100 Flash Override 闪越
011 Flash 闪速
010 Immediate 即时
001 Priority 优先
000 Routine 普通(默认)

3个质量标志位:

名称 0的含义 1的含义
D Delay(延迟) 普通延迟 低延迟(如语音通话)
T Throughput(吞吐量) 普通吞吐 高吞吐(如文件传输)
R Reliability(可靠性) 普通可靠性 高可靠性

注意: D、T、R 三个标志不建议同时设置超过两个,因为提升某一项往往以牺牲另一项为代价。

2.4 Total Length(总长度)— 16 位

整个数据包(头部 + 数据)的总字节数。
总长度=IP头部长度+数据长度\text{总长度} = \text{IP头部长度} + \text{数据长度}总长度=IP头部长度+数据长度
最大值=216−1=65535 字节\text{最大值} = 2^{16} - 1 = 65535 \text{ 字节}最大值=2161=65535 字节
两个重要约定:

  • 所有主机必须能接受最大 576 字节的数据包(无论是整包还是分片后的)
  • 576 = 512字节数据 + 64字节头部,是早期互联网的安全基线

实际含义: 如果你不确定目标主机能否接收大包,就别发超过576字节的包。

2.5 Identification(标识)— 16 位

发送方给这个数据包分配的唯一编号,主要用于分片重组时识别哪些碎片属于同一个原始包
标识范围=0∼65535(216 种值)\text{标识范围} = 0 \sim 65535 \quad (2^{16} \text{ 种值})标识范围=065535(216 种值)
唯一性要求: 对于同一对(源IP, 目标IP, 协议号),在数据包的生存时间内,ID 必须唯一。

2.6 Flags(标志)— 3 位

3个控制位,控制分片行为:

位编号:  0    1    2
        +----+----+----+
        |  0 | DF | MF |
        +----+----+----+
         保留  不分片  更多分片

名称 值=0 值=1
0 保留 必须为0
1 DF(Don’t Fragment) 允许分片 禁止分片
2 MF(More Fragments) 这是最后一片 后面还有分片

DF=1 的后果: 如果包太大无法通过某段网络,且 DF=1,路由器会直接丢弃该包并发送 ICMP 错误消息,而不是帮你分片。

2.7 Fragment Offset(分片偏移)— 13 位

指明"这个分片的数据"在原始完整数据包数据部分的哪个位置开始。
偏移单位=8 字节(64位)\text{偏移单位} = 8 \text{ 字节(64位)}偏移单位=8 字节(64位)
实际字节位置=Fragment Offset×8\text{实际字节位置} = \text{Fragment Offset} \times 8实际字节位置=Fragment Offset×8
最大偏移=(213−1)×8=8191×8=65528 字节\text{最大偏移} = (2^{13} - 1) \times 8 = 8191 \times 8 = 65528 \text{ 字节}最大偏移=(2131)×8=8191×8=65528 字节
为什么用8字节为单位? 因为 13 位最多表示 8191,乘以 8 后约等于 65535,恰好与 Total Length 的最大值匹配。
第一个分片的偏移永远是 0。

2.8 Time to Live(TTL,生存时间)— 8 位

防止数据包在网络中无限循环的"自毁倒计时"。
TTLmax⁡=28−1=255 秒(理论上限)TTL_{\max} = 2^8 - 1 = 255 \text{ 秒(理论上限)}TTLmax=281=255 秒(理论上限)
实际含义=最多经过的路由器跳数\text{实际含义} = \text{最多经过的路由器跳数}实际含义=最多经过的路由器跳数
工作规则:

  • 发送方设置初始 TTL(常见值:64 或 128)
  • 每经过一个路由器,TTL 至少减 1(即使处理时间不足1秒)
  • TTL 降到 0 时,路由器丢弃该包,并发送 ICMP “Time Exceeded” 消息给发送方

TTL 是上界,不是精确计时器。 它保证了数据包不会永远在网络中游荡。
traceroute 工具正是利用TTL从1开始递增来探测路径上每一跳的路由器。

2.9 Protocol(协议)— 8 位

标识 IP 数据部分使用的是哪个上层协议。

Protocol值 协议
1 ICMP
6 TCP
17 UDP

路由器和主机根据这个字段知道把数据交给哪个上层模块处理。

2.10 Header Checksum(头部校验和)— 16 位

只验证IP头部是否在传输中损坏,不验证数据部分
校验和算法(反码求和):

计算步骤:
1. 把整个IP头部视为一串16位(2字节)的字
2. 将校验和字段本身暂时设为 0
3. 对所有16位字做"反码累加"(ones' complement sum)
4. 对累加结果取反码,得到校验和
5. 填入校验和字段
验证步骤:
1. 对包含校验和的整个头部再做一次反码累加
2. 结果应为全1(0xFFFF),否则头部已损坏,立即丢弃

什么是反码求和?
反码(ones’ complement)中,正数和负数的关系是按位取反。反码求和就是普通加法,但进位要回卷(wrap around)到最低位:
例如:0xFFFF+0x0001=0x10000→回卷0x0000+1=0x0001\text{例如:} \quad 0xFFFF + 0x0001 = 0x10000 \xrightarrow{\text{回卷}} 0x0000 + 1 = 0x0001例如:0xFFFF+0x0001=0x10000回卷 0x0000+1=0x0001
为什么每跳都要重算校验和? 因为 TTL 每跳都会变化,头部内容改变了,校验和就必须重新计算。

2.11 Source / Destination Address(源/目的地址)— 各 32 位

各占 32 位(4字节),就是我们熟悉的 IPv4 地址,如 192.168.1.1
一个物理主机可以有多个 IP 地址(多宿主,multi-homing),一个 IP 地址也可以对应多个物理接口。

2.12 Options(选项)— 可变长度

Options 是可选附加字段,长度可变(0到40字节之间),必须以4字节对齐。
选项有两种格式:

格式一(单字节选项,无数据):
+--------+
|  类型  |
+--------+
格式二(多字节选项,含数据):
+--------+--------+--------...--------+
|  类型  |  长度  |      数据         |
+--------+--------+--------...--------+
注意:长度字段包含类型字节和长度字节本身

选项类型字节的内部结构(8位):

位编号:  0      1   2      3   4   5   6   7
        +------+---+---+---+---+---+---+---+
        | 复制 |  类别(2位)  |   选项编号(5位)  |
        +------+-------------+---------------+

字段 位数 含义
复制标志 1位 1=分片时复制到所有分片;0=只保留在第一片
类别 2位 0=控制;2=调试与测量
选项编号 5位 具体是哪种选项

所有已定义选项汇总

类别 编号 长度 名称 说明
0 0 1字节 End of Option List 选项列表结束标志
0 1 1字节 No Operation 空操作,用于对齐
0 2 11字节 Security 安全标记(军用)
0 3 可变 Loose Source Routing 松散源路由
0 9 可变 Strict Source Routing 严格源路由
0 7 可变 Record Route 记录路由
0 8 4字节 Stream ID 流标识符
2 4 可变 Internet Timestamp 时间戳

各选项详解

① End of Option List(类型=0)

+--------+
|00000000|  = 0x00
+--------+

标记选项列表结束,在选项不与头部边界对齐时使用。分片时可任意复制/删除。
② No Operation(类型=1)

+--------+
|00000001|  = 0x01
+--------+

填充用,让后续选项从32位边界开始。无实际含义。
③ Security(类型=130,0x82)
用于军事/政府网络,携带数据的保密级别信息:

+--------+--------+--------+--------+--------+--------+--------+--------+
|10000010|00001011|  S(安全级别,16位) | C(隔离区,16位)  | H(处理限制,16位)|
+--------+--------+--------+--------+--------+--------+--------+--------+
+--------+--------+--------+
|      TCC(传输控制码,24位) |
+--------+--------+--------+
类型=130  长度=11

安全级别(Security Level)取值:

值(16进制) 级别
0x0000 Unclassified(无密级)
0xF135 Confidential(机密)
0xD788 Secret(秘密)
0x6BC5 Top Secret(绝密)

④ Loose Source Routing,LSRR(类型=131)
发送方指定数据包"必须经过哪些节点",但节点之间可以走任意路径(松散)。

+--------+--------+--------+--------+--------+--------+
|10000011| 总长度 | 指针ptr|  IP1地址(32位)  |  IP2...  |
+--------+--------+--------+--------+--------+--------+
  类型=131

工作原理:

  • 路由表中预填若干 IP 地址(中间站点)
  • 数据包到达每个站点时,把下一个 IP 从路由列表取出填入目标地址
  • 同时把自己的 IP 写回列表(记录已走路径)
  • 指针(ptr)指向下一个待处理的 IP 地址位置
    ⑤ Strict Source Routing,SSRR(类型=137)
    与 LSRR 相同,但要求严格按照指定路径,数据包必须直接(不经其他中间节点)到达列表中的下一跳。
    ⑥ Record Route(类型=7)
    记录数据包经过的所有路由器 IP,相当于给数据包一张"路途日志"。
+--------+--------+--------+--------+--------+
|00000111| 总长度 | 指针ptr|  (预留空间,初始为0)  |
+--------+--------+--------+--------+--------+
  类型=7
  • 发送方预先分配足够大的记录空间(全0填充)
  • 每经过一个路由器,该路由器把自己的 IP 写入当前指针处,指针加4
  • 若空间已满,继续转发但不再记录(可能发送 ICMP 参数问题报文)
  • 不随分片复制,只保留在第一个分片中
    ⑦ Internet Timestamp(类型=68,0x44)
    记录数据包经过各节点时的时间戳,用于网络延迟诊断。
+--------+--------+--------+--------+
|01000100| 总长度 | 指针ptr|oflw|flg|
+--------+--------+--------+--------+
|         IP地址(可选,32位)        |
+--------+--------+--------+--------+
|         时间戳(32位,毫秒)        |
+--------+--------+--------+--------+
  类型=68  最大总长度40字节
  • oflw(4位):因空间不足未能记录的节点数量
  • flg(4位)的三种模式:

flg值 含义
0 只记录时间戳(不记IP)
1 记录IP + 时间戳
3 仅在IP匹配自己时记录时间戳

时间戳是以世界协调时(UT)午夜0时为基准的毫秒数
时间戳=当前毫秒数(自UT午夜起)\text{时间戳} = \text{当前毫秒数(自UT午夜起)}时间戳=当前毫秒数(自UT午夜起)
若无法提供标准时间,可写入任意值但需将最高位置1作为标记。

2.13 Padding(填充)— 可变长度

用全0填充,确保 IP 头部总长度是 32位(4字节)的整数倍

三、分片与重组完整流程

3.1 分片流程伪代码解析

RFC 791 给出的官方分片算法:

变量说明:
  TL   = 当前数据包总长度
  MTU  = 下一段网络的最大传输单元
  IHL  = 当前头部长度(单位:32位字)
  DF   = 不分片标志
  MF   = 更多分片标志
  FO   = 分片偏移
  NFB  = 本次分片的8字节块数量
算法:
如果 TL <= MTU:
    直接转发,不分片
否则:
    如果 DF == 1:
        丢弃数据包,发ICMP错误
    否则:
        [生成第一个分片]
        第1步:复制原始IP头部
        第2步:保存旧值 OIHL=IHL, OTL=TL, OFO=FO, OMF=MF
        第3步:NFB = (MTU - IHL*4) / 8
               (计算第一片能放多少个8字节块)
        第4步:取前 NFB*8 字节的数据
        第5步:修正头部:
               MF = 1(还有后续分片)
               TL = IHL*4 + NFB*8
               重新计算校验和
        第6步:提交第一个分片
        [生成第二个分片(递归处理,可能还需再分)]
        第7步:选择性复制头部(不是所有Options都复制)
        第8步:追加剩余数据
        第9步:修正头部:
               IHL 根据复制的Options重新计算
               TL = OTL - NFB*8 - (OIHL-IHL)*4
               FO = OFO + NFB(偏移增加NFB个8字节块)
               MF = OMF(继承原来的MF标志)
               重新计算校验和
        第10步:将第二个分片重新送入分片判断(递归)

NFB 计算公式:
NFB=⌊MTU−IHL×48⌋NFB = \left\lfloor \frac{MTU - IHL \times 4}{8} \right\rfloorNFB=8MTUIHL×4
第二片偏移更新:
FO新=FO旧+NFBFO_{新} = FO_{旧} + NFBFO=FO+NFB

3.2 重组流程伪代码解析

变量说明:
  BUFID = 缓冲区标识符(源IP + 目标IP + 协议 + ID 四者拼接)
  RCVBT = 已接收片段的位图(每一位对应一个8字节块)
  TDL   = 总数据长度
  TIMER = 超时计时器(初始推荐值:15秒)
算法:
1. 计算 BUFID = 源IP | 目标IP | 协议 | 标识ID
2. 如果 FO==0 且 MF==0(这是一个完整的未分片包):
      如果已有该BUFID的缓冲区:清除它
      直接转发,结束
3. 如果没有该BUFID的缓冲区:分配重组资源,TIMER=15秒, TDL=0
4. 将本片数据放入缓冲区的正确位置:
      位置 = 从第 FO*8 字节 到 第 (TL-IHL*4)+FO*8 字节
5. 在位图 RCVBT 中标记已收到的块
6. 如果 MF==0(这是最后一片):
      TDL = TL - IHL*4 + FO*8(计算原始数据总长度)
7. 如果 FO==0(这是第一片):
      保存头部到头部缓冲区
8. 检查是否集齐所有分片:
      如果 TDL != 0
      且 RCVBT 中从第0位到第(TDL+7)/8位全部为1:
          重组完成!提交完整数据包,释放资源,结束
9. 否则:TIMER = max(TIMER, TTL),等待更多分片
10. 计时器超时:丢弃所有该BUFID的重组资源,结束

缓冲区标识符:
BUFID={源IP}∥{目标IP}∥{协议号}∥{标识ID}BUFID = \{源IP\} \| \{目标IP\} \| \{协议号\} \| \{标识ID\}BUFID={IP}{目标IP}{协议号}{标识ID}
重组超时的推荐初始值:15秒
TTLmax⁡≈255 秒≈4.25 分钟TTL_{\max} \approx 255 \text{ 秒} \approx 4.25 \text{ 分钟}TTLmax255 4.25 分钟

四、RFC 791 官方示例详解

示例1:最小数据包(1字节数据)

字段值:
  Version = 4(IPv4)
  IHL     = 5(头部20字节,无Options)
  ToS     = 0(普通服务)
  总长度  = 21(20字节头部 + 1字节数据)
  ID      = 111
  Flags   = 0(不分片,无更多分片)
  FO      = 0
  TTL     = 123
  Protocol= 1(ICMP)
  数据    = 1字节

总长度=20+1=21 字节\text{总长度} = 20 + 1 = 21 \text{ 字节}总长度=20+1=21 字节

示例2:分片示例(452字节数据,MTU=280)

原始数据包:
总长度=20+452=472 字节\text{总长度} = 20 + 452 = 472 \text{ 字节}总长度=20+452=472 字节
MTU=280字节,所以每片最多数据量:
NFB=⌊280−208⌋=⌊2608⌋=32NFB = \left\lfloor \frac{280 - 20}{8} \right\rfloor = \left\lfloor \frac{260}{8} \right\rfloor = 32NFB=828020=8260=32
第一片数据量=32×8=256 字节\text{第一片数据量} = 32 \times 8 = 256 \text{ 字节}第一片数据量=32×8=256 字节
分片1(第一片):

总长度 = 20 + 256 = 276 字节
ID     = 111(与原包相同)
Flags  = MF=1(后面还有)
偏移   = 0(从第0字节开始)
TTL    = 119(经过路由器后减少了4)

分片2(最后一片):

剩余数据 = 452 - 256 = 196 字节
总长度  = 20 + 196 = 216 字节
ID      = 111(同一原始包)
Flags   = MF=0(这是最后一片)
偏移    = 32(第256字节开始,32 = 256/8)

分片2偏移=2568=32\text{分片2偏移} = \frac{256}{8} = 32分片2偏移=8256=32
重组时验证:
分片1覆盖=[0,255] 字节\text{分片1覆盖} = [0, 255] \text{ 字节}分片1覆盖=[0,255] 字节
分片2覆盖=[32×8,32×8+196)=[256,452) 字节\text{分片2覆盖} = [32 \times 8, 32 \times 8 + 196) = [256, 452) \text{ 字节}分片2覆盖=[32×8,32×8+196)=[256,452) 字节
恰好无缝衔接,重组成功\text{恰好无缝衔接,重组成功}恰好无缝衔接,重组成功

五、IP 接口规范(SEND/RECV)

RFC 791 还规定了应用程序调用 IP 模块的接口规范:
发送接口 SEND:

SEND(src, dst, prot, TOS, TTL, BufPTR, len, Id, DF, opt) => result
参数说明:
  src    = 源IP地址
  dst    = 目标IP地址
  prot   = 上层协议号(6=TCP, 17=UDP等)
  TOS    = 服务类型
  TTL    = 生存时间初始值
  BufPTR = 数据缓冲区指针
  len    = 数据长度
  Id     = 数据包标识符
  DF     = 是否设置不分片标志
  opt    = 选项数据
返回值:
  OK    = 发送成功
  Error = 参数错误或网络错误

接收接口 RECV:

RECV(BufPTR, prot) => result, src, dst, TOS, len, opt
参数说明:
  BufPTR = 接收缓冲区指针
  prot   = 期望接收的协议类型
返回值:
  result = OK 或 Error
  src    = 数据包来源IP
  dst    = 目标IP
  TOS    = 服务类型
  len    = 数据长度
  opt    = 选项数据

健壮性原则(鲁棒性原则):

发送时保守(严格按规范构造),接收时宽容(能理解就接受,不苛求技术细节完美)

六、数据传输字节序

IP 协议采用大端序(Big-Endian),即网络字节序

  • 多字节数值的最高有效字节(MSB)先发送
  • 字节内部最高有效位(bit 0)在左
示例:值 170(十进制)= 0b10101010
位编号:  0  1  2  3  4  5  6  7
        +--+--+--+--+--+--+--+--+
        | 1| 0| 1| 0| 1| 0| 1| 0|
        +--+--+--+--+--+--+--+--+
          最高位在左(bit 0 = MSB)
多字节示例:值 0x1234 在网络中传输顺序:
  先发 0x12(高字节),再发 0x34(低字节)

在C++中处理网络字节序需要使用 htons()/ntohs()(16位)和 htonl()/ntohl()(32位)函数。

七、完整 C++ 实现

以下代码实现了 IP 头部的构建、校验和计算、分片、以及 ToS/Options 的解析展示:

#include <iostream>
#include <vector>
#include <cstdint>
#include <cstring>
#include <iomanip>
#include <cassert>
#include <sstream>
#include <algorithm>
// ============================================================
// IP 头部结构体(对应 RFC 791 Figure 4)
// 注意:实际网络传输需要考虑字节序(大端序)
// ============================================================
struct IPHeader {
    uint8_t  version_ihl;      // 高4位=Version(4), 低4位=IHL
    uint8_t  tos;              // Type of Service(服务类型)
    uint16_t total_length;     // 总长度(头部+数据,字节)
    uint16_t identification;   // 标识符(用于分片重组)
    uint16_t flags_offset;     // 高3位=Flags,低13位=Fragment Offset
    uint8_t  ttl;              // Time to Live(生存时间)
    uint8_t  protocol;         // 上层协议号(1=ICMP, 6=TCP, 17=UDP)
    uint16_t checksum;         // 头部校验和
    uint32_t src_addr;         // 源IP地址(32位)
    uint32_t dst_addr;         // 目标IP地址(32位)
    // Options 和 Padding 不在固定结构中,单独处理
};
// ============================================================
// 辅助函数:把32位IP整数转为点分十进制字符串
// 例如:0xC0A80101 -> "192.168.1.1"
// ============================================================
std::string ip_to_str(uint32_t ip) {
    std::ostringstream oss;
    oss << ((ip >> 24) & 0xFF) << "."
        << ((ip >> 16) & 0xFF) << "."
        << ((ip >>  8) & 0xFF) << "."
        << ( ip        & 0xFF);
    return oss.str();
}
// ============================================================
// 辅助函数:把点分十进制字符串转为32位IP整数
// 例如:"192.168.1.1" -> 0xC0A80101
// ============================================================
uint32_t str_to_ip(const std::string& s) {
    uint32_t result = 0;
    int octet = 0, shift = 24;
    for (char c : s) {
        if (c == '.') {
            result |= (uint32_t)(octet & 0xFF) << shift;
            shift -= 8;
            octet = 0;
        } else {
            octet = octet * 10 + (c - '0');
        }
    }
    result |= (uint32_t)(octet & 0xFF); // 最后一组
    return result;
}
// ============================================================
// 核心函数:计算 IP 头部校验和
// 算法:对头部所有16位字做反码累加,再对结果取反码
//
// 反码求和(ones' complement sum):
//   普通加法,但进位要回卷到最低位
//   例如:0xFFFF + 1 = 0x10000 -> 回卷 -> 0x0001
// ============================================================
uint16_t compute_checksum(const uint8_t* data, int len) {
    uint32_t sum = 0;
    // 每次取2字节(16位)累加
    for (int i = 0; i < len - 1; i += 2) {
        // 大端:高字节在前,低字节在后
        uint16_t word = ((uint16_t)data[i] << 8) | data[i + 1];
        sum += word;
        // 处理进位:超出16位的部分回卷到最低位
        if (sum > 0xFFFF) {
            sum = (sum & 0xFFFF) + 1; // 回卷(wrap around)
        }
    }
    // 如果头部长度是奇数字节(理论上IP头部不会,但处理边界情况)
    if (len % 2 != 0) {
        sum += (uint16_t)data[len - 1] << 8;
        if (sum > 0xFFFF) sum = (sum & 0xFFFF) + 1;
    }
    // 对最终结果取反码(按位取反)
    return (uint16_t)(~sum & 0xFFFF);
}
// ============================================================
// 验证校验和:对含校验和的完整头部再次计算,结果应为 0xFFFF
// ============================================================
bool verify_checksum(const uint8_t* data, int len) {
    uint32_t sum = 0;
    for (int i = 0; i < len - 1; i += 2) {
        uint16_t word = ((uint16_t)data[i] << 8) | data[i + 1];
        sum += word;
        if (sum > 0xFFFF) sum = (sum & 0xFFFF) + 1;
    }
    return (sum == 0xFFFF);
}
// ============================================================
// 分片信息结构体(模拟一个分片的关键字段)
// ============================================================
struct Fragment {
    uint16_t identification;  // 与原始包相同的ID
    uint16_t fragment_offset; // 偏移(单位:8字节块)
    bool     more_fragments;  // true=后面还有分片,false=最后一片
    int      data_start;      // 本片数据在原始数据中的起始字节
    int      data_length;     // 本片数据长度(字节)
};
// ============================================================
// 分片函数(RFC 791 分片算法的 C++ 实现)
// 参数:
//   total_data   - 原始数据总字节数
//   mtu          - 下一段网络的 MTU(含IP头)
//   header_size  - IP头部大小(通常为20字节)
//   id           - 数据包标识符
// 返回:所有分片列表
// ============================================================
std::vector<Fragment> fragment_datagram(int total_data,
                                         int mtu,
                                         int header_size,
                                         uint16_t id) {
    std::vector<Fragment> result;
    // 每片最多能装的数据量,必须是8的倍数(RFC791要求)
    // NFB = (MTU - IHL*4) / 8
    int max_data = ((mtu - header_size) / 8) * 8;
    int offset = 0; // 当前处理到原始数据的哪个字节
    while (offset < total_data) {
        Fragment frag;
        frag.identification   = id;
        frag.data_start       = offset;
        // 本片实际能装的数据量
        int remaining     = total_data - offset;
        frag.data_length  = std::min(remaining, max_data);
        // 分片偏移 = 当前起始字节 / 8(换算成8字节块数)
        frag.fragment_offset  = (uint16_t)(offset / 8);
        // 是否还有后续分片
        frag.more_fragments   = (offset + frag.data_length < total_data);
        result.push_back(frag);
        offset += frag.data_length;
    }
    return result;
}
// ============================================================
// 打印分片信息
// ============================================================
void print_fragments(const std::vector<Fragment>& frags,
                     int mtu, int header_size) {
    std::cout << "分片结果(MTU=" << mtu
              << " 字节,头部=" << header_size << " 字节)\n";
    std::cout << std::string(70, '-') << "\n";
    std::cout << std::setw(5)  << "编号"
              << std::setw(8)  << "ID"
              << std::setw(10) << "偏移值"
              << std::setw(10) << "实际字节"
              << std::setw(8)  << "MF标志"
              << std::setw(10) << "数据长度"
              << "\n";
    std::cout << std::string(70, '-') << "\n";
    for (int i = 0; i < (int)frags.size(); ++i) {
        const auto& f = frags[i];
        std::cout << std::setw(5)  << (i + 1)
                  << std::setw(8)  << f.identification
                  << std::setw(10) << f.fragment_offset
                  << std::setw(10) << (f.fragment_offset * 8)
                  << std::setw(8)  << (f.more_fragments ? "1(有)" : "0(无)")
                  << std::setw(10) << f.data_length
                  << "\n";
    }
    std::cout << std::string(70, '-') << "\n";
    std::cout << "共 " << frags.size() << " 个分片\n\n";
}
// ============================================================
// 解析并打印 ToS 字段的各个子字段
// ============================================================
void parse_tos(uint8_t tos) {
    uint8_t precedence  = (tos >> 5) & 0x07; // 高3位
    bool    delay       = (tos >> 4) & 0x01;  // 第4位
    bool    throughput  = (tos >> 3) & 0x01;  // 第5位
    bool    reliability = (tos >> 2) & 0x01;  // 第6位
    const char* prec_names[] = {
        "000-普通(Routine)",
        "001-优先(Priority)",
        "010-即时(Immediate)",
        "011-闪速(Flash)",
        "100-闪越(Flash Override)",
        "101-关键(CRITIC/ECP)",
        "110-网间控制(Internetwork Control)",
        "111-网络控制(Network Control)"
    };
    std::cout << "ToS 字段解析(值=0x"
              << std::hex << (int)tos << std::dec << "):\n";
    std::cout << "  优先级:    " << prec_names[precedence] << "\n";
    std::cout << "  延迟:      " << (delay       ? "低延迟" : "普通延迟") << "\n";
    std::cout << "  吞吐量:    " << (throughput  ? "高吞吐" : "普通吞吐") << "\n";
    std::cout << "  可靠性:    " << (reliability ? "高可靠" : "普通可靠") << "\n\n";
}
// ============================================================
// 构建一个完整的 IP 头部(填入各字段,计算校验和)
// 返回序列化后的字节数组(20字节,无Options)
// ============================================================
std::vector<uint8_t> build_ip_header(
    uint8_t  tos,
    uint16_t total_length,
    uint16_t id,
    bool     df,          // Don't Fragment
    bool     mf,          // More Fragments
    uint16_t frag_offset, // 单位:8字节块
    uint8_t  ttl,
    uint8_t  protocol,
    uint32_t src,
    uint32_t dst)
{
    std::vector<uint8_t> buf(20, 0); // 20字节,全0初始化
    // Version=4, IHL=5 → 组合成一个字节:0x45
    buf[0] = (4 << 4) | 5;
    // Type of Service
    buf[1] = tos;
    // Total Length(大端序:高字节在前)
    buf[2] = (total_length >> 8) & 0xFF;
    buf[3] =  total_length       & 0xFF;
    // Identification
    buf[4] = (id >> 8) & 0xFF;
    buf[5] =  id       & 0xFF;
    // Flags(3位) + Fragment Offset(13位) 合并为16位
    // Flags: bit1=DF, bit2=MF
    uint16_t flags_off = frag_offset & 0x1FFF; // 低13位是偏移
    if (df) flags_off |= (1 << 14);             // bit14 = DF
    if (mf) flags_off |= (1 << 13);             // bit13 = MF
    buf[6] = (flags_off >> 8) & 0xFF;
    buf[7] =  flags_off       & 0xFF;
    // TTL
    buf[8] = ttl;
    // Protocol
    buf[9] = protocol;
    // Checksum(先置0,后计算)
    buf[10] = 0;
    buf[11] = 0;
    // Source Address(大端序)
    buf[12] = (src >> 24) & 0xFF;
    buf[13] = (src >> 16) & 0xFF;
    buf[14] = (src >>  8) & 0xFF;
    buf[15] =  src        & 0xFF;
    // Destination Address
    buf[16] = (dst >> 24) & 0xFF;
    buf[17] = (dst >> 16) & 0xFF;
    buf[18] = (dst >>  8) & 0xFF;
    buf[19] =  dst        & 0xFF;
    // 计算并填入校验和
    uint16_t csum = compute_checksum(buf.data(), 20);
    buf[10] = (csum >> 8) & 0xFF;
    buf[11] =  csum       & 0xFF;
    return buf;
}
// ============================================================
// 打印 IP 头部各字段(人类可读格式)
// ============================================================
void print_ip_header(const std::vector<uint8_t>& buf) {
    if (buf.size() < 20) { std::cout << "头部太短!\n"; return; }
    uint8_t  version  = (buf[0] >> 4) & 0x0F;
    uint8_t  ihl      = buf[0] & 0x0F;
    uint8_t  tos      = buf[1];
    uint16_t tot_len  = ((uint16_t)buf[2] << 8) | buf[3];
    uint16_t id       = ((uint16_t)buf[4] << 8) | buf[5];
    uint16_t flags_fo = ((uint16_t)buf[6] << 8) | buf[7];
    bool     df       = (flags_fo >> 14) & 1;
    bool     mf       = (flags_fo >> 13) & 1;
    uint16_t foffset  = flags_fo & 0x1FFF;
    uint8_t  ttl      = buf[8];
    uint8_t  proto    = buf[9];
    uint16_t csum     = ((uint16_t)buf[10] << 8) | buf[11];
    uint32_t src      = ((uint32_t)buf[12] << 24) | ((uint32_t)buf[13] << 16)
                      | ((uint32_t)buf[14] <<  8) |  (uint32_t)buf[15];
    uint32_t dst      = ((uint32_t)buf[16] << 24) | ((uint32_t)buf[17] << 16)
                      | ((uint32_t)buf[18] <<  8) |  (uint32_t)buf[19];
    std::cout << "=== IP 头部内容 ===\n";
    std::cout << "  Version:         " << (int)version << "\n";
    std::cout << "  IHL:             " << (int)ihl
              << " (" << (ihl * 4) << " 字节)\n";
    std::cout << "  TOS:             0x"
              << std::hex << std::setw(2) << std::setfill('0')
              << (int)tos << std::dec << std::setfill(' ') << "\n";
    std::cout << "  Total Length:    " << tot_len << " 字节\n";
    std::cout << "  Identification:  " << id << "\n";
    std::cout << "  DF:              " << (df ? "1(禁止分片)" : "0(允许分片)") << "\n";
    std::cout << "  MF:              " << (mf ? "1(后续有分片)" : "0(最后一片)") << "\n";
    std::cout << "  Fragment Offset: " << foffset
              << " (" << (foffset * 8) << " 字节)\n";
    std::cout << "  TTL:             " << (int)ttl << "\n";
    std::cout << "  Protocol:        " << (int)proto;
    if (proto == 1)  std::cout << " (ICMP)";
    if (proto == 6)  std::cout << " (TCP)";
    if (proto == 17) std::cout << " (UDP)";
    std::cout << "\n";
    std::cout << "  Checksum:        0x"
              << std::hex << std::setw(4) << std::setfill('0')
              << csum << std::dec << std::setfill(' ') << "\n";
    std::cout << "  Source:          " << ip_to_str(src) << "\n";
    std::cout << "  Destination:     " << ip_to_str(dst) << "\n";
    // 验证校验和
    bool valid = verify_checksum(buf.data(), 20);
    std::cout << "  校验和验证:      " << (valid ? "通过" : "失败!") << "\n";
    std::cout << "\n";
}
// ============================================================
// 主函数:演示各功能
// ============================================================
int main() {
    std::cout << std::string(60, '=') << "\n";
    std::cout << "RFC 791 IP 协议头部演示\n";
    std::cout << std::string(60, '=') << "\n\n";
    // -------------------------------------------------------
    // 演示1:构建并解析 RFC 791 示例1(最小数据包)
    // Version=4, IHL=5, 总长=21, ID=111, TTL=123, Protocol=1
    // -------------------------------------------------------
    std::cout << "--- 演示1:RFC 791 示例1(最小数据包,21字节)---\n\n";
    {
        uint32_t src = str_to_ip("10.0.0.1");
        uint32_t dst = str_to_ip("10.0.0.2");
        auto hdr = build_ip_header(
            0,      // tos = 0(普通服务)
            21,     // total_length = 20头部 + 1字节数据
            111,    // id = 111
            false,  // df = 不禁止分片
            false,  // mf = 没有更多分片(完整包)
            0,      // fragment_offset = 0
            123,    // ttl = 123
            1,      // protocol = 1(ICMP)
            src, dst
        );
        print_ip_header(hdr);
    }
    // -------------------------------------------------------
    // 演示2:ToS 字段解析
    // -------------------------------------------------------
    std::cout << "--- 演示2:ToS 字段解析 ---\n\n";
    // 优先级5(101),低延迟(D=1),高吞吐(T=1),普通可靠(R=0)
    // ToS = 101 1 1 0 0 0 = 0xB8
    parse_tos(0xB8);
    // -------------------------------------------------------
    // 演示3:RFC 791 示例2 的分片过程(452字节数据,MTU=280)
    // -------------------------------------------------------
    std::cout << "--- 演示3:RFC 791 示例2 分片(452字节,MTU=280)---\n\n";
    {
        auto frags = fragment_datagram(452, 280, 20, 111);
        print_fragments(frags, 280, 20);
        // 验证:分片1应是256字节数据,偏移0,MF=1
        assert(frags[0].data_length    == 256);
        assert(frags[0].fragment_offset == 0);
        assert(frags[0].more_fragments  == true);
        // 分片2应是196字节数据,偏移32(=256/8),MF=0
        assert(frags[1].data_length    == 196);
        assert(frags[1].fragment_offset == 32);
        assert(frags[1].more_fragments  == false);
        std::cout << "验证通过:分片数量=" << frags.size()
                  << ", 分片1偏移=" << frags[0].fragment_offset
                  << ", 分片2偏移=" << frags[1].fragment_offset << "\n\n";
    }
    // -------------------------------------------------------
    // 演示4:校验和计算与验证
    // -------------------------------------------------------
    std::cout << "--- 演示4:头部校验和验证 ---\n\n";
    {
        uint32_t src = str_to_ip("192.168.1.100");
        uint32_t dst = str_to_ip("8.8.8.8");
        // 正常包
        auto hdr = build_ip_header(0, 576, 42, false, false, 0,
                                   64, 6, src, dst);
        std::cout << "正常构建的头部(TCP, 576字节, TTL=64):\n";
        print_ip_header(hdr);
        // 模拟 TTL 被路由器减1后重新计算校验和
        hdr[8] -= 1; // TTL 减1
        hdr[10] = hdr[11] = 0; // 清零校验和字段
        uint16_t new_csum = compute_checksum(hdr.data(), 20);
        hdr[10] = (new_csum >> 8) & 0xFF;
        hdr[11] =  new_csum       & 0xFF;
        std::cout << "路由器将 TTL 减1后重新计算校验和:\n";
        print_ip_header(hdr);
        // 模拟数据损坏
        hdr[2] ^= 0xFF; // 故意破坏 Total Length 字段
        bool damaged = verify_checksum(hdr.data(), 20);
        std::cout << "故意损坏 Total Length 后,校验和验证:"
                  << (damaged ? "通过(不应该)" : "失败(正确检测到损坏)")
                  << "\n\n";
    }
    // -------------------------------------------------------
    // 演示5:大包多片分片(3000字节,MTU=1500)
    // -------------------------------------------------------
    std::cout << "--- 演示5:3000字节数据经过MTU=1500的网络 ---\n\n";
    {
        auto frags = fragment_datagram(3000, 1500, 20, 9999);
        print_fragments(frags, 1500, 20);
    }
    return 0;
}

八、知识结构图

IP头部字段

基本信息

分片控制

传输控制

地址

Options选项

Version 4位
协议版本号

IHL 4位
头部长度x4=字节数

TOS 8位
优先级+延迟+吞吐+可靠

Identification 16位
分片共享同一ID

Flags 3位
DF=禁止分片 MF=还有分片

Fragment Offset 13位
偏移=值x8字节

Total Length 16位
最大65535字节

TTL 8位
每跳减1 为0则丢弃

Protocol 8位
1=ICMP 6=TCP 17=UDP

Header Checksum 16位
反码累加再取反

Source Address 32位

Destination Address 32位

End of List 类型0

No Operation 类型1

Security 类型130
军事安全标记

LSRR 类型131
松散源路由

SSRR 类型137
严格源路由

Record Route 类型7
记录经过的IP

Timestamp 类型68
记录各跳时间戳

九、分片流程图

收到数据包

TL <= MTU?

直接转发,不分片

DF == 1?

丢弃,发ICMP错误

计算 NFB = MTU-IHL*4 / 8
生成第一片:前NFB*8字节
设置MF=1,重算校验和

提交第一片

生成第二片:剩余数据
FO += NFB
MF继承原值

十、重组流程图

是,完整包

否,是分片

收到分片

计算 BUFID=
源IP+目标IP+协议+ID

FO==0 且 MF==0?

清除同BUFID缓存
直接转发

已有该BUFID缓冲?

分配缓冲区
TIMER=15秒

将分片数据写入缓冲区
更新位图RCVBT

MF==0?

计算总数据长度TDL

所有分片到齐?
RCVBT全为1?

重组完成,转发完整包

更新计时器
继续等待

超时?

丢弃所有缓存

十一、关键数值速查


字段 位数 最小值 最大值 备注
Version 4 IPv4固定为4
IHL 4 5 15 对应20~60字节
TOS 8 0 255 8个子字段
Total Length 16 20 65535 字节
Identification 16 0 65535
Fragment Offset 13 0 8191 乘8得字节数
TTL 8 0 255 0则丢弃
Protocol 8 1/6/17最常见
Checksum 16 反码求和
Src/Dst Address 32 4字节IP地址

RFC 793:TCP 传输控制协议详解(第1、2章)

本文基于1981年9月发布的 RFC 793(TCP规范),从零开始用中文讲解 TCP 的设计哲学、核心机制与工作原理。

一、TCP 是什么?为什么需要它?

一句话理解

IP 协议负责把数据包"尽力"送到目的地,但它不保证:

  • 数据一定到达
  • 数据顺序正确
  • 数据没有重复
  • 数据没有损坏
    TCP 就是建立在 IP 之上的"可靠快递员",它在不可靠的网络上提供可靠的、有序的、双向的字节流传输。

TCP 在协议栈中的位置

+---------------------+
|   应用层(高层协议) |  Telnet、FTP、HTTP等
+---------------------+
|        TCP          |  <- 我们要讲的这层
+---------------------+
|   Internet Protocol |  IP协议(尽力而为)
+---------------------+
|   通信网络(物理层) |  以太网、Wi-Fi等
+---------------------+

数据向下流动时被层层包裹,向上时层层剥开。
TCP 不直接与网络硬件打交道,它调用 IP 模块,IP 再调用设备驱动。

二、TCP 的六大核心功能

2.1 基本数据传输(Basic Data Transfer)

TCP 把应用程序的数据流切成一段一段的"段(Segment)"来传输。

应用程序视角(连续字节流):
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
TCP 打包后变成多个段:
[段1: AAAAAAA][段2: AAAAAAA][段3: AAAAAAA][段4: AAAA...]

PUSH 功能: 正常情况下 TCP 会攒够数据再发,但有时应用程序需要立刻发送(比如远程命令行每按一个键都要即时响应)。设置 PUSH 标志后,TCP 必须立刻把缓冲区里的所有数据发出去,接收方也必须立刻把数据交给应用程序,不能再等待。

2.2 可靠性(Reliability)

TCP 通过三个机制保证可靠传输:
机制1:序列号(Sequence Number)
每个字节都被编上号码,就像书的页码:

发送的字节流:
字节: H  e  l  l  o  W  o  r  l  d
序号: 1  2  3  4  5  6  7  8  9  10
如果收到的顺序是:序号5,3,1,4,2 → TCP会重新排序为 1,2,3,4,5
如果序号3重复到来两次 → TCP丢弃重复的那个

机制2:确认应答(Acknowledgment,ACK)
接收方每收到数据,就告诉发送方"我收到了,下一个期望收到序号X的字节":

发送方                          接收方
  |---发送 seq=1~100 的数据--->   |
  |<---ACK=101(期望下一个是101)--|
  |---发送 seq=101~200 的数据--->  |
  |<---ACK=201---               |

机制3:超时重传(Timeout Retransmission)
发送方每发出一段数据,就启动一个计时器:

  • 收到 ACK → 删除重传队列中对应的数据,停止计时
  • 超时未收到 ACK → 重新发送该段数据
发送方                    网络(数据丢失)         接收方
  |---发送段1,启动计时器--->  X(数据丢了)         |
  |                                               |
  |(计时器超时!)                                |
  |---重新发送段1----------->                      |
  |                                  <---ACK=收到--|

注意: TCP 的 ACK 只保证"接收方 TCP 模块"收到了数据,不保证应用程序已经读取了。

2.3 流量控制(Flow Control)

防止发送方发得太快,把接收方"淹没"。
滑动窗口(Window)机制:
接收方在每个 ACK 里附带一个"窗口大小",告诉发送方"你还可以再发多少字节":
窗口=接收方当前可用缓冲区大小(字节数)\text{窗口} = \text{接收方当前可用缓冲区大小(字节数)}窗口=接收方当前可用缓冲区大小(字节数)

接收方缓冲区(假设总共1000字节):
情形1:缓冲区还有500字节空闲
  ACK中:window = 500
  → 发送方最多再发500字节
情形2:缓冲区全满
  ACK中:window = 0
  → 发送方必须停下来等待
发送方能发的数据范围:
[已确认] [已发出但未确认,大小不超过窗口] [待发送]
   ^                   ^                      ^
   |                   |                      |
 已收到ACK           占用窗口           还没发出

这样接收方通过调整窗口大小,就能控制发送方的速度。

2.4 多路复用(Multiplexing)

一台电脑上同时运行着浏览器、邮件客户端、游戏等多个程序,它们都需要用网络。TCP 通过**端口号(Port)**来区分不同的通信流。
Socket(套接字) = IP地址 + 端口号
Socket=IP地址(32位)+端口号(16位)\text{Socket} = \text{IP地址(32位)} + \text{端口号(16位)}Socket=IP地址(32位)+端口号(16位)
一个TCP连接=本地Socket+远端Socket(唯一标识一条连接)\text{一个TCP连接} = \text{本地Socket} + \text{远端Socket(唯一标识一条连接)}一个TCP连接=本地Socket+远端Socket(唯一标识一条连接)

一台电脑同时进行的多个连接:
本地                               远端
192.168.1.1:12345  <—TCP连接1—>  93.184.216.34:80   (浏览器访问网站)
192.168.1.1:12346  <—TCP连接2—>  74.125.24.27:443   (浏览器访问Google)
192.168.1.1:12347  <—TCP连接3—>  52.0.14.116:25     (邮件发送)
每条连接由四元组唯一标识:
(本地IP, 本地端口, 远端IP, 远端端口)

知名端口(Well-known Ports): 常用服务绑定固定端口,方便客户端连接:

端口 服务
21 FTP(文件传输)
23 Telnet(远程登录)
25 SMTP(邮件发送)
80 HTTP(网页)
443 HTTPS(加密网页)

2.5 连接管理(Connections)

TCP 是面向连接的协议,通信前必须先建立连接,通信完成后要关闭连接。
连接的所有状态信息存储在一个叫做 TCB(Transmission Control Block,传输控制块) 的数据结构中:

TCB 包含的信息:
+---------------------+
| 本地Socket          | 我是谁
| 远端Socket          | 对方是谁
| 发送序列号状态       | 我发到哪了
| 接收序列号状态       | 我收到哪了
| 发送窗口大小        | 对方允许我发多少
| 接收窗口大小        | 我告诉对方能发多少
| 重传队列            | 等待确认的数据
| 计时器状态          | 超时控制
+---------------------+

建立连接:三次握手(Three-way Handshake)
使用 SYN(同步序列号)控制标志:

客户端(主动发起)                     服务器(被动等待)
     |                                      |
     |---SYN(seq=x)------------------->   |  第1次:我想连接
     |                                      |
     |<--SYN+ACK(seq=y, ack=x+1)------   |  第2次:好的,我也想连你
     |                                      |
     |---ACK(ack=y+1)----------------->   |  第3次:收到,连接建立!
     |                                      |
     |         连接建立,开始传数据          |

为什么需要三次? 因为需要双向确认序列号,两次不够(服务器不知道自己的SYN有没有被客户端收到)。
关闭连接:使用 FIN 标志

发起方                              另一方
  |---FIN("我发完了")----------->   |
  |<--ACK("好的,收到")----------   |
  |<--FIN("我也发完了")----------   |
  |---ACK("好的")--------------->   |
  |                                   |
  |        连接彻底关闭               |

主动 OPEN 与被动 OPEN:

  • 主动 OPEN:主动发起连接请求(客户端行为)
  • 被动 OPEN:等待别人来连接(服务器行为),可以指定接受哪个客户端,也可以接受任意客户端(外部Socket填全0表示"任意")

2.6 优先级与安全(Precedence and Security)

TCP 利用 IP 头部的 ToS 字段和安全选项,为每条连接设定优先级和安全级别。
不是所有 TCP 实现都支持这个功能,对于普通民用网络,通常使用默认值。

三、TCP 的工作环境

互联网系统的组成元素

互联网系统组成:
[主机A]                              [主机B]
  |                                     |
[本地网络1]---[网关/路由器]---[本地网络2]
  (如以太网)      (转发数据)    (如ARPANET)

组件 角色
主机(Host) 生产和消费数据的计算机
进程(Process) 主机上运行的程序(TCP通信的真正主体)
端口(Port) 进程在TCP中的"门牌号"
网关(Gateway) 连接不同网络的中转设备(即路由器)
数据包(Packet) 网络中传输的基本数据单元

一切通信本质上都是进程间通信(Inter-Process Communication)。

数据在系统中的流动路径

从发送进程到接收进程,数据经历了如下旅程:

调用SEND

打包成TCP段

封装成IP数据包

转发

查路由表
重新封装

到达

剥掉IP头
重组分片

排序去重
放入缓冲区

发送进程
应用程序

发送方TCP模块

发送方IP模块

本地网络1

网关/路由器

本地网络2

接收方IP模块

接收方TCP模块

接收进程
应用程序

网关处理数据的三个步骤:

1. 收到数据:  [本地网络1头部 | IP数据包 | TCP段 | 应用数据]
              ↓ 剥掉本地网络1的头部
2. 查路由:    [IP数据包 | TCP段 | 应用数据]
              ↓ 确定下一跳网络
3. 重新封装:  [本地网络2头部 | IP数据包 | TCP段 | 应用数据]
              → 发往本地网络2

四、可靠通信的详细机制

序列号与确认号

每个字节都有序号,TCP 段头部携带:

  • 序列号(Seq):本段数据第一个字节的编号
  • 确认号(ACK):期望收到的下一个字节的编号(即已正确收到的最后字节+1)
    ACK号=已成功收到的最后一个字节的序号+1\text{ACK号} = \text{已成功收到的最后一个字节的序号} + 1ACK=已成功收到的最后一个字节的序号+1
例子:发送 "Hello"(5个字节,初始序号100)
发送方发出:
  段1:seq=100,数据="Hel"(字节100,101,102)
  段2:seq=103,数据="lo" (字节103,104)
接收方回复:
  ACK=103(意思:100,101,102我收到了,下一个期望103)
  ACK=105(意思:103,104我也收到了,下一个期望105)

重传队列工作原理

发送方维护一个重传队列:
重传队列:
  [数据(seq=100~102), 计时器=3s]
  [数据(seq=103~104), 计时器=3s]
情况1:收到ACK=103
  → 删除seq=100~102的条目
  队列:[数据(seq=103~104), 计时器=3s]
情况2:计时器超时还没收到ACK=103
  → 重新发送 seq=100~102 的数据
  → 重置计时器

滑动窗口详细示意

序列号空间(想象成一条长带子):
|<—已确认—>|<—已发出未确认(窗口)—>|<—可发送—>|<—不能发—>|
  1     100  101              200    201     300   301...
窗口 = 100字节(接收方通告的)
当前可以发送:101到200的数据
等ACK=200收到后,窗口向右滑动

五、连接建立详细流程

OPEN 调用的两种模式

被动 OPEN(服务器用):

服务器调用:PASSIVE OPEN(本地端口=80, 外部Socket=任意)
含义:我在端口80等待,任何客户端来了我都接受
TCP创建一个TCB条目,状态=LISTEN(监听中)

主动 OPEN(客户端用):

客户端调用:ACTIVE OPEN(本地端口=12345, 目标IP=93.184.216.34, 目标端口=80)
含义:我要主动连接那台服务器的80端口
TCP创建TCB,发送SYN段,等待服务器响应

三次握手与序列号同步

为什么初始序列号(ISN)要用时钟? 防止旧连接的"游魂"数据包干扰新连接。
ISN=f(时钟)(基于系统时钟,每隔一段时间递增)\text{ISN} = f(\text{时钟}) \quad \text{(基于系统时钟,每隔一段时间递增)}ISN=f(时钟)(基于系统时钟,每隔一段时间递增)

三次握手详细过程:
客户端                                    服务器
  |                                          |
  |  ①发送 SYN                              |
  |  seq=100(客户端的ISN)                   |
  |----------------------------------------> |
  |                                          |
  |  ②服务器收到,回复 SYN+ACK              |
  |  seq=300(服务器的ISN)                   |
  |  ack=101(期望客户端下一个发101)         |
  | <----------------------------------------|
  |                                          |
  |  ③客户端确认                             |
  |  seq=101                                 |
  |  ack=301(期望服务器下一个发301)         |
  |----------------------------------------> |
  |                                          |
  |      连接建立!双方都知道了对方的ISN      |
  |      客户端从101开始发数据               |
  |      服务器从301开始发数据               |

完成三次握手后,双方都同步了各自的初始序列号,可以开始可靠通信。

六、数据通信详解

PUSH 标志的作用

没有PUSH时(TCP自己决定何时发):
应用程序写入:A  → TCP缓冲区等待攒更多数据
应用程序写入:B  → 缓冲区:AB,继续等待
应用程序写入:C  → 缓冲区:ABC,TCP认为够了 → 发送[ABC]
有PUSH时(立刻发):
应用程序写入:A,设置PUSH → TCP立刻发送[A]
(不等后续数据,适合交互式应用如SSH/Telnet)

紧急数据(Urgent Data)

TCP 提供了一种"插队"机制,告诉接收方"有紧急数据在路上,赶快处理"。

  • TCP 本身不定义如何处理紧急数据,只是通知接收进程
  • 接收进程收到通知后自行决定如何处理
  • 典型用途:Telnet 中的 Ctrl+C 中断信号

七、鲁棒性原则(Robustness Principle)

RFC 793 第2.10节提出了一条著名的设计原则,后来被称为 Postel 法则

发送时保守,接收时宽容
(Be conservative in what you do, be liberal in what you accept from others.)
含义:

  • 发送时:严格按照规范构造数据,不发送对方可能无法处理的内容
  • 接收时:尽量接受和理解不完全符合规范的数据(只要含义清晰)
    原因: 不同厂商实现的 TCP 可能存在细节差异,过于苛刻会导致互操作性差。

八、TCP 与 IP 的分工

TCP 负责:                         IP 负责:
+---------------------------+      +---------------------------+
| 排序(乱序重排)           |      | 路由(找路径)            |
| 去重(丢弃重复包)          |      | 分片(切割大包)          |
| 重传(丢包重发)            |      | 重组(拼接碎片)          |
| 流控(窗口机制)            |      | 尽力而为投递              |
| 拥塞控制(后续RFC定义)     |      | 不保证可靠性              |
| 连接管理(建立/关闭)       |      | 无连接                    |
+---------------------------+      +---------------------------+

TCP 假设 IP 可能会:

  • 丢失数据
  • 打乱顺序
  • 产生重复
  • 传输损坏的数据
    TCP 对此全部容忍,并自行修复。

九、完整 C++ 演示代码

以下代码模拟 TCP 的核心机制:序列号管理、ACK确认、重传队列、滑动窗口和三次握手状态机。

#include <iostream>     // 标准输入输出
#include <vector>       // 动态数组
#include <queue>        // 队列(用于待发送数据)
#include <map>          // 有序映射(重传队列)
#include <string>       // 字符串
#include <cassert>      // 断言
#include <algorithm>    // std::min/max
#include <iomanip>      // 格式化输出
// ============================================================
// TCP 连接状态枚举(简化版状态机)
// RFC 793 定义了完整的11个状态,这里演示核心几个
// ============================================================
enum class TCPState {
    CLOSED,       // 初始状态,无连接
    LISTEN,       // 被动等待连接(服务器PASSIVE OPEN后)
    SYN_SENT,     // 已发送SYN,等待服务器回应(客户端主动连接后)
    SYN_RECEIVED, // 收到SYN并已回复SYN+ACK(服务器状态)
    ESTABLISHED,  // 连接建立,可以传数据
    FIN_WAIT,     // 已发送FIN,等待对方确认
    CLOSE_WAIT,   // 收到对方FIN,等待本地关闭
    CLOSED_DONE   // 完全关闭
};
// 状态名称字符串
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:     return "FIN_WAIT";
        case TCPState::CLOSE_WAIT:   return "CLOSE_WAIT";
        case TCPState::CLOSED_DONE:  return "CLOSED_DONE";
        default:                     return "UNKNOWN";
    }
}
// ============================================================
// TCP 段结构体(简化版,只含核心字段)
// 完整的TCP段还有很多字段(头部选项等)
// ============================================================
struct TCPSegment {
    uint32_t seq_num;      // 序列号:本段第一个字节的编号
    uint32_t ack_num;      // 确认号:期望收到的下一个字节编号
    uint16_t window;       // 窗口大小:告诉对方我能接收多少字节
    bool     syn;          // SYN标志:建立连接时使用
    bool     ack;          // ACK标志:确认收到数据
    bool     fin;          // FIN标志:关闭连接时使用
    bool     push;         // PUSH标志:要求立刻交给应用程序
    std::string data;      // 数据内容(简化为字符串)
    TCPSegment()
        : seq_num(0), ack_num(0), window(0)
        , syn(false), ack(false), fin(false), push(false) {}
};
// 打印TCP段内容
void print_segment(const std::string& direction, const TCPSegment& seg) {
    std::cout << direction << " [";
    if (seg.syn)  std::cout << "SYN ";
    if (seg.ack)  std::cout << "ACK ";
    if (seg.fin)  std::cout << "FIN ";
    if (seg.push) std::cout << "PSH ";
    std::cout << "] seq=" << seg.seq_num
              << " ack=" << seg.ack_num
              << " win=" << seg.window;
    if (!seg.data.empty())
        std::cout << " data=\"" << seg.data << "\"";
    std::cout << "\n";
}
// ============================================================
// 重传队列条目(存储已发送但未确认的数据)
// ============================================================
struct RetransmitEntry {
    uint32_t    seq_num;      // 数据起始序列号
    std::string data;         // 数据内容
    int         timer;        // 剩余超时计数(模拟,单位:轮次)
    RetransmitEntry(uint32_t s, const std::string& d, int t)
        : seq_num(s), data(d), timer(t) {}
};
// ============================================================
// TCP 连接模拟类(TCB,传输控制块)
// 包含一端的完整TCP状态
// ============================================================
class TCPConnection {
public:
    std::string name;           // 标识符("客户端"或"服务器")
    TCPState    state;          // 当前连接状态
    uint32_t    snd_nxt;        // 发送序列号:下一个要发送的字节编号
    uint32_t    snd_una;        // 发送未确认序号:最早未被ACK的序号
    uint32_t    rcv_nxt;        // 接收序列号:期望下一个收到的字节编号
    uint16_t    snd_wnd;        // 发送窗口:对方允许我发送的字节数
    uint16_t    rcv_wnd;        // 接收窗口:我告诉对方能接收多少
    std::vector<RetransmitEntry> retransmit_queue; // 重传队列
    std::string recv_buffer;    // 接收缓冲区(存放已收到但未读取的数据)
    TCPConnection(const std::string& n, uint32_t isn)
        : name(n), state(TCPState::CLOSED)
        , snd_nxt(isn), snd_una(isn), rcv_nxt(0)
        , snd_wnd(1000), rcv_wnd(1000) {}
    // ---------------------------------------------------------
    // 打印当前状态
    // ---------------------------------------------------------
    void print_state() const {
        std::cout << "[" << name << "] 状态=" << state_name(state)
                  << " snd_nxt=" << snd_nxt
                  << " snd_una=" << snd_una
                  << " rcv_nxt=" << rcv_nxt
                  << " 重传队列=" << retransmit_queue.size() << "项"
                  << "\n";
    }
    // ---------------------------------------------------------
    // 被动OPEN:服务器等待连接
    // ---------------------------------------------------------
    void passive_open() {
        assert(state == TCPState::CLOSED);
        state = TCPState::LISTEN;
        std::cout << "[" << name << "] 被动OPEN,进入LISTEN状态,等待连接...\n";
    }
    // ---------------------------------------------------------
    // 主动OPEN:客户端发起连接,发送第一个SYN
    // 返回要发送给对方的SYN段
    // ---------------------------------------------------------
    TCPSegment active_open() {
        assert(state == TCPState::CLOSED);
        state = TCPState::SYN_SENT;
        TCPSegment seg;
        seg.syn     = true;
        seg.seq_num = snd_nxt;  // ISN(初始序列号)
        seg.window  = rcv_wnd;
        // SYN本身占用一个序号(虽然没有数据)
        snd_nxt++;
        // 加入重传队列(如果对方不回应就重传)
        retransmit_queue.emplace_back(seg.seq_num, "[SYN]", 5);
        std::cout << "[" << name << "] 主动OPEN,发送SYN,进入SYN_SENT状态\n";
        print_segment("  发送", seg);
        return seg;
    }
    // ---------------------------------------------------------
    // 处理收到的段(根据当前状态做不同处理)
    // 返回需要发送给对方的响应段(如果有的话)
    // ---------------------------------------------------------
    TCPSegment receive_segment(const TCPSegment& seg) {
        TCPSegment response; // 默认响应(可能不发送)
        switch (state) {
        // 服务器处于LISTEN状态,收到客户端SYN
        case TCPState::LISTEN:
            if (seg.syn && !seg.ack) {
                std::cout << "[" << name << "] 收到SYN,发送SYN+ACK,进入SYN_RECEIVED\n";
                print_segment("  收到", seg);
                // 记住客户端的序列号
                rcv_nxt = seg.seq_num + 1; // SYN占一个序号
                // 构建 SYN+ACK 响应
                response.syn     = true;
                response.ack     = true;
                response.seq_num = snd_nxt;   // 服务器自己的ISN
                response.ack_num = rcv_nxt;    // 确认客户端的SYN
                response.window  = rcv_wnd;
                snd_nxt++; // SYN占一个序号
                retransmit_queue.emplace_back(response.seq_num, "[SYN+ACK]", 5);
                state = TCPState::SYN_RECEIVED;
                print_segment("  发送", response);
            }
            break;
        // 客户端处于SYN_SENT状态,收到服务器SYN+ACK
        case TCPState::SYN_SENT:
            if (seg.syn && seg.ack) {
                std::cout << "[" << name << "] 收到SYN+ACK,发送ACK,连接建立(ESTABLISHED)\n";
                print_segment("  收到", seg);
                // 更新确认号,确认对方的SYN
                rcv_nxt = seg.seq_num + 1;
                snd_una = seg.ack_num; // 对方确认了我的SYN
                snd_wnd = seg.window;  // 更新发送窗口
                // 清空重传队列(SYN已被确认)
                retransmit_queue.clear();
                // 发送ACK确认
                response.ack     = true;
                response.seq_num = snd_nxt;
                response.ack_num = rcv_nxt;
                response.window  = rcv_wnd;
                state = TCPState::ESTABLISHED;
                print_segment("  发送", response);
            }
            break;
        // 服务器处于SYN_RECEIVED状态,收到客户端的ACK(完成三次握手)
        case TCPState::SYN_RECEIVED:
            if (seg.ack && !seg.syn) {
                std::cout << "[" << name << "] 收到ACK,三次握手完成,连接建立(ESTABLISHED)\n";
                print_segment("  收到", seg);
                snd_una = seg.ack_num;
                snd_wnd = seg.window;
                retransmit_queue.clear(); // SYN+ACK已被确认
                state = TCPState::ESTABLISHED;
                // 服务器不需要再发送任何东西(三次握手结束)
            }
            break;
        // 连接已建立,处理数据和ACK
        case TCPState::ESTABLISHED:
            // 如果是带数据的段
            if (!seg.data.empty()) {
                std::cout << "[" << name << "] 收到数据段\n";
                print_segment("  收到", seg);
                if (seg.seq_num == rcv_nxt) {
                    // 序列号正确,接收数据
                    recv_buffer += seg.data;
                    rcv_nxt += (uint32_t)seg.data.size();
                    // 发送ACK
                    response.ack     = true;
                    response.seq_num = snd_nxt;
                    response.ack_num = rcv_nxt; // 告知下一个期望的序号
                    response.window  = rcv_wnd;
                    print_segment("  发送ACK", response);
                } else {
                    std::cout << "  [警告] 序号不连续,丢弃(期望"
                              << rcv_nxt << ",收到" << seg.seq_num << ")\n";
                }
            }
            // 如果包含ACK
            if (seg.ack) {
                uint32_t new_ack = seg.ack_num;
                if (new_ack > snd_una) {
                    std::cout << "[" << name << "] 收到ACK=" << new_ack
                              << ",滑动窗口前进\n";
                    snd_una = new_ack;
                    snd_wnd = seg.window; // 更新窗口大小
                    // 从重传队列中删除已确认的数据
                    auto it = retransmit_queue.begin();
                    while (it != retransmit_queue.end()) {
                        if (it->seq_num + it->data.size() <= new_ack) {
                            std::cout << "  删除重传队列条目 seq="
                                      << it->seq_num << "\n";
                            it = retransmit_queue.erase(it);
                        } else {
                            ++it;
                        }
                    }
                }
            }
            // 如果是FIN(对方要关闭连接)
            if (seg.fin) {
                std::cout << "[" << name << "] 收到FIN,进入CLOSE_WAIT\n";
                rcv_nxt++; // FIN占一个序号
                response.ack     = true;
                response.seq_num = snd_nxt;
                response.ack_num = rcv_nxt;
                response.window  = rcv_wnd;
                state = TCPState::CLOSE_WAIT;
                print_segment("  发送ACK", response);
            }
            break;
        default:
            break;
        }
        return response;
    }
    // ---------------------------------------------------------
    // 发送数据(连接已建立时)
    // 返回要发送的TCP段
    // ---------------------------------------------------------
    TCPSegment send_data(const std::string& data, bool push = false) {
        assert(state == TCPState::ESTABLISHED);
        assert(!data.empty());
        // 检查窗口是否允许发送
        uint32_t in_flight = snd_nxt - snd_una; // 已发出未确认的字节数
        if (in_flight + data.size() > snd_wnd) {
            std::cout << "[" << name << "] 窗口已满,无法发送!"
                      << "(已在途=" << in_flight
                      << ",窗口=" << snd_wnd << ")\n";
            return TCPSegment();
        }
        TCPSegment seg;
        seg.seq_num = snd_nxt;
        seg.ack_num = rcv_nxt;
        seg.ack     = true; // 数据段通常也带ACK
        seg.push    = push;
        seg.data    = data;
        seg.window  = rcv_wnd;
        // 加入重传队列
        retransmit_queue.emplace_back(snd_nxt, data, 5);
        snd_nxt += (uint32_t)data.size();
        std::cout << "[" << name << "] 发送数据"
                  << (push ? "(PUSH)" : "") << "\n";
        print_segment("  发送", seg);
        return seg;
    }
    // ---------------------------------------------------------
    // 主动关闭连接(发送FIN)
    // ---------------------------------------------------------
    TCPSegment close_connection() {
        assert(state == TCPState::ESTABLISHED
            || state == TCPState::CLOSE_WAIT);
        TCPSegment seg;
        seg.fin     = true;
        seg.ack     = true;
        seg.seq_num = snd_nxt;
        seg.ack_num = rcv_nxt;
        seg.window  = rcv_wnd;
        snd_nxt++; // FIN占一个序号
        if (state == TCPState::ESTABLISHED)
            state = TCPState::FIN_WAIT;
        else
            state = TCPState::CLOSED_DONE;
        std::cout << "[" << name << "] 发送FIN,请求关闭连接\n";
        print_segment("  发送", seg);
        return seg;
    }
};
// ============================================================
// 主函数:完整演示三次握手、数据传输、四次挥手
// ============================================================
int main() {
    std::cout << std::string(60, '=') << "\n";
    std::cout << "RFC 793 TCP 核心机制演示\n";
    std::cout << std::string(60, '=') << "\n\n";
    // -------------------------------------------------------
    // 创建客户端和服务器(各有自己的初始序列号ISN)
    // 实际中ISN基于系统时钟生成,这里直接指定
    // -------------------------------------------------------
    TCPConnection client("客户端", 100); // 客户端ISN=100
    TCPConnection server("服务器", 300); // 服务器ISN=300
    // -------------------------------------------------------
    // 阶段1:三次握手建立连接
    // -------------------------------------------------------
    std::cout << "=== 阶段1:三次握手(建立连接)===\n\n";
    // 服务器先被动等待
    server.passive_open();
    std::cout << "\n";
    // 第1次握手:客户端发SYN
    std::cout << "-- 第1次握手 --\n";
    TCPSegment syn = client.active_open();
    client.print_state();
    std::cout << "\n";
    // 第2次握手:服务器收到SYN,回复SYN+ACK
    std::cout << "-- 第2次握手 --\n";
    TCPSegment syn_ack = server.receive_segment(syn);
    server.print_state();
    std::cout << "\n";
    // 第3次握手:客户端收到SYN+ACK,回复ACK
    std::cout << "-- 第3次握手 --\n";
    TCPSegment final_ack = client.receive_segment(syn_ack);
    client.print_state();
    std::cout << "\n";
    // 服务器收到最终ACK,连接建立
    server.receive_segment(final_ack);
    server.print_state();
    std::cout << "\n";
    // -------------------------------------------------------
    // 阶段2:数据传输(客户端向服务器发送数据)
    // -------------------------------------------------------
    std::cout << "=== 阶段2:数据传输 ===\n\n";
    // 客户端发送"Hello"(不用PUSH,TCP自己决定时机)
    std::cout << "-- 客户端发送数据 \"Hello\" --\n";
    TCPSegment data1 = client.send_data("Hello");
    client.print_state();
    std::cout << "\n";
    // 服务器收到数据,回复ACK
    std::cout << "-- 服务器收到数据并ACK --\n";
    TCPSegment ack1 = server.receive_segment(data1);
    server.print_state();
    std::cout << "  服务器接收缓冲区:\"" << server.recv_buffer << "\"\n\n";
    // 客户端收到ACK,更新状态
    std::cout << "-- 客户端处理ACK --\n";
    client.receive_segment(ack1);
    client.print_state();
    std::cout << "\n";
    // 客户端发送"World"(带PUSH,要求立刻交给应用程序)
    std::cout << "-- 客户端发送 \"World\"(带PUSH)--\n";
    TCPSegment data2 = client.send_data("World", true);
    std::cout << "\n";
    std::cout << "-- 服务器收到PUSH数据 --\n";
    TCPSegment ack2 = server.receive_segment(data2);
    std::cout << "  服务器接收缓冲区:\"" << server.recv_buffer
              << "\"(PUSH触发立刻交给应用程序)\n\n";
    client.receive_segment(ack2);
    // -------------------------------------------------------
    // 阶段3:流量控制演示
    // -------------------------------------------------------
    std::cout << "=== 阶段3:流量控制演示 ===\n\n";
    // 模拟服务器缩小窗口(缓冲区快满了)
    std::cout << "-- 服务器通告窗口缩小到50字节 --\n";
    // (实际中窗口通过ACK携带,这里直接修改演示效果)
    client.snd_wnd = 50;
    std::cout << "  客户端发送窗口更新为 " << client.snd_wnd << " 字节\n";
    // 尝试发送超过窗口大小的数据
    std::string big_data(60, 'X'); // 60字节,超过50字节窗口
    std::cout << "-- 尝试发送60字节数据(超过50字节窗口)--\n";
    client.send_data(big_data); // 应该被阻止
    std::cout << "\n";
    // 发送小于窗口的数据
    std::string small_data(30, 'Y'); // 30字节,不超窗口
    std::cout << "-- 发送30字节数据(在窗口范围内)--\n";
    TCPSegment data3 = client.send_data(small_data);
    std::cout << "\n";
    server.receive_segment(data3);
    // -------------------------------------------------------
    // 阶段4:关闭连接(四次挥手)
    // -------------------------------------------------------
    std::cout << "\n=== 阶段4:关闭连接 ===\n\n";
    // 客户端发起关闭
    std::cout << "-- 客户端发送FIN(主动关闭)--\n";
    TCPSegment fin1 = client.close_connection();
    std::cout << "\n";
    // 服务器收到FIN,回复ACK,进入CLOSE_WAIT
    std::cout << "-- 服务器收到FIN --\n";
    TCPSegment ack_fin1 = server.receive_segment(fin1);
    server.print_state();
    std::cout << "\n";
    // 服务器也发送FIN(表示我也关闭了)
    std::cout << "-- 服务器发送FIN --\n";
    TCPSegment fin2 = server.close_connection();
    server.print_state();
    std::cout << "\n";
    // 客户端收到FIN,回复ACK
    std::cout << "-- 客户端收到服务器FIN,发送最终ACK --\n";
    client.receive_segment(fin2);
    client.state = TCPState::CLOSED_DONE;
    client.print_state();
    std::cout << "\n";
    std::cout << "=== 连接完全关闭 ===\n\n";
    // -------------------------------------------------------
    // 汇总
    // -------------------------------------------------------
    std::cout << std::string(60, '-') << "\n";
    std::cout << "演示完成!\n";
    std::cout << "服务器共收到数据:\"" << server.recv_buffer << "\"\n";
    std::cout << std::string(60, '-') << "\n";
    return 0;
}

十、核心知识点思维导图

TCP协议

六大核心功能

连接管理

可靠性机制

设计原则

基本数据传输
字节流+PUSH标志

可靠性
序号+ACK+重传

流量控制
滑动窗口

多路复用
端口+Socket

连接管理
建立与关闭

优先级与安全
利用IP的ToS字段

主动OPEN
客户端发起

被动OPEN
服务器等待

三次握手
SYN+SYN-ACK+ACK

四次挥手
FIN+ACK+FIN+ACK

TCB数据结构
存储连接全部状态

序列号
每字节唯一编号

ACK确认
收到后告知发送方

重传队列
超时未ACK则重发

校验和
检测数据损坏

鲁棒性原则
发送保守 接收宽容

全双工
双向同时传输

面向连接
先建立再通信

十一、关键术语速查


术语 全称 含义
TCP Transmission Control Protocol 传输控制协议
TCB Transmission Control Block 传输控制块,存储连接状态的数据结构
Segment TCP段,TCP传输的基本数据单元
SYN Synchronize 同步序列号,用于建立连接
ACK Acknowledgment 确认,告知发送方数据已收到
FIN Finish 终止,用于关闭连接
PUSH 推送标志,要求立刻发送/交付数据
ISN Initial Sequence Number 初始序列号,连接建立时的起始序号
Port 端口,标识主机上的具体进程
Socket 套接字 = IP地址 + 端口号
Window 窗口,流量控制的核心,限制发送方速度
Well-known Socket 知名端口,固定分配给常用服务
Full Duplex 全双工,双向同时独立传输
Passive OPEN 被动连接,服务器等待客户端连接
Active OPEN 主动连接,客户端主动发起连接

TCP 协议详解:从零开始理解

一、TCP 是什么?

TCP(传输控制协议)是互联网上两台计算机之间可靠通信的基础协议。你可以把它想象成两个人之间寄信,但这个邮差系统会:

  • 确认每封信都送达了
  • 如果信件丢失,自动重发
  • 保证信件按顺序到达
  • 控制发送速度,不让对方被信件淹没

二、TCP 数据包头部格式

每次发送数据,TCP 都会在数据前面加一个"信封",这个信封就是头部(Header)

 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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|          Source Port          |       Destination Port        |
|        源端口(16位)           |        目标端口(16位)         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                        Sequence Number                        |
|                        序列号(32位)                           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Acknowledgment Number                      |
|                        确认号(32位)                           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  Data |       |U|A|P|R|S|F|                                   |
|Offset |Reserve|R|C|S|S|Y|I|          Window(窗口)            |
|数据偏移| 保留位 |G|K|H|T|N|N|                                   |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|           Checksum            |         Urgent Pointer        |
|         校验和(16位)           |         紧急指针(16位)        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Options(选项)       |    Padding(填充)   |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                             data(数据)                       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

各字段详细解释


字段 大小 含义
源端口 16位 发送方的端口号(如你的浏览器用12345端口)
目标端口 16位 接收方的端口号(如服务器用80端口)
序列号 32位 本段数据第一个字节的编号
确认号 32位 期望收到对方下一个字节的编号
数据偏移 4位 头部有多少个32位字(告诉你数据从哪里开始)
保留位 6位 留给未来用,必须为0
控制位 6位 URG/ACK/PSH/RST/SYN/FIN 六个标志
窗口 16位 接收方还能接收多少字节
校验和 16位 用于检测数据是否损坏
紧急指针 16位 URG=1时有效,指向紧急数据末尾

6个控制位(Control Bits)说明

URG = 1  →  有紧急数据,接收方要优先处理
ACK = 1  →  确认号字段有效(连接建立后始终为1)
PSH = 1  →  推送功能,数据要立刻交给应用层,不能缓存
RST = 1  →  重置连接(出错时强行断开)
SYN = 1  →  同步序列号(建立连接时使用)
FIN = 1  →  发送方没有更多数据了(关闭连接时使用)

校验和的计算

TCP校验和不只覆盖TCP头部和数据,还覆盖一个伪头部(Pseudo Header)

+--------+--------+--------+--------+
|           源IP地址(32位)          |
+--------+--------+--------+--------+
|           目的IP地址(32位)        |
+--------+--------+--------+--------+
|  0x00  |  协议号 |    TCP总长度     |
+--------+--------+--------+--------+

计算方法:对头部+伪头部+数据的所有16位字求反码和,结果填入校验和字段。这样可以防止数据包被错误地路由到错误的地址。

选项(Options)

TCP选项有两种格式:

格式一:单字节
+--------+
| 种类字节 |
+--------+
格式二:多字节
+--------+--------+--------...--------+
| 种类字节 | 长度字节 |    数据字节...    |
+--------+--------+--------...--------+
注意:长度字节包含"种类字节"和"长度字节"本身

目前定义的选项:

种类值(八进制) 长度 含义
0 - 选项列表结束
1 - 空操作(用于对齐)
2 4字节 最大报文段大小(MSS)

MSS(Maximum Segment Size,最大报文段大小):只在握手时的SYN包中出现,告诉对方自己最多能接收多大的数据块。

三、核心概念:序列号

为什么需要序列号?

想象你要发100页文件给朋友,你把每页都编号(第1页、第2页……)。如果朋友收到了第1、2、4页,发现缺第3页,就知道要请你重发第3页。
TCP 也是这样:每一个字节都有一个序列号

序列号空间

序列号是32位整数,范围从 000232−12^{32} - 12321(约43亿)。用完之后从0重新开始(模 2322^{32}232 运算)。
序列号范围=[0,232−1]\text{序列号范围} = [0, 2^{32} - 1]序列号范围=[0,2321]

序列空间的四个区域

发送序列空间示意图:
          1          2          3          4
 ---------|----------|----------|----------
       SND.UNA    SND.NXT    SND.UNA
                             +SND.WND
区域1:已发送且已被确认的旧序号(可以忘掉了)
区域2:已发送但还没收到确认的序号(等待ACK)
区域3:允许发送新数据的序号范围(发送窗口)
区域4:还不能发送,对方没准备好
接收序列空间示意图:
          1          2          3
 ---------|----------|----------
       RCV.NXT    RCV.NXT
                  +RCV.WND
区域1:已收到并确认的旧序号
区域2:允许接收的序号范围(接收窗口)
区域3:还不能接收,暂时不处理

判断一个ACK是否有效

一个"可接受的ACK"(新的ACK)满足:
SND.UNA<SEG.ACK≤SND.NXTSND.UNA < SEG.ACK \leq SND.NXTSND.UNA<SEG.ACKSND.NXT
意思是:ACK号必须比最老的未确认序号大,同时不超过下一个要发送的序号。

判断一个收到的数据段是否有效

数据段在接收窗口内,满足以下任一条件:
RCV.NXT≤SEG.SEQ<RCV.NXT+RCV.WNDRCV.NXT \leq SEG.SEQ < RCV.NXT + RCV.WNDRCV.NXTSEG.SEQ<RCV.NXT+RCV.WND
或RCV.NXT≤SEG.SEQ+SEG.LEN−1<RCV.NXT+RCV.WND\text{或} \quad RCV.NXT \leq SEG.SEQ + SEG.LEN - 1 < RCV.NXT + RCV.WNDRCV.NXTSEG.SEQ+SEG.LEN1<RCV.NXT+RCV.WND
(前者检查段的开头是否在窗口内,后者检查段的结尾是否在窗口内)
对于特殊情况,完整的判断表:

段长度 接收窗口 判断条件
0 0 SEG.SEQ=RCV.NXTSEG.SEQ = RCV.NXTSEG.SEQ=RCV.NXT
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 开头或结尾在窗口内即可

初始序列号(ISN)的选择

建立新连接时,不能随便从0开始,否则新连接的数据包可能和网络中残留的旧连接数据包混淆。
解决方法:ISN由一个时钟驱动,该时钟每 444 微秒低位加1,周期约为:
ISN周期=232×4μs≈4.55 小时\text{ISN周期} = 2^{32} \times 4\mu s \approx 4.55 \text{ 小时}ISN周期=232×4μs4.55 小时
只要网络中数据包的最大生存时间(MSL = 2分钟)远小于4.55小时,就不会重复。

四、TCP 连接状态机

TCP连接在生命周期内会经历多个状态:

被动OPEN
(服务器等待)

主动OPEN
(客户端发SYN)

收到SYN
发送SYN+ACK

调用SEND

同时发起连接
收到SYN

收到SYN+ACK
发送ACK

收到ACK

收到RST

本地调用CLOSE
发送FIN

收到FIN
发送ACK

收到ACK

收到FIN
发送ACK

收到FIN
发送ACK

本地调用CLOSE
发送FIN

收到ACK

收到ACK

等待2MSL超时

CLOSED

LISTEN

SYN_SENT

SYN_RCVD

ESTABLISHED

FIN_WAIT_1

CLOSE_WAIT

FIN_WAIT_2

CLOSING

TIME_WAIT

LAST_ACK

各状态含义


状态 含义
CLOSED 无连接,不存在TCB
LISTEN 等待任何客户端的连接请求
SYN-SENT 已发送SYN,等待对方SYN+ACK
SYN-RECEIVED 已收到且发送了SYN,等待ACK确认
ESTABLISHED 连接已建立,可以正常收发数据
FIN-WAIT-1 已发FIN,等待对方ACK或FIN
FIN-WAIT-2 已收到对方ACK,等待对方FIN
CLOSE-WAIT 已收到对方FIN,等待本地应用关闭
CLOSING 双方同时关闭,等待对方ACK
LAST-ACK 已发FIN,等待最后的ACK
TIME-WAIT 等待2MSL(4分钟),确保对方收到最后ACK

五、建立连接:三次握手

为什么需要三次握手?

双方都需要确认对方的初始序列号。两次握手只能让一方确认,三次才能让双方都确认。

正常三次握手流程

TCP A(客户端)                              TCP B(服务器)
    |                                           |
    |  状态: CLOSED                             |  状态: LISTEN
    |                                           |
    |--- SEQ=100, CTL=SYN ------------------->|  (1) A发SYN,序号100
    |                                           |  状态: SYN-RECEIVED
    |                                           |
    |<-- SEQ=300, ACK=101, CTL=SYN+ACK -------|  (2) B发SYN+ACK
    |  状态: ESTABLISHED                        |      确认A的序号(101=100+1)
    |                                           |      B自己的序号是300
    |                                           |
    |--- SEQ=101, ACK=301, CTL=ACK ---------->|  (3) A确认B的序号(301=300+1)
    |                                           |  状态: ESTABLISHED
    |                                           |
    |--- SEQ=101, ACK=301, CTL=ACK+DATA ----->|  (4) 开始发数据
    |                                           |

关键点:

  • 步骤1:A选择初始序号100,发SYN
  • 步骤2:B选择初始序号300,同时确认A的SYN(ACK=101,表示期待收到第101字节)
  • 步骤3:A确认B的SYN(ACK=301,表示期待收到第301字节)
  • 注意:ACK包本身不占序号空间(否则会出现"确认的确认"死循环)

同时发起连接(两端同时SYN)

TCP A                                    TCP B
  |                                        |
  |  CLOSED                                |  CLOSED
  |                                        |
  |------ SEQ=100, CTL=SYN ------------>  |  (A的SYN在路上)
  |  SYN-SENT                              |
  |                                        |
  |  <-- SEQ=300, CTL=SYN -------------- |  B也发了SYN
  |  SYN-RECEIVED                          |  SYN-SENT -> SYN-RECEIVED
  |                                        |
  |------ SEQ=100, ACK=301, SYN+ACK -->  |  A回复SYN+ACK
  |                                        |
  |  <-- SEQ=300, ACK=101, SYN+ACK ----  |  B回复SYN+ACK
  |  ESTABLISHED                           |  ESTABLISHED

这种情况虽然罕见,但TCP完全支持。

处理旧的重复SYN(RST的作用)

TCP A(客户端)                              TCP B(服务器)
  |                                          |
  |  SYN-SENT                                |  LISTEN
  |                                          |
  |...  SEQ=100, SYN  .......................|  A的SYN还在路上
  |                                          |
  |  (网络中有旧的重复SYN)                     |
  |  SEQ=90, SYN (旧的!) -->                 |  B以为是新连接请求
  |                                          |  SYN-RECEIVED
  |                                          |
  |<-- SEQ=300, ACK=91, SYN+ACK  ----------|  B确认了旧SYN(ACK=91)
  |                                          |
  |  A发现ACK=91不对(应该是101)              |
  |--- SEQ=91, CTL=RST --------------------->|  A发RST告诉B出错了
  |                                          |  B回到LISTEN状态
  |                                          |
  |...  SEQ=100, SYN  ....................-->|  A的原始SYN终于到达
  |                                          |  正常握手继续...

六、关闭连接:四次挥手

关闭连接是半双工的:每一方独立关闭自己的发送方向。

正常关闭流程(A主动关闭)

TCP A                                              TCP B
  |  ESTABLISHED                                     |  ESTABLISHED
  |                                                  |
  |  (应用层调用CLOSE)                                |
  |--- SEQ=100, ACK=300, CTL=FIN+ACK ------------>|  A发FIN,进入FIN-WAIT-1
  |  FIN-WAIT-1                                      |  CLOSE-WAIT
  |                                                  |
  |<-- SEQ=300, ACK=101, CTL=ACK ------------------|  B确认A的FIN
  |  FIN-WAIT-2                                      |
  |                                                  |  (B的应用层调用CLOSE)
  |<-- SEQ=300, ACK=101, CTL=FIN+ACK --------------|  B发FIN
  |  TIME-WAIT                                       |  LAST-ACK
  |                                                  |
  |--- SEQ=101, ACK=301, CTL=ACK ------------------>|  A确认B的FIN
  |                                                  |  CLOSED
  |  (等待2MSL = 4分钟)                              |
  |  CLOSED                                          |

为什么需要TIME-WAIT等待2MSL?
因为最后A发给B的ACK可能丢失。如果丢失了,B会重发FIN。A必须等待足够长的时间(2个最大报文段生存时间 = 4分钟),确保B收到了最终的ACK,才能真正关闭。

同时关闭流程

TCP A                                              TCP B
  |  ESTABLISHED                                     |  ESTABLISHED
  |                                                  |
  |  (双方同时调用CLOSE)                              |
  |--- SEQ=100, ACK=300, FIN+ACK ------------>  ... |
  |<-- SEQ=300, ACK=100, FIN+ACK ------------ ...   |
  |  FIN-WAIT-1                                      |  FIN-WAIT-1
  |                                                  |
  |  (双方都收到对方的FIN)                            |
  |--- SEQ=101, ACK=301, ACK ---------------------->|
  |<-- SEQ=301, ACK=101, ACK -----------------------|
  |  TIME-WAIT                                       |  TIME-WAIT
  |  (2MSL后)                                        |  (2MSL后)
  |  CLOSED                                          |  CLOSED

七、半开连接(Half-Open Connection)

当一端崩溃重启后,另一端还以为连接正常,这就是半开连接

场景:A崩溃了,B不知道

TCP A(崩溃重启)                           TCP B(以为还正常)
  |  CLOSED(崩溃了)                         |  ESTABLISHED
  |                                          |  (B记得状态:发送300,接收100)
  |                                          |
  |--- SEQ=400, CTL=SYN ------------------>|  A重启后发SYN(新序号400)
  |  SYN-SENT                               |
  |                                          |  B困惑:这个SYN不在窗口内
  |<-- SEQ=300, ACK=100, CTL=ACK ----------|  B按老状态回复ACK
  |                                          |
  |  A没发过序号100的数据,发现出错了         |
  |--- SEQ=100, CTL=RST ------------------>|  A发RST
  |                                          |  B收到RST,中止连接 (CLOSED)
  |                                          |
  |--- SEQ=400, CTL=SYN ------------------>|  A重新发SYN,正常握手

八、RST(重置)信号

RST是TCP的"紧急刹车",用于异常情况下强制断开连接。

RST何时发送?

情况一:连接不存在(CLOSED状态)

  • 收到任何非RST包,都回复RST
  • 如果来包有ACK:<SEQ=SEG.ACK><CTL=RST>
  • 如果来包没有ACK:<SEQ=0><ACK=SEG.SEQ+SEG.LEN><CTL=RST,ACK>
    情况二:非同步状态(LISTEN、SYN-SENT、SYN-RECEIVED)
  • 收到不期待的ACK时,发RST
    情况三:已同步状态(ESTABLISHED等)
  • 收到不在窗口内的段时,只回复ACK(不发RST)
  • 安全参数不匹配时,发RST并关闭连接

收到RST如何处理?

处于LISTEN状态    → 忽略RST
处于SYN-RECEIVED  → 如果来自LISTEN,回到LISTEN;否则关闭
处于其他状态      → 通知应用层,关闭连接,进入CLOSED

九、数据传输机制

核心变量

发送方维护:
  SND.UNA  = 最老的未被确认的序号(等待ACK)
  SND.NXT  = 下一个要发送的序号
  SND.WND  = 发送窗口大小(对方允许我发多少)
接收方维护:
  RCV.NXT  = 下一个期望收到的序号
  RCV.WND  = 接收窗口大小(我还能接收多少)

三个变量的关系:

  • 空闲时(所有数据都已确认):SND.UNA=SND.NXTSND.UNA = SND.NXTSND.UNA=SND.NXT
  • SND.NXT−SND.UNASND.NXT - SND.UNASND.NXTSND.UNA = 已发但未确认的数据量("在路上"的数据)

重传超时(RTO)的计算

不能用固定的超时时间,因为网络延迟变化很大。TCP使用**平滑往返时间(SRTT)**来动态计算:
SRTT=α×SRTT+(1−α)×RTTSRTT = \alpha \times SRTT + (1 - \alpha) \times RTTSRTT=α×SRTT+(1α)×RTT
其中 RTTRTTRTT 是实际测量的往返时间,α\alphaα 是平滑系数(建议 0.80.80.80.90.90.9)。
基于SRTT计算重传超时时间(RTO):
RTO=min⁡(UBOUND,max⁡(LBOUND,β×SRTT))RTO = \min(UBOUND, \max(LBOUND, \beta \times SRTT))RTO=min(UBOUND,max(LBOUND,β×SRTT))
其中:

  • UBOUNDUBOUNDUBOUND:上界(如1分钟)
  • LBOUNDLBOUNDLBOUND:下界(如1秒)
  • β\betaβ:延迟方差系数(建议 1.31.31.32.02.02.0

紧急数据(URG)

当应用层有紧急数据(如Ctrl+C信号),TCP会:

  1. 设置URG标志位为1
  2. 设置紧急指针(Urgent Pointer)= 紧急数据末尾位置相对当前序号的偏移
正常数据流:  [普通数据][普通数据][紧急数据末尾][普通数据]
                                      ^
                                 紧急指针指向这里

接收方发现 RCV.UP>RCV.NXTRCV.UP > RCV.NXTRCV.UP>RCV.NXT(紧急指针超过了当前读取位置),就进入"紧急模式",通知应用层优先处理。

窗口管理

窗口大小决定了网络中最多有多少数据在"飞行"(已发出但未确认)。
不应该做的(“缩窗”):

  • 先广告大窗口,后广告小窗口
  • 这会让对方不知道该发多少,造成混乱
    建议:
  • 接收方:等到缓冲区增加了至少20%~40%再更新窗口
  • 发送方:等窗口足够大再发,避免发很多小包
    零窗口探测:
  • 即使接收窗口为0,发送方也要每隔2分钟发1字节探测包
  • 确保接收方窗口打开后能通知发送方

十、用户接口命令

TCP提供给应用层的命令:

命令 格式 作用
OPEN OPEN(本地端口, 远端套接字, 主动/被动) 建立或等待连接
SEND SEND(连接名, 缓冲区, 字节数, PUSH, URG) 发送数据
RECEIVE RECEIVE(连接名, 缓冲区, 字节数) 接收数据
CLOSE CLOSE(连接名) 优雅关闭(发完已有数据再关)
ABORT ABORT(连接名) 强制关闭(丢弃所有数据)
STATUS STATUS(连接名) 查询连接状态

CLOSE和ABORT的区别:

  • CLOSE:我没有新数据要发了,但会把缓冲区里的数据发完
  • ABORT:立刻关闭,缓冲区数据全部丢弃,发RST

十一、完整的 C++ 代码示例

下面的代码模拟了TCP三次握手和四次挥手的状态机逻辑(非真实网络实现,用于理解协议):

#include <iostream>
#include <string>
#include <cstdint>
// ============================================================
// TCP 状态枚举
// ============================================================
enum class TcpState {
    CLOSED,
    LISTEN,
    SYN_SENT,
    SYN_RECEIVED,
    ESTABLISHED,
    FIN_WAIT_1,
    FIN_WAIT_2,
    CLOSE_WAIT,
    CLOSING,
    LAST_ACK,
    TIME_WAIT
};
// ============================================================
// TCP 控制标志位(对应头部的6个控制位)
// ============================================================
struct TcpFlags {
    bool URG = false;  // 紧急指针有效
    bool ACK = false;  // 确认号有效
    bool PSH = false;  // 推送(立刻交给应用层)
    bool RST = false;  // 重置连接
    bool SYN = false;  // 同步序列号(握手用)
    bool FIN = false;  // 没有更多数据(挥手用)
};
// ============================================================
// 模拟的 TCP 数据段结构
// ============================================================
struct TcpSegment {
    uint32_t seq;     // 序列号
    uint32_t ack;     // 确认号
    TcpFlags flags;   // 控制标志
    uint16_t window;  // 窗口大小
    std::string data; // 数据内容(简化,实际是字节流)
};
// ============================================================
// TCP 连接状态块(TCB:Transmission Control Block)
// 记录一条连接的所有状态信息
// ============================================================
struct TCB {
    TcpState state = TcpState::CLOSED;
    // 发送序列变量
    uint32_t SND_UNA = 0;  // 最老的未确认序号
    uint32_t SND_NXT = 0;  // 下一个要发的序号
    uint32_t SND_WND = 0;  // 发送窗口大小
    uint32_t ISS = 0;      // 初始发送序号
    // 接收序列变量
    uint32_t RCV_NXT = 0;  // 下一个期望收到的序号
    uint32_t RCV_WND = 1024; // 接收窗口大小
    uint32_t IRS = 0;      // 初始接收序号
};
// ============================================================
// 工具函数:将状态枚举转换为可读字符串
// ============================================================
std::string stateToString(TcpState state) {
    switch (state) {
        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";
    }
}
// ============================================================
// 工具函数:打印数据段信息
// ============================================================
void printSegment(const std::string& from, const std::string& to,
                  const TcpSegment& seg) {
    std::cout << "  " << from << " --> " << to << " : ";
    std::cout << "[SEQ=" << seg.seq << "]";
    if (seg.flags.ACK) std::cout << "[ACK=" << seg.ack << "]";
    if (seg.flags.SYN) std::cout << "[SYN]";
    if (seg.flags.ACK && !seg.flags.SYN) std::cout << "";
    if (seg.flags.FIN) std::cout << "[FIN]";
    if (seg.flags.RST) std::cout << "[RST]";
    std::cout << "[WIN=" << seg.window << "]";
    if (!seg.data.empty()) std::cout << "[DATA=\"" << seg.data << "\"]";
    std::cout << "\n";
}
// ============================================================
// 模拟三次握手
// 演示客户端(A)主动连接服务器(B)的过程
// ============================================================
void simulateThreeWayHandshake() {
    std::cout << "========================================\n";
    std::cout << "         三次握手建立连接\n";
    std::cout << "========================================\n";
    TCB clientTCB; // 客户端 A 的状态块
    TCB serverTCB; // 服务器 B 的状态块
    // ----- 服务器进入监听状态 -----
    serverTCB.state = TcpState::LISTEN;
    std::cout << "[服务器B] 被动OPEN -> 状态: " 
              << stateToString(serverTCB.state) << "\n\n";
    // ----- 第一步:客户端发送 SYN -----
    // 客户端选择初始序号 ISS = 100
    clientTCB.ISS = 100;
    clientTCB.SND_UNA = clientTCB.ISS;     // 未确认序号 = ISS
    clientTCB.SND_NXT = clientTCB.ISS + 1; // 下一个序号 = ISS+1(SYN占1个序号)
    clientTCB.state = TcpState::SYN_SENT;
    TcpSegment synSegment;
    synSegment.seq = clientTCB.ISS;  // 序号 = 100
    synSegment.flags.SYN = true;      // 设置SYN标志
    synSegment.window = clientTCB.RCV_WND;
    std::cout << "步骤1: 客户端发送SYN\n";
    printSegment("客户端A", "服务器B", synSegment);
    std::cout << "  客户端A 状态: " << stateToString(clientTCB.state) << "\n\n";
    // ----- 第二步:服务器回复 SYN+ACK -----
    // 服务器收到SYN,选择自己的ISS = 300
    serverTCB.IRS = synSegment.seq;         // 记录对方初始序号
    serverTCB.RCV_NXT = synSegment.seq + 1; // 下一个期望收到101(SYN占1个序号)
    serverTCB.ISS = 300;
    serverTCB.SND_UNA = serverTCB.ISS;
    serverTCB.SND_NXT = serverTCB.ISS + 1;
    serverTCB.state = TcpState::SYN_RECEIVED;
    TcpSegment synAckSegment;
    synAckSegment.seq = serverTCB.ISS;      // 服务器序号 = 300
    synAckSegment.ack = serverTCB.RCV_NXT;  // 确认号 = 101(期望收到101)
    synAckSegment.flags.SYN = true;
    synAckSegment.flags.ACK = true;
    synAckSegment.window = serverTCB.RCV_WND;
    std::cout << "步骤2: 服务器回复SYN+ACK\n";
    printSegment("服务器B", "客户端A", synAckSegment);
    std::cout << "  服务器B 状态: " << stateToString(serverTCB.state) << "\n\n";
    // ----- 第三步:客户端回复 ACK -----
    // 客户端收到SYN+ACK,验证ACK值是否正确
    bool ackValid = (synAckSegment.ack > clientTCB.SND_UNA) &&
                    (synAckSegment.ack <= clientTCB.SND_NXT);
    // 判断: SND.UNA(100) < SEG.ACK(101) <= SND.NXT(101) 成立
    if (ackValid) {
        clientTCB.IRS = synAckSegment.seq;          // 记录服务器初始序号300
        clientTCB.RCV_NXT = synAckSegment.seq + 1;  // 下一个期望收到301
        clientTCB.SND_UNA = synAckSegment.ack;       // 更新未确认序号=101
        clientTCB.state = TcpState::ESTABLISHED;     // 连接建立!
    }
    TcpSegment ackSegment;
    ackSegment.seq = clientTCB.SND_NXT;  // 序号 = 101
    ackSegment.ack = clientTCB.RCV_NXT;  // 确认号 = 301
    ackSegment.flags.ACK = true;
    ackSegment.window = clientTCB.RCV_WND;
    std::cout << "步骤3: 客户端发送ACK\n";
    printSegment("客户端A", "服务器B", ackSegment);
    std::cout << "  客户端A 状态: " << stateToString(clientTCB.state) << "\n";
    // 服务器收到ACK,连接建立
    serverTCB.SND_UNA = ackSegment.ack; // 注意:此处简化处理
    serverTCB.state = TcpState::ESTABLISHED;
    std::cout << "  服务器B 状态: " << stateToString(serverTCB.state) << "\n\n";
    std::cout << "*** 三次握手完成,连接建立成功!***\n\n";
}
// ============================================================
// 模拟四次挥手
// 演示客户端(A)主动关闭连接的过程
// ============================================================
void simulateFourWayHandshake() {
    std::cout << "========================================\n";
    std::cout << "         四次挥手关闭连接\n";
    std::cout << "========================================\n";
    // 假设连接已经建立,双方的序号状态如下
    TCB clientTCB;
    clientTCB.state = TcpState::ESTABLISHED;
    clientTCB.SND_NXT = 101;
    clientTCB.RCV_NXT = 301;
    TCB serverTCB;
    serverTCB.state = TcpState::ESTABLISHED;
    serverTCB.SND_NXT = 301;
    serverTCB.RCV_NXT = 101;
    // ----- 第一步:客户端发送 FIN -----
    // 应用层调用CLOSE,TCP发送FIN
    TcpSegment finSegment;
    finSegment.seq = clientTCB.SND_NXT;  // 序号 = 101
    finSegment.ack = clientTCB.RCV_NXT;  // 确认号 = 301
    finSegment.flags.FIN = true;
    finSegment.flags.ACK = true;
    finSegment.window = 1024;
    clientTCB.SND_NXT += 1; // FIN 占用一个序号
    clientTCB.state = TcpState::FIN_WAIT_1;
    std::cout << "步骤1: 客户端调用CLOSE,发送FIN\n";
    printSegment("客户端A", "服务器B", finSegment);
    std::cout << "  客户端A 状态: " << stateToString(clientTCB.state) << "\n\n";
    // ----- 第二步:服务器回复 ACK -----
    // 服务器收到FIN,确认它,进入CLOSE-WAIT(等待自己的应用层关闭)
    serverTCB.RCV_NXT = finSegment.seq + 1; // 下一个期望序号 = 102(FIN+1)
    TcpSegment ackSegment;
    ackSegment.seq = serverTCB.SND_NXT;   // 服务器序号 = 301
    ackSegment.ack = serverTCB.RCV_NXT;   // 确认号 = 102
    ackSegment.flags.ACK = true;
    ackSegment.window = 1024;
    serverTCB.state = TcpState::CLOSE_WAIT;
    std::cout << "步骤2: 服务器确认FIN(ACK)\n";
    printSegment("服务器B", "客户端A", ackSegment);
    std::cout << "  服务器B 状态: " << stateToString(serverTCB.state) << "\n";
    // 客户端收到ACK,进入FIN-WAIT-2(等待服务器的FIN)
    clientTCB.state = TcpState::FIN_WAIT_2;
    std::cout << "  客户端A 状态: " << stateToString(clientTCB.state) << "\n\n";
    // ----- 第三步:服务器发送 FIN -----
    // 服务器应用层也调用CLOSE,TCP发送FIN
    TcpSegment serverFinSegment;
    serverFinSegment.seq = serverTCB.SND_NXT; // 序号 = 301
    serverFinSegment.ack = serverTCB.RCV_NXT;  // 确认号 = 102
    serverFinSegment.flags.FIN = true;
    serverFinSegment.flags.ACK = true;
    serverFinSegment.window = 1024;
    serverTCB.SND_NXT += 1; // FIN 占用一个序号
    serverTCB.state = TcpState::LAST_ACK;
    std::cout << "步骤3: 服务器调用CLOSE,发送FIN\n";
    printSegment("服务器B", "客户端A", serverFinSegment);
    std::cout << "  服务器B 状态: " << stateToString(serverTCB.state) << "\n\n";
    // ----- 第四步:客户端回复最后的 ACK -----
    // 客户端收到服务器FIN,发最后的ACK,进入TIME-WAIT
    clientTCB.RCV_NXT = serverFinSegment.seq + 1; // = 302
    TcpSegment lastAckSegment;
    lastAckSegment.seq = clientTCB.SND_NXT;  // 序号 = 102
    lastAckSegment.ack = clientTCB.RCV_NXT;  // 确认号 = 302
    lastAckSegment.flags.ACK = true;
    lastAckSegment.window = 1024;
    clientTCB.state = TcpState::TIME_WAIT;
    std::cout << "步骤4: 客户端发送最后的ACK\n";
    printSegment("客户端A", "服务器B", lastAckSegment);
    std::cout << "  客户端A 状态: " << stateToString(clientTCB.state)
              << "(等待2MSL = 4分钟后进入CLOSED)\n";
    // 服务器收到最后ACK,关闭
    serverTCB.state = TcpState::CLOSED;
    std::cout << "  服务器B 状态: " << stateToString(serverTCB.state) << "\n\n";
    std::cout << "*** 四次挥手完成,连接关闭!***\n\n";
}
// ============================================================
// 演示序列号有效性检查
// ============================================================
void demonstrateSequenceCheck() {
    std::cout << "========================================\n";
    std::cout << "         序列号有效性检查演示\n";
    std::cout << "========================================\n";
    // 假设接收方状态
    uint32_t RCV_NXT = 500;  // 期望收到序号500
    uint32_t RCV_WND = 100;  // 接收窗口大小100(接受500~599)
    std::cout << "接收窗口: RCV.NXT=" << RCV_NXT
              << ", RCV.WND=" << RCV_WND
              << ", 有效范围=[" << RCV_NXT << ", "
              << RCV_NXT + RCV_WND - 1 << "]\n\n";
    // 测试几个数据段
    struct TestCase {
        uint32_t seq;
        uint32_t len;
        std::string description;
    };
    TestCase cases[] = {
        {500, 10, "正常情况:段从500开始,长度10"},
        {490, 15, "部分重叠:段开始于490,结尾在505(窗口内)"},
        {590, 20, "跨越右边界:段开始590,结尾在609"},
        {400, 50, "完全在窗口左侧:旧数据"},
        {610,  5, "完全在窗口右侧:太新的数据"},
    };
    for (auto& tc : cases) {
        uint32_t segEnd = tc.seq + tc.len - 1;
        // 判断段的开头是否在窗口内
        bool startInWindow = (tc.seq >= RCV_NXT) &&
                             (tc.seq < RCV_NXT + RCV_WND);
        // 判断段的结尾是否在窗口内
        bool endInWindow   = (segEnd >= RCV_NXT) &&
                             (segEnd < RCV_NXT + RCV_WND);
        bool acceptable = (tc.len > 0) ? (startInWindow || endInWindow)
                                        : (tc.seq == RCV_NXT); // 零长度段
        std::cout << "测试: " << tc.description << "\n";
        std::cout << "  SEG.SEQ=" << tc.seq << ", SEG.LEN=" << tc.len
                  << ", 段末=" << segEnd << "\n";
        std::cout << "  开头在窗口内: " << (startInWindow ? "是" : "否")
                  << ", 结尾在窗口内: " << (endInWindow ? "是" : "否") << "\n";
        std::cout << "  结果: " << (acceptable ? "[接受]" : "[丢弃]") << "\n\n";
    }
}
// ============================================================
// 演示平滑往返时间(SRTT)和重传超时(RTO)计算
// ============================================================
void demonstrateRTOCalculation() {
    std::cout << "========================================\n";
    std::cout << "    重传超时(RTO)动态计算演示\n";
    std::cout << "========================================\n";
    // 参数设置
    const double ALPHA  = 0.875; // 平滑系数(RFC推荐7/8 = 0.875)
    const double BETA   = 2.0;   // 延迟方差系数
    const double LBOUND = 1.0;   // RTO下界:1秒
    const double UBOUND = 60.0;  // RTO上界:60秒
    // 模拟测量到的RTT值(单位:秒)
    double rttSamples[] = {0.5, 0.3, 0.8, 0.4, 1.2, 0.2, 0.6};
    int sampleCount = sizeof(rttSamples) / sizeof(rttSamples[0]);
    // 初始SRTT用第一个RTT样本
    double SRTT = rttSamples[0];
    std::cout << "参数: ALPHA=" << ALPHA << ", BETA=" << BETA
              << ", LBOUND=" << LBOUND << "s, UBOUND=" << UBOUND << "s\n\n";
    std::cout << "初始SRTT = " << SRTT << "s(用第一个RTT样本)\n\n";
    for (int i = 1; i < sampleCount; i++) {
        double RTT = rttSamples[i];
        // 更新平滑往返时间:SRTT = α*SRTT + (1-α)*RTT
        SRTT = ALPHA * SRTT + (1.0 - ALPHA) * RTT;
        // 计算重传超时:RTO = min(UBOUND, max(LBOUND, β*SRTT))
        double RTO_raw = BETA * SRTT;
        double RTO = std::min(UBOUND, std::max(LBOUND, RTO_raw));
        std::cout << "测量RTT=" << RTT << "s"
                  << " -> 更新SRTT=" << SRTT
                  << "s -> β*SRTT=" << RTO_raw
                  << "s -> RTO=" << RTO << "s\n";
    }
    std::cout << "\n";
}
// ============================================================
// 主函数:运行所有演示
// ============================================================
int main() {
    // 演示1:三次握手
    simulateThreeWayHandshake();
    // 演示2:四次挥手
    simulateFourWayHandshake();
    // 演示3:序列号有效性检查
    demonstrateSequenceCheck();
    // 演示4:RTO计算
    demonstrateRTOCalculation();
    return 0;
}

运行结果示意

========================================
         三次握手建立连接
========================================
[服务器B] 被动OPEN -> 状态: LISTEN
步骤1: 客户端发送SYN
  客户端A --> 服务器B : [SEQ=100][SYN][WIN=1024]
  客户端A 状态: SYN-SENT
步骤2: 服务器回复SYN+ACK
  服务器B --> 客户端A : [SEQ=300][ACK=101][SYN][WIN=1024]
  服务器B 状态: SYN-RECEIVED
步骤3: 客户端发送ACK
  客户端A --> 服务器B : [SEQ=101][ACK=301][WIN=1024]
  客户端A 状态: ESTABLISHED
  服务器B 状态: ESTABLISHED
*** 三次握手完成,连接建立成功!***

十二、总结:TCP 核心设计思想

可靠性保证的三大机制:
1. 序列号 + 确认号
   每个字节都有编号,接收方确认收到了哪些字节
   丢包了就重发,乱序了就重排
2. 滑动窗口(流量控制)
   接收方告诉发送方自己还能接收多少
   防止发送太快把接收方淹没
3. 超时重传
   用SRTT动态计算RTO,自适应网络状况
   超时未确认就重发
连接管理的两大机制:
建立:三次握手
  ↳ 双方互相确认对方的初始序号
  ↳ 防止旧的重复连接请求引起混乱
关闭:四次挥手
  ↳ 每个方向独立关闭(半双工)
  ↳ TIME-WAIT等待2MSL确保最后ACK到达
Logo

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

更多推荐