RFC791-IP学习:RFC 791:互联网协议(IP)详解
本文基于1981年9月发布的 RFC 791(互联网协议规范),从零开始用中文讲解 IP 协议的设计思想与工作机制。
一、背景与动机
IP协议是什么?
IP(Internet Protocol,互联网协议)是整个互联网的"地址+投递"系统。
一句话理解: 就像快递公司,IP负责把一包数据从发送方送到接收方,至于路上丢没丢、顺序对不对,它不管。
RFC 791 是 IP 协议的官方规范文档,1981年由 Jon Postel 编写,定义了我们今天仍在使用的 IPv4 协议基础。
这套系统最初叫做 catenet(互联网络),设计目标是让不同类型的网络能够互相连通,就像不同国家的邮政系统通过标准信封格式协作投递信件一样。
二、IP协议的范围与定位
IP只做两件事
IP协议的职责非常明确,只做两件核心事情:
- 寻址(Addressing):知道数据从哪来、到哪去
- 分片与重组(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 = 14801500−20=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+协议号}
重组步骤:
- 收集所有 ID 相同的分片
- 按 Fragment Offset 排序
- MF=0 的那片是最后一片
- 拼接所有分片数据,还原原始内容
重组示意:
收到分片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 | 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{ 字节}最大值=216−1=65535 字节
两个重要约定:
- 所有主机必须能接受最大 576 字节的数据包(无论是整包还是分片后的)
- 576 = 512字节数据 + 64字节头部,是早期互联网的安全基线
实际含义: 如果你不确定目标主机能否接收大包,就别发超过576字节的包。
2.5 Identification(标识)— 16 位
发送方给这个数据包分配的唯一编号,主要用于分片重组时识别哪些碎片属于同一个原始包。
标识范围=0∼65535(216 种值)\text{标识范围} = 0 \sim 65535 \quad (2^{16} \text{ 种值})标识范围=0∼65535(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{ 字节}最大偏移=(213−1)×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=28−1=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=⌊8MTU−IHL×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{ 分钟}TTLmax≈255 秒≈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=⌊8280−20⌋=⌊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;
}
八、知识结构图
九、分片流程图
十、重组流程图
十一、关键数值速查
| 字段 | 位数 | 最小值 | 最大值 | 备注 |
|---|---|---|---|---|
| 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)。
数据在系统中的流动路径
从发送进程到接收进程,数据经历了如下旅程:
网关处理数据的三个步骤:
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 | 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位整数,范围从 000 到 232−12^{32} - 1232−1(约43亿)。用完之后从0重新开始(模 2322^{32}232 运算)。
序列号范围=[0,232−1]\text{序列号范围} = [0, 2^{32} - 1]序列号范围=[0,232−1]
序列空间的四个区域
发送序列空间示意图:
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.ACK≤SND.NXT
意思是:ACK号必须比最老的未确认序号大,同时不超过下一个要发送的序号。
判断一个收到的数据段是否有效
数据段在接收窗口内,满足以下任一条件:
RCV.NXT≤SEG.SEQ<RCV.NXT+RCV.WNDRCV.NXT \leq SEG.SEQ < RCV.NXT + RCV.WNDRCV.NXT≤SEG.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.WND或RCV.NXT≤SEG.SEQ+SEG.LEN−1<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.NXT≤SEG.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μs≈4.55 小时
只要网络中数据包的最大生存时间(MSL = 2分钟)远小于4.55小时,就不会重复。
四、TCP 连接状态机
TCP连接在生命周期内会经历多个状态:
各状态含义
| 状态 | 含义 |
|---|---|
| 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.NXT−SND.UNA = 已发但未确认的数据量("在路上"的数据)
重传超时(RTO)的计算
不能用固定的超时时间,因为网络延迟变化很大。TCP使用**平滑往返时间(SRTT)**来动态计算:
SRTT=α×SRTT+(1−α)×RTTSRTT = \alpha \times SRTT + (1 - \alpha) \times RTTSRTT=α×SRTT+(1−α)×RTT
其中 RTTRTTRTT 是实际测量的往返时间,α\alphaα 是平滑系数(建议 0.80.80.8 到 0.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.3 到 2.02.02.0)
紧急数据(URG)
当应用层有紧急数据(如Ctrl+C信号),TCP会:
- 设置URG标志位为1
- 设置紧急指针(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到达
更多推荐


所有评论(0)