TCP/ip详解=TCP--连接管理
·
<?php
/**
* ===================================================================
* TCP 完整指南 (274-320)
* 大白话解释 + PHP Sockets 代码示例
* ===================================================================
*
* 运行环境: Windows/Linux, PHP 7.4+ (需要 sockets 扩展)
* 确保开启: extension=sockets (php.ini)
*
* 本文件可以分段运行,每个章节独立。
* php tcp_complete_guide.php
*/
// ===================================================================
// 第〇部分: 前置知识 —— 创建原始套接字来"看"TCP首部
// ===================================================================
echo "╔══════════════════════════════════════════════════╗\n";
echo "║ TCP 完整指南 —— 274~320 大白话 + PHP代码 ║\n";
echo "╚══════════════════════════════════════════════════╝\n\n";
// 辅助函数: 打印分隔线
function section(string $num, string $title): void {
echo "\n" . str_repeat("━", 60) . "\n";
echo " {$num}. {$title}\n";
echo str_repeat("━", 60) . "\n";
}
// 辅助函数: 检查 sockets 扩展
function check_sockets_ext(): void {
if (!extension_loaded('sockets')) {
die("❌ 请先开启 sockets 扩展: php.ini 中 extension=sockets\n");
}
}
// ===================================================================
// 274. TCP首部格式
// ===================================================================
section("274", "TCP首部格式");
/*
* 【大白话】
* TCP首部就是TCP数据包最前面那块"身份证"。
* 每个TCP包 = 首部(20~60字节) + 数据(body)。
*
* TCP首部长这样(每个格子是一个字段):
*
* 0 8 16 24 31
* ┌───────────────┬───────────────┬───────────────┬───────────────┐
* │ 源端口 (16位) │ 目的端口 (16位) │
* ├───────────────┴───────────────┼───────────────┴───────────────┤
* │ 序列号 (32位) │
* ├───────────────────────────────┼───────────────────────────────┤
* │ 确认号 (32位) │
* ├───────┬───────┬───┬───────────┼───────┬───────────┬───────────┤
* │数据偏移│ 保留 │N C│ 标志位 │ 窗口大小 │ │
* │ (4位) │ (3位) │S E│ (9位) │ (16位) │ │
* ├───────┴───────┴───┴───────────┼───────┴───────────┤ │
* │ 检验和 (16位) │ 紧急指针 (16位) │ │
* ├───────────────────────────────┴───────────────────┤ │
* │ 选项 (0~40字节,可变长) │ │
* └───────────────────────────────────────────────────┘
*
* 固定首部 = 20字节(必选),选项最多再加40字节,所以总共20~60字节。
*/
// PHP中无法直接构造TCP首部(这是内核干的活),
// 但我们可以用 raw socket 发送自定义的TCP包来"触摸"首部。
// 下面演示如何拼出一个TCP首部的二进制结构:
function build_tcp_header(
int $src_port, // 源端口
int $dst_port, // 目的端口
int $seq, // 序列号
int $ack, // 确认号
int $data_offset, // 数据偏移(首部长度/4)
int $flags, // 标志位
int $window, // 窗口大小
int $checksum = 0, // 检验和(先填0)
int $urgent = 0 // 紧急指针
): string {
// pack() 把数字按指定格式打包成二进制
// N = 32位无符号大端, n = 16位无符号大端
// C = 8位无符号, J = 32位无符号大端(同N)
$header = '';
$header .= pack('n', $src_port); // 源端口 16位
$header .= pack('n', $dst_port); // 目的端口 16位
$header .= pack('N', $seq); // 序列号 32位
$header .= pack('N', $ack); // 确认号 32位
// 数据偏移(高4位) + 保留位(3位,填0) + 标志位(低9位)
// 高4位 = data_offset << 4, 低12位 = flags (实际标志位只用9位)
$offset_and_flags = ($data_offset << 12) | ($flags & 0x0FFF);
$header .= pack('n', $offset_and_flags); // 2字节
$header .= pack('n', $window); // 窗口 16位
$header .= pack('n', $checksum); // 检验和 16位
$header .= pack('n', $urgent); // 紧急指针 16位
return $header; // 返回完整的20字节TCP首部
}
// 验证一下长度
$test_header = build_tcp_header(8080, 80, 100, 200, 5, 0, 65535);
echo "✅ 构造的TCP首部长度: " . strlen($test_header) . " 字节 (应该是20)\n";
echo " 二进制(hex): " . bin2hex($test_header) . "\n";
echo " 解析: 源端口=" . unpack('n', substr($test_header,0,2))[1];
echo " 目的端口=" . unpack('n', substr($test_header,2,2))[1] . "\n";
// ===================================================================
// 275. TCP源端口与目的端口
// ===================================================================
section("275", "TCP源端口与目的端口");
/*
* 【大白话】
* 端口就是"门牌号"。
* 源端口 = 你自己开的门,目的端口 = 你要去找的那扇门。
*
* 比如你浏览器访问百度:
* 源端口: 52341 (系统随机分配的一个临时端口)
* 目的端口: 443 (HTTPS的标准端口)
*
* 百度回你的时候:
* 源端口: 443 → 目的端口: 52341 (正好反过来)
*
* 端口范围: 0~65535 (16位,2¹⁶ = 65536)
* 0~1023: 知名端口 (需要管理员权限)
* 1024~49151: 注册端口
* 49152~65535: 临时/动态端口 (系统自动分配)
*/
check_sockets_ext();
// ---- 客户端: 连接到服务器,看看实际分配的端口 ----
echo "\n📌 示例: 创建TCP连接,观察源端口和目的端口\n\n";
// 创建一个TCP socket
$client = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if ($client === false) {
echo "创建socket失败: " . socket_strerror(socket_last_error()) . "\n";
} else {
echo "✅ TCP socket 创建成功\n";
// 获取本地地址(源端口)
socket_getsockname($client, $local_addr, $local_port);
echo " 本地地址(源): {$local_addr}:{$local_port}\n";
echo " ↑ 源端口 {$local_port} 就是系统自动分配的临时端口\n";
socket_close($client);
}
// ---- 服务端: 绑定指定端口 ----
echo "\n📌 示例: 服务端绑定固定端口\n\n";
$server = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($server, SOL_SOCKET, SO_REUSEADDR, 1);
socket_bind($server, '127.0.0.1', 8080);
socket_getsockname($server, $addr, $port);
echo "✅ 服务端绑定到: {$addr}:{$port}\n";
echo " 这个 8080 就是目的端口,客户端要连这个端口\n";
socket_close($server);
// ---- 端口范围说明 ----
echo "\n📌 端口分类:\n";
echo " 知名端口 (0-1023): HTTP=80, HTTPS=443, SSH=22, FTP=21\n";
echo " 注册端口 (1024-49151): MySQL=3306, Redis=6379, Tomcat=8080\n";
echo " 动态端口 (49152-65535): 系统自动分配的临时端口\n";
// ===================================================================
// 276. 序列号 (Sequence Number)
// ===================================================================
section("276", "序列号 (Sequence Number)");
/*
* 【大白话】
* 序列号就是给每个字节编个号。
* 假设你要发送一句话"Hello",那么:
* 第1个字节 H → 序列号=100
* 第2个字节 e → 序列号=101
* 第3个字节 l → 序列号=102
* ...
* TCP首部里写的序列号 = 这段数据的第一个字节的编号。
*
* 序列号是32位的,范围 0 ~ 4,294,967,295 (约42亿)。
* 用完了就绕回到0继续用。
*
* 初始序列号(ISN)是随机的,不固定从0开始,
* 这是为了防止跟历史连接的数据搞混。
*
* 序列号的作用:
* 1. 给数据排序 —— 网络可能先发后到
* 2. 去重 —— 同一个包发了两遍,序列号一样就丢掉
* 3. 可靠性 —— 接收方通过序列号告诉发送方"我收到了哪些"
*/
echo "\n📌 序列号的本质: 字节编号\n";
function demonstrate_sequence_number(): void {
$data = "HelloTCP"; // 8个字节
$isn = 1000; // 假设初始序列号是1000
echo "数据: \"{$data}\" (8字节)\n";
echo "初始序列号(ISN): {$isn}\n\n";
echo "每个字节的序列号:\n";
for ($i = 0; $i < strlen($data); $i++) {
echo " 字节[{$i}] = '{$data[$i]}' → 序列号=" . ($isn + $i) . "\n";
}
echo "\n";
echo "如果分两段发送:\n";
// 第一段: 前5个字节 "Hello"
echo " 第一段 TCP包: SEQ={$isn}, 数据=\"Hello\" (5字节)\n";
// 第二段: 后3个字节 "TCP"
$next_seq = $isn + 5;
echo " 第二段 TCP包: SEQ={$next_seq}, 数据=\"TCP\" (3字节)\n";
echo " ↑ 第二段的SEQ = 第一段SEQ + 第一段数据长度\n";
}
demonstrate_sequence_number();
// ===================================================================
// 277. 确认号 (Acknowledgment Number)
// ===================================================================
section("277", "确认号 (Acknowledgment Number)");
/*
* 【大白话】
* 确认号 = "我期望收到的下一个字节的序列号"。
* 等于: 对方发来的最后一个字节的序列号 + 1
*
* 比如你收到对方发来的数据: SEQ=100, 数据长度=20 字节
* 那你回的ACK包里,确认号 = 100 + 20 = 120
* 意思是"120号之前的我都收到了,你该发120了"。
*
* 注意: 只有ACK标志位=1时,确认号字段才有效。
*/
echo "\n📌 确认号的逻辑: 告诉你我收到了多少\n";
function demonstrate_ack_number(): void {
// 模拟通信
// 发送方 → 接收方: SEQ=1000, 数据=20字节
// 接收方 → 发送方: ACK=1020 (表示1000~1019都收到了)
$sent_seq = 1000;
$data_len = 20;
$expected_ack = $sent_seq + $data_len;
echo "发送方发出: SEQ={$sent_seq}, 数据长度={$data_len}字节\n";
echo "接收方收到数据后回复:\n";
echo " ACK={$expected_ack} (={$sent_seq}+{$data_len})\n";
echo " 含义: \"{$expected_ack}号之前的字节我都收到了,下次请从{$expected_ack}开始发\"\n";
// 确认号的累加
echo "\n多次交互演示:\n";
$seq = 5000;
foreach ([10, 15, 8, 20] as $i => $len) {
$ack = $seq + $len;
echo " 第" . ($i+1) . "次: 发SEQ={$seq}, 长度={$len} → 回复ACK={$ack}\n";
$seq = $ack;
}
}
demonstrate_ack_number();
// ===================================================================
// 278. 数据偏移 (Data Offset)
// ===================================================================
section("278", "数据偏移 (Data Offset)");
/*
* 【大白话】
* "数据偏移"这个名字起得不好,其实就是"首部长度"。
* 它告诉接收方: TCP首部有多长,数据从哪个位置开始。
*
* 这个字段只有4位,最大值是15。
* 但是TCP首部最长60字节,15怎么表达60呢?
* 答案是: 单位是"4字节"。
* 所以值=5 → 5×4=20字节(最小),值=15 → 15×4=60字节(最大)
*
* 为什么叫"偏移"不叫"长度"?
* 因为它是从TCP段开头到数据开始处的偏移量。
*/
echo "\n📌 数据偏移 = 首部长度 ÷ 4\n";
function demonstrate_data_offset(): void {
echo "数据偏移字段(4位)的取值范围: 5 ~ 15\n\n";
$examples = [
5 => "最小TCP首部,20字节,没有选项字段",
6 => "24字节首部,有4字节选项",
7 => "28字节首部,有8字节选项",
8 => "32字节首部,有12字节选项(含MSS=4, SACK=8)",
10 => "40字节首部,有20字节选项",
15 => "60字节首部(最大值),有40字节选项",
];
foreach ($examples as $offset => $desc) {
$header_len = $offset * 4;
echo " 数据偏移={$offset} → 首部长度={$header_len}字节 → {$desc}\n";
}
echo "\n在你的TCP首部构造函数中:\n";
echo " build_tcp_header(..., \$data_offset, ...)\n";
echo " 如果要20字节首部,传 \$data_offset=5\n";
echo " 如果要32字节首部,传 \$data_offset=8\n\n";
// 演示如何从二进制中提取数据偏移
echo "从TCP首部字节中解析数据偏移:\n";
$header = build_tcp_header(80, 443, 100, 200, 7, 0, 65535);
$byte12_13 = unpack('n', substr($header, 12, 2))[1];
$offset = ($byte12_13 >> 12) & 0x0F; // 取高4位
$hlen = $offset * 4;
echo " 首部第13字节高4位 = {$offset} → 首部长度 = {$hlen}字节\n";
}
demonstrate_data_offset();
// ===================================================================
// 279. TCP标志位 (Flags)
// ===================================================================
section("279", "TCP标志位 (Flags)");
/*
* 【大白话】
* TCP首部里有9个标志位,每个就像是一个开关,用来控制TCP的行为。
* 这9个标志位合起来占用第14字节的低6位 + 第13字节的最高位(NS)。
*
* 从高到低排列:
* NS CWR ECE URG ACK PSH RST SYN FIN
*
* 最常用的是这6个:
* SYN → 建立连接
* ACK → 确认收到
* FIN → 结束连接
* RST → 重置连接(出错了!)
* PSH → 赶紧推给应用程序,别缓存了
* URG → 有紧急数据
*
* 另外3个跟拥塞控制有关:
* CWR → 拥塞窗口减小(Congestion Window Reduced)
* ECE → 收到拥塞通知(ECN Echo)
* NS → ECN-nonce,防止ECN欺骗
*/
// 定义标志位的二进制值
define('TCP_FIN', 0x01); // 0000 0001
define('TCP_SYN', 0x02); // 0000 0010
define('TCP_RST', 0x04); // 0000 0100
define('TCP_PSH', 0x08); // 0000 1000
define('TCP_ACK', 0x10); // 0001 0000
define('TCP_URG', 0x20); // 0010 0000
define('TCP_ECE', 0x40); // 0100 0000
define('TCP_CWR', 0x80); // 1000 0000
// NS 不在低8位,在第12字节的高位
echo "\n📌 TCP标志位定义:\n";
echo " FIN=0x01 (1) SYN=0x02 (2) RST=0x04 (4)\n";
echo " PSH=0x08 (8) ACK=0x10 (16) URG=0x20 (32)\n";
echo " ECE=0x40 (64) CWR=0x80 (128)\n\n";
echo "常见组合:\n";
echo " SYN = 0x02 → 三次握手第一步\n";
echo " SYN|ACK = 0x12 → 三次握手第二步\n";
echo " ACK = 0x10 → 普通确认包\n";
echo " FIN|ACK = 0x11 → 四次挥手\n";
echo " RST = 0x04 → 重置连接\n";
echo " RST|ACK = 0x14 → 带确认的重置\n";
echo " FIN|PSH|ACK = 0x19 → 最后一段数据+结束\n";
// 解析标志位的辅助函数
function parse_flags(int $flags): array {
return [
'FIN' => (bool)($flags & TCP_FIN),
'SYN' => (bool)($flags & TCP_SYN),
'RST' => (bool)($flags & TCP_RST),
'PSH' => (bool)($flags & TCP_PSH),
'ACK' => (bool)($flags & TCP_ACK),
'URG' => (bool)($flags & TCP_URG),
];
}
function flags_to_string(int $flags): string {
$names = [];
if ($flags & TCP_FIN) $names[] = 'FIN';
if ($flags & TCP_SYN) $names[] = 'SYN';
if ($flags & TCP_RST) $names[] = 'RST';
if ($flags & TCP_PSH) $names[] = 'PSH';
if ($flags & TCP_ACK) $names[] = 'ACK';
if ($flags & TCP_URG) $names[] = 'URG';
return $names ? implode('|', $names) : 'NONE';
}
echo "\n📌 标志位解析示例:\n";
foreach ([TCP_SYN, TCP_SYN|TCP_ACK, TCP_ACK, TCP_FIN|TCP_ACK, TCP_RST] as $f) {
echo " 0x" . dechex($f) . " = " . flags_to_string($f) . "\n";
}
// ===================================================================
// 280. SYN标志
// ===================================================================
section("280", "SYN标志");
/*
* 【大白话】
* SYN = Synchronize = "同步"
* 它的作用就一个: 建立连接。
*
* SYN=1 的包只能在TCP三次握手的时候出现。
* 正常数据传输时 SYN 永远是 0。
*
* SYN包的特点:
* 1. 会携带初始序列号(ISN)
* 2. SYN=1的包即使没有数据,也会消耗一个序列号
* 3. 服务端收到SYN后会回复SYN+ACK
*/
echo "\n📌 SYN包的作用: 发起连接请求\n";
function syn_example(): void {
// 客户端发起连接 → SYN=1, SEQ=x
$client_isn = rand(0, 4294967295);
echo "【三次握手 - 第一步】\n";
echo "客户端 → 服务端: SYN=1, SEQ={$client_isn}\n";
echo " 含义: \"嗨,我想跟你建立连接,我的起始序列号是{$client_isn}\"\n\n";
// 服务端回复 → SYN=1, ACK=1, SEQ=y, ACK=x+1
$server_isn = rand(0, 4294967295);
echo "【三次握手 - 第二步】\n";
echo "服务端 → 客户端: SYN=1, ACK=1, SEQ={$server_isn}, ACK=" . ($client_isn+1) . "\n";
echo " 含义: \"收到,我也要跟你建立连接,我的起始序列号是{$server_isn}\"\n\n";
echo "注意: SYN=1的时候,序列号就算没有数据也会被消耗1个\n";
echo " ACK号 = 对方SEQ + 1 (这个+1就是SYN消耗的那个序列号)\n";
}
syn_example();
// ===================================================================
// 281. ACK标志
// ===================================================================
section("281", "ACK标志");
/*
* 【大白话】
* ACK = Acknowledgment = "确认收到了"
* 这是TCP里最常用的标志位。
*
* 连接建立之后(除了第一个SYN包),几乎所有TCP包都有ACK=1。
* ACK=1 表示"确认号字段里的值是有效的"。
*
* 注意区分:
* ACK标志位 = 一个开关,=1表示这个包是确认包
* ACK号(确认号) = 数值,告诉对方我收到了哪些数据
*/
echo "\n📌 ACK标志: 几乎所有数据包都会带\n\n";
function ack_example(): void {
echo "正常数据传输中:\n";
echo " 发送方 → 接收方: SEQ=100, ACK=500, 数据=\"Hello\"\n";
echo " ACK=500表示: \"你发到499号的数据我都收到了\"\n";
echo " SEQ=100表示: \"我现在发给你的是从100号开始的数据\"\n\n";
echo " 接收方 → 发送方: SEQ=500, ACK=105, 数据=\"\"(纯ACK)\n";
echo " ACK=105表示: \"100~104的数据我收到了\"\n\n";
echo "注意: 第一个SYN包是没有ACK标志的:\n";
echo " SYN包: ACK=0 (不确认任何东西,因为还没建立连接)\n";
echo " SYN+ACK包: ACK=1 (服务端确认收到了客户端的SYN)\n";
echo " 数据传输包: ACK=1 (几乎所有都是)\n";
}
ack_example();
// ---- PHP代码: 观察实际TCP连接中的ACK ----
echo "\n📌 PHP代码: 实现一个简单的TCP服务器和客户端,观察ACK\n\n";
function run_echo_server_client(): void {
// 创建服务端
$server = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($server, SOL_SOCKET, SO_REUSEADDR, 1);
socket_bind($server, '127.0.0.1', 9090);
socket_listen($server, 1);
socket_set_nonblock($server); // 非阻塞
// 创建客户端
$client = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_connect($client, '127.0.0.1', 9090);
// 服务端接受连接
$tries = 0;
while (!($conn = @socket_accept($server)) && $tries < 100) {
usleep(10000); // 等10ms
$tries++;
}
socket_set_block($conn);
if (!isset($conn) || $conn === false) {
echo " ⚠️ 连接接受失败\n";
socket_close($client);
socket_close($server);
return;
}
// 客户端发送数据 (带ACK标志)
$msg = "Hello Server!";
socket_write($client, $msg, strlen($msg));
echo " 客户端 → 服务端: \"{$msg}\" (内核自动加上ACK标志)\n";
// 服务端接收数据
$buf = '';
socket_recv($conn, $buf, 1024, 0);
echo " 服务端收到: \"{$buf}\"\n";
echo " ↑ 服务端内核会自动回复ACK确认包(你看不到,内核处理的)\n";
// 服务端回复
$reply = "Hello Client!";
socket_write($conn, $reply, strlen($reply));
echo " 服务端 → 客户端: \"{$reply}\" (内核自动加上ACK标志)\n";
// 客户端接收
$buf2 = '';
socket_recv($client, $buf2, 1024, 0);
echo " 客户端收到: \"{$buf2}\"\n";
echo " ↑ 客户端内核也会自动回复ACK确认包\n";
socket_close($conn);
socket_close($client);
socket_close($server);
}
run_echo_server_client();
// ===================================================================
// 282. FIN标志
// ===================================================================
section("282", "FIN标志");
/*
* 【大白话】
* FIN = Finish = "我说完了"
* 当一方不再发送数据时,就发一个FIN=1的包,表示"我这边数据发完了"。
*
* 注意: FIN只表示"我不发了",不代表"我也不收了"。
* 对方还可以继续发数据过来(这就是半关闭状态)。
*
* FIN=1也消耗一个序列号,跟SYN一样。
*/
echo "\n📌 FIN标志: 优雅地结束连接\n\n";
function fin_example(): void {
echo "主动关闭方发出FIN:\n";
echo " 发送方 → 接收方: FIN=1, ACK=1, SEQ=u, ACK=v\n";
echo " 含义: \"我这边数据发完了,想关连接了\"\n\n";
echo "被动关闭方收到FIN后:\n";
echo " 接收方 → 发送方: ACK=1, SEQ=v, ACK=u+1\n";
echo " 含义: \"知道了,我收到了你的关闭请求\"\n\n";
echo " (这时发送方→接收方的方向已经关了,但接收方还可以继续发数据)\n\n";
echo " 接收方 → 发送方: FIN=1, ACK=1, SEQ=w, ACK=u+1\n";
echo " 含义: \"我这边数据也发完了,我也要关\"\n\n";
echo " 发送方 → 接收方: ACK=1, SEQ=u+1, ACK=w+1\n";
echo " 含义: \"好的,知道了\"\n";
echo " 发送方进入 TIME_WAIT,等2MSL(约60秒)后彻底关闭\n";
}
fin_example();
// ---- PHP代码: 演示FIN ----
echo "\n📌 PHP代码: 观察FIN流程\n\n";
function fin_demonstration(): void {
$server = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($server, SOL_SOCKET, SO_REUSEADDR, 1);
socket_bind($server, '127.0.0.1', 9091);
socket_listen($server, 1);
socket_set_nonblock($server);
$client = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_connect($client, '127.0.0.1', 9091);
$tries = 0;
while (!($conn = @socket_accept($server)) && $tries < 100) {
usleep(10000);
$tries++;
}
socket_set_block($conn);
if (!isset($conn) || $conn === false) {
socket_close($client);
socket_close($server);
return;
}
// 客户端发送数据
socket_write($client, "data1", 5);
$buf = '';
socket_recv($conn, $buf, 1024, 0);
echo " 服务端收到: \"{$buf}\"\n";
// 客户端主动关闭 (发送FIN)
socket_shutdown($client, 1); // SHUT_WR = 关闭写端 = 发送FIN
echo " 客户端 → 服务端: FIN (通过 socket_shutdown 发送)\n";
echo " ↑ socket_shutdown(\$sock, 1) 就是告诉内核\"我不发了\"\n";
echo " 内核会自动发送一个 FIN 包给对方\n";
// 服务端读 → 会读到0字节(EOF),表示对方发完了
$buf2 = '';
$n = @socket_recv($conn, $buf2, 1024, 0);
if ($n === 0) {
echo " 服务端读到0字节 → 说明客户端已经关闭了写端(FIN已收到)\n";
}
// 服务端还可以继续发数据(半关闭状态)
socket_write($conn, "reply", 5);
echo " 服务端 → 客户端: \"reply\" (半关闭状态,服务端还能发)\n";
$buf3 = '';
socket_recv($client, $buf3, 1024, 0);
echo " 客户端收到: \"{$buf3}\"\n";
socket_close($conn);
socket_close($client);
socket_close($server);
}
fin_demonstration();
// ===================================================================
// 283. RST标志
// ===================================================================
section("283", "RST标志");
/*
* 【大白话】
* RST = Reset = "重置/复位"
* 这是TCP里的"紧急按钮",表示连接出问题了,直接重置。
*
* RST和FIN的区别:
* FIN = 正常关门,握手告别
* RST = 一脚踹开门,连接直接死掉
*
* RST常见出现场景:
* 1. 连接请求到达一个没有在监听的端口 → 回复RST
* 2. 连接已关闭,但对方还在发数据 → 回复RST
* 3. 一方崩溃重启后收到旧连接的数据 → 回复RST
* 4. 半打开连接 → RST来恢复
* 5. SO_LINGER设为0时关闭socket → 发RST而不是FIN
*/
echo "\n📌 RST标志: TCP的紧急重置按钮\n\n";
function rst_example(): void {
echo "RST出现的典型场景:\n\n";
echo "场景1: 连接到一个没有程序监听的端口\n";
echo " 客户端 → SYN → 服务端的某个端口\n";
echo " 服务端 → RST → 客户端 (\"这里没人!\")\n\n";
echo "场景2: 连接断开后对方还在发数据\n";
echo " 客户端已关闭连接\n";
echo " 服务端还在发数据 → 客户端回复RST (\"连接已死!\")\n\n";
echo "场景3: 使用 SO_LINGER 发RST\n";
echo " socket_set_option(\$sock, SOL_SOCKET, SO_LINGER, ['l_onoff'=>1, 'l_linger'=>0]);\n";
echo " socket_close(\$sock); // 不发FIN,直接发RST!\n";
}
rst_example();
// ---- PHP代码: 触发RST ----
echo "\n📌 PHP代码: 故意触发RST\n\n";
function trigger_rst(): void {
// 连接到没人在听的端口 → 应该收到 RST (连接被拒绝)
$sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
echo "尝试连接 127.0.0.1:19999 (没有程序在监听)...\n";
if (@socket_connect($sock, '127.0.0.1', 19999)) {
echo " 连接成功? 不太可能...\n";
} else {
$err = socket_last_error($sock);
echo " 连接失败: " . socket_strerror($err) . "\n";
echo " ↑ 内核收到了RST包,所以告诉你\"Connection refused\"\n";
}
socket_close($sock);
// 用SO_LINGER发RST而不是FIN
echo "\n使用SO_LINGER强制发RST:\n";
$server = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($server, SOL_SOCKET, SO_REUSEADDR, 1);
socket_bind($server, '127.0.0.1', 9092);
socket_listen($server, 1);
socket_set_nonblock($server);
$client = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_connect($client, '127.0.0.1', 9092);
$tries = 0;
while (!($conn = @socket_accept($server)) && $tries < 100) {
usleep(10000);
$tries++;
}
if (isset($conn) && $conn !== false) {
// 设置 SO_LINGER: l_onoff=1, l_linger=0 → close时发RST
socket_set_option($conn, SOL_SOCKET, SO_LINGER, ['l_onoff' => 1, 'l_linger' => 0]);
echo " 已设置 SO_LINGER (l_onoff=1, l_linger=0)\n";
echo " 关闭socket → 发送RST而不是FIN\n";
socket_close($conn);
}
socket_close($client);
socket_close($server);
}
trigger_rst();
// ===================================================================
// 284. PSH标志
// ===================================================================
section("284", "PSH标志");
/*
* 【大白话】
* PSH = Push = "赶紧推送给应用程序!"
*
* 正常情况下,接收方的TCP内核可能会攒一批数据再交给应用程序
* (为了提高效率)。但是发送方如果设置了PSH=1,就等于告诉接收方:
* "别等了,收到就立刻交给应用程序!"
*
* 什么时候会自动设PSH?
* - 发送缓冲区被清空时,最后一个包会自动设PSH
* - 应用程序可以自己要求设PSH
*
* 注意: PHP socket层面不能直接设置PSH,但可以通过TCP_NODELAY间接影响。
*/
echo "\n📌 PSH标志: 别缓存,赶紧送!\n\n";
function psh_example(): void {
echo "PSH的作用:\n";
echo " 不设PSH: 内核攒够数据(或超时) → 一次性交给应用 → 效率高但有延迟\n";
echo " 设PSH: 收到就立刻交给应用 → 延迟低但效率可能差\n\n";
echo "实际开发中:\n";
echo " - PSH 通常由内核自动设置,应用层不需要手动控制\n";
echo " - 如果你用 socket_write() 发送完一批数据,\n";
echo " 内核通常会在最后一个包自动设置PSH\n";
echo " - PSH 和 TCP_NODELAY 配合可以降低延迟\n";
}
psh_example();
// ---- PHP代码: PSH相关 ----
echo "\n📌 PHP代码: TCP_NODELAY与PSH的关系\n\n";
function psh_related_demo(): void {
$server = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($server, SOL_SOCKET, SO_REUSEADDR, 1);
socket_bind($server, '127.0.0.1', 9093);
socket_listen($server, 1);
socket_set_nonblock($server);
$client = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_connect($client, '127.0.0.1', 9093);
$tries = 0;
while (!($conn = @socket_accept($server)) && $tries < 100) {
usleep(10000);
$tries++;
}
socket_set_block($conn);
if (!isset($conn) || !$conn) {
socket_close($client);
socket_close($server);
return;
}
// ---- Nagle算法 vs TCP_NODELAY ----
/*
* Nagle算法(默认开启): 攒够数据再发,小包会合并
* TCP_NODELAY: 禁用Nagle,有数据就立刻发
*
* 这与PSH有关:
* Nagle开启: 小包合并 → 合并后的包可能才会设PSH
* NODELAY: 每个小包都立刻发 → 每个包都可能设PSH
*/
// 禁用Nagle (启用TCP_NODELAY)
$flag = 1;
socket_set_option($client, SOL_TCP, TCP_NODELAY, $flag);
echo " 客户端: TCP_NODELAY=1 (禁用Nagle算法)\n";
echo " ↑ 现在每次 socket_write() 都会立刻发送,内核会给最后字节设PSH\n\n";
// 发送几个小数据包
socket_write($client, "A"); // 立刻发,PSH可能被设
socket_write($client, "B"); // 立刻发,PSH可能被设
socket_write($client, "C"); // 立刻发,PSH可能被设
echo " 发送了3个小包: 'A', 'B', 'C'\n";
echo " 每个包内核都可能自动设置PSH标志\n";
// 服务端接收
$total = '';
for ($i = 0; $i < 3; $i++) {
$b = '';
socket_recv($conn, $b, 1, 0);
$total .= $b;
}
echo " 服务端收到: \"{$total}\"\n\n";
echo "注意: PHP的socket层无法直接观察PSH标志,\n";
echo " 但用 tcpdump/Wireshark 抓包可以看到PSH标志被设置\n";
socket_close($conn);
socket_close($client);
socket_close($server);
}
psh_related_demo();
// ===================================================================
// 285. URG标志
// ===================================================================
section("285", "URG标志");
/*
* 【大白话】
* URG = Urgent = "紧急数据!"
* 设置URG=1表示这个包里有紧急数据,接收方应该优先处理。
* 紧急数据放在"紧急指针"字段指定的位置。
*
* 实际情况: URG几乎没人用。
* 原因:
* 1. 紧急数据只有1字节(设计上的限制)
* 2. 应用层有更好的方式处理优先级
* 3. 很多TCP实现不完全支持
*
* 相比URG,更常见的做法是:
* - 另开一条TCP连接传紧急数据(如FTP的命令通道和数据通道)
* - 在应用层协议中标优先级(如HTTP/2的优先级)
*/
echo "\n📌 URG标志: 理论上有用,实际几乎没人用\n\n";
function urg_example(): void {
echo "URG的工作方式:\n";
echo " 1. 发送方设置 URG=1\n";
echo " 2. 紧急指针字段指向紧急数据的最后一个字节+1\n";
echo " 3. 接收方收到后触发 SIGURG 信号(如果进程注册了的话)\n";
echo " 4. 接收方优先读取紧急数据\n\n";
echo "PHP中:\n";
echo " - PHP sockets 不直接支持发送带URG标志的数据\n";
echo " - socket_send() 第四个参数可以传 MSG_OOB(Out-Of-Band)\n";
echo " - MSG_OOB 就是TCP紧急数据机制\n\n";
echo "为什么URG基本不用:\n";
echo " 1. 紧急数据只有1字节有效\n";
echo " 2. 多个紧急数据会互相覆盖\n";
echo " 3. 不同操作系统的实现不一致\n";
echo " 4. 不如另开连接或应用层协议处理\n";
}
urg_example();
// ---- PHP代码: MSG_OOB (紧急数据) ----
echo "\n📌 PHP代码: 发送紧急数据(MSG_OOB)\n\n";
function urg_demo(): void {
$server = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($server, SOL_SOCKET, SO_REUSEADDR, 1);
socket_bind($server, '127.0.0.1', 9094);
socket_listen($server, 1);
socket_set_nonblock($server);
$client = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_connect($client, '127.0.0.1', 9094);
$tries = 0;
while (!($conn = @socket_accept($server)) && $tries < 100) {
usleep(10000);
$tries++;
}
socket_set_block($conn);
if (!isset($conn) || !$conn) {
socket_close($client);
socket_close($server);
return;
}
// 发送普通数据
socket_write($client, "normal data", 11);
echo " 发送普通数据: \"normal data\"\n";
// 发送紧急数据 (MSG_OOB = Out-Of-Band)
@socket_send($client, "!", 1, MSG_OOB);
echo " 发送紧急数据: \"!\" (MSG_OOB → URG标志=1)\n";
echo " ↑ 这个字节会被放在紧急指针指定的位置\n";
echo " 对方可以用 MSG_OOB 标志来接收\n";
// 服务端先收普通数据
$normal = '';
socket_recv($conn, $normal, 1024, 0);
echo " 服务端收到普通数据: \"{$normal}\"\n";
// 服务端接收紧急数据
$urgent = '';
@socket_recv($conn, $urgent, 1, MSG_OOB);
echo " 服务端收到紧急数据: \"{$urgent}\"\n";
socket_close($conn);
socket_close($client);
socket_close($server);
}
urg_demo();
// ===================================================================
// 286. 窗口大小字段
// ===================================================================
section("286", "窗口大小字段");
/*
* 【大白话】
* 窗口大小 = "我还能接收多少数据"
*
* 这个字段告诉对方: 我现在的接收缓冲区还剩多少空间。
* 发送方看到窗口=0就不会再发数据了,直到对方窗口重新变大。
*
* 这是TCP流量控制的核心机制,也叫"滑动窗口"。
*
* 窗口大小是16位的,最大值65535字节(约64KB)。
* 如果不够用,可以通过"窗口缩放选项"来放大(最大到1GB)。
*
* 类比:
* 你朋友往你碗里夹菜,窗口大小就是你碗里还剩多少空间。
* 碗满了(窗口=0),你朋友就暂停夹菜,等你吃掉一些(窗口>0)再继续。
*/
echo "\n📌 窗口大小: TCP的流量控制核心\n\n";
function window_size_example(): void {
echo "窗口大小的含义:\n";
echo " 发送方问: \"你还能接收多少?\"\n";
echo " 接收方答: \"我还能收N字节\"(窗口大小=N)\n";
echo " 发送方: \"好,我一次最多发N字节给你\"\n\n";
echo "滑动窗口示意:\n";
echo " 接收方缓冲区总大小: 8192字节\n";
echo " 应用已读取: 4096字节\n";
echo " 内核已接收未读取: 2048字节\n";
echo " 剩余空间(窗口): 8192 - 2048 = 6144字节\n";
echo " → 通告窗口 = 6144\n";
}
window_size_example();
// ---- PHP代码: 观察和设置窗口相关参数 ----
echo "\n📌 PHP代码: 接收缓冲区和窗口大小\n\n";
function window_demo(): void {
$server = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($server, SOL_SOCKET, SO_REUSEADDR, 1);
socket_bind($server, '127.0.0.1', 9095);
socket_listen($server, 1);
socket_set_nonblock($server);
$client = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_connect($client, '127.0.0.1', 9095);
$tries = 0;
while (!($conn = @socket_accept($server)) && $tries < 100) {
usleep(10000);
$tries++;
}
socket_set_block($conn);
if (!isset($conn) || !$conn) {
socket_close($client);
socket_close($server);
return;
}
// 获取接收缓冲区大小
$rcvbuf = 0;
socket_get_option($conn, SOL_SOCKET, SO_RCVBUF, $rcvbuf);
echo " 服务端 SO_RCVBUF (接收缓冲区): {$rcvbuf} 字节\n";
echo " ↑ 内核会根据这个缓冲区大小来通告窗口\n";
echo " 实际窗口 = 缓冲区 - 已接收未读取的数据\n\n";
// 设置更大的接收缓冲区
$new_buf = 256 * 1024; // 256KB
socket_set_option($conn, SOL_SOCKET, SO_RCVBUF, $new_buf);
socket_get_option($conn, SOL_SOCKET, SO_RCVBUF, $rcvbuf);
echo " 设置 SO_RCVBUF = 256KB 后实际值: {$rcvbuf} 字节\n";
echo " ↑ 实际值通常是请求值的2倍(内核有最小/最大限制)\n\n";
// 获取发送缓冲区大小
$sndbuf = 0;
socket_get_option($client, SOL_SOCKET, SO_SNDBUF, $sndbuf);
echo " 客户端 SO_SNDBUF (发送缓冲区): {$sndbuf} 字节\n";
socket_close($conn);
socket_close($client);
socket_close($server);
}
window_demo();
// ===================================================================
// 287. TCP检验和
// ===================================================================
section("287", "TCP检验和");
/*
* 【大白话】
* 检验和 = "数据有没有在路上坏掉?"
*
* 发送方:
* 1. 把TCP首部+数据+伪首部(包含IP地址)当成一串16位数字
* 2. 全部加起来,取反码,放进检验和字段
*
* 接收方:
* 1. 用同样的方法再算一遍
* 2. 如果结果全为1,说明数据完整
* 3. 如果不是全1,说明数据坏了,丢掉这个包
*
* 注意: TCP的检验和是"端到端"的,不像以太网的CRC是"逐跳"的。
*/
echo "\n📌 TCP检验和: 端到端的数据完整性校验\n\n";
function checksum_example(): void {
echo "TCP检验和的计算方法(反码求和):\n\n";
echo "伪首部(12字节) = 源IP + 目的IP + 协议号 + TCP长度\n";
echo "这是为了同时校验IP地址是否正确\n\n";
// 简化的检验和计算示例
$words = [0x4500, 0x0073, 0x0000, 0x4000, 0x4011, 0x0000, 0xc0a8, 0x0001, 0xc0a8, 0x00c7];
echo "示例数据(16位字): ";
foreach ($words as $w) echo sprintf("0x%04X ", $w);
echo "\n";
// 反码求和
$sum = 0;
foreach ($words as $word) {
$sum += $word;
// 如果有进位,回卷
if ($sum > 0xFFFF) {
$sum = ($sum & 0xFFFF) + ($sum >> 16);
}
}
$checksum = ~$sum & 0xFFFF;
echo "和 = 0x" . sprintf("%04X", $sum) . "\n";
echo "检验和(反码) = 0x" . sprintf("%04X", $checksum) . "\n\n";
echo "PHP中计算检验和:\n";
echo " 应用层不需要手动计算检验和\n";
echo " → 内核自动计算和验证\n";
echo " → 如果自己构造raw socket发包,才需要手动算\n";
}
checksum_example();
// ---- PHP代码: 手动计算TCP检验和(用于raw socket) ----
echo "\n📌 PHP代码: 手动计算TCP检验和\n\n";
function calculate_tcp_checksum(
string $src_ip, // 源IP (字符串格式如 "192.168.1.1")
string $dst_ip, // 目的IP
string $tcp_header,// TCP首部
string $data = '' // TCP数据
): int {
// 1. 构造伪首部
$pseudo = '';
$pseudo .= pack('N', ip2long($src_ip)); // 源IP (4字节)
$pseudo .= pack('N', ip2long($dst_ip)); // 目的IP (4字节)
$pseudo .= pack('C', 0); // 保留(1字节,填0)
$pseudo .= pack('C', 6); // 协议号(1字节): TCP=6
$pseudo .= pack('n', strlen($tcp_header) + strlen($data)); // TCP长度(2字节)
// 2. 合并伪首部+TCP首部+数据
$packet = $pseudo . $tcp_header . $data;
// 3. 如果总长度是奇数,末尾补0字节
if (strlen($packet) % 2 != 0) {
$packet .= "\x00";
}
// 4. 按16位字累加
$sum = 0;
$len = strlen($packet);
for ($i = 0; $i < $len; $i += 2) {
$word = unpack('n', substr($packet, $i, 2))[1];
$sum += $word;
// 进位回卷
if ($sum > 0xFFFF) {
$sum = ($sum & 0xFFFF) + ($sum >> 16);
}
}
// 5. 取反码
return ~$sum & 0xFFFF;
}
// 测试
$src = '192.168.1.100';
$dst = '10.0.0.1';
$tcp = build_tcp_header(54321, 80, 1000, 0, 5, TCP_SYN, 65535);
$csum = calculate_tcp_checksum($src, $dst, $tcp);
echo " 源IP: {$src}\n";
echo " 目的IP: {$dst}\n";
echo " TCP检验和: 0x" . sprintf("%04X", $csum) . "\n";
echo " ↑ 如果把检验和填回TCP首部,再算一遍会得到 0xFFFF\n";
// 验证: 填回检验和再算 = 0xFFFF
$tcp_with_csum = substr($tcp, 0, 16) . pack('n', $csum) . substr($tcp, 18);
$verify = calculate_tcp_checksum($src, $dst, $tcp_with_csum);
echo " 验证: 填回检验和后再算 = 0x" . sprintf("%04X", $verify) . " (应该是FFFF)\n";
// ===================================================================
// 288. 紧急指针
// ===================================================================
section("288", "紧急指针");
/*
* 【大白话】
* 紧急指针和URG标志是一起用的。
* 只有当URG=1时,紧急指针才有意义。
*
* 紧急指针的值 = 从序列号开始,到紧急数据最后一个字节的偏移量。
*
* 比如:
* SEQ=1000, 紧急指针=5
* → 紧急数据的最后一个字节是 1000+5-1=1004 号字节
* → 也就是第5个字节
*
* 接收方收到URG=1后:
* 1. 识别出紧急数据的位置
* 2. 触发SIGURG信号
* 3. 应用可以用 MSG_OOB 来读紧急数据
*/
echo "\n📌 紧急指针: 指向紧急数据的位置\n\n";
echo "紧急指针的计算:\n";
echo " SEQ=2000, 紧急指针=8\n";
echo " 紧急数据最后字节 = 2000 + 8 - 1 = 2007\n";
echo " 如果是1字节紧急数据: 那就是字节2007\n\n";
echo "构造带URG的TCP首部:\n";
echo " \$flags = TCP_URG | TCP_ACK;\n";
echo " \$urgent_pointer = 1; // 紧急数据只有1字节\n";
echo " 首部中的紧急指针字段 = 1\n";
// ===================================================================
// 289. TCP选项
// ===================================================================
section("289", "TCP选项");
/*
* 【大白话】
* TCP选项就是TCP首部后面的"附加信息",不是必须的。
* 选项在20字节固定首部之后,最多40字节。
*
* 每个选项的格式: [类型(1字节)][长度(1字节)][值(变长)]
*
* 常见的选项:
* 类型0: 选项结束(EOL) —— 选项区的结束标记
* 类型1: 无操作(NOP) —— 填充用的,占1字节
* 类型2: 最大段大小(MSS)
* 类型3: 窗口缩放(Window Scale)
* 类型4: 选择确认允许(SACK Permitted)
* 类型5: 选择确认(SACK)
* 类型8: 时间戳(Timestamp)
*
* 选项必须按4字节对齐,不够就用NOP填充。
*/
echo "\n📌 TCP选项: 附在首部后面的附加功能\n\n";
// ---- PHP代码: 解析和构造TCP选项 ----
echo "📌 PHP代码: 构造TCP选项\n\n";
function build_tcp_options(array $options_list): string {
$options = '';
foreach ($options_list as $opt) {
$options .= $opt;
}
// 4字节对齐: 如果选项总长不是4的倍数,用NOP(0x01)填充
$len = strlen($options);
$pad = (4 - ($len % 4)) % 4;
for ($i = 0; $i < $pad; $i++) {
$options .= chr(1); // NOP = 0x01
}
return $options;
}
// MSS选项: 类型=2, 长度=4, 值=1460
$mss_option = chr(2) . chr(4) . pack('n', 1460);
echo " MSS选项: 类型=2, 长度=4, MSS=1460\n";
echo " 二进制: " . bin2hex($mss_option) . "\n\n";
// 窗口缩放: 类型=3, 长度=3, 缩放因子=7
$wscale_option = chr(3) . chr(3) . chr(7);
echo " 窗口缩放选项: 类型=3, 长度=3, 缩放因子=7\n";
echo " 二进制: " . bin2hex($wscale_option) . "\n\n";
// SACK允许: 类型=4, 长度=2
$sack_permitted = chr(4) . chr(2);
echo " SACK允许选项: 类型=4, 长度=2\n";
echo " 二进制: " . bin2hex($sack_permitted) . "\n\n";
// 时间戳: 类型=8, 长度=10, TSval=0x12345678, TSecr=0
$ts_option = chr(8) . chr(10) . pack('N', 0x12345678) . pack('N', 0);
echo " 时间戳选项: 类型=8, 长度=10, TSval=0x12345678\n";
echo " 二进制: " . bin2hex($ts_option) . "\n\n";
// 组合所有选项
$all_options = build_tcp_options([$mss_option, $wscale_option, $sack_permitted, $ts_option]);
echo " 组合后的选项(" . strlen($all_options) . "字节): " . bin2hex($all_options) . "\n";
// 计算数据偏移
$data_offset = (20 + strlen($all_options)) / 4;
echo " 数据偏移 = (20 + " . strlen($all_options) . ") / 4 = {$data_offset}\n";
echo " 首部总长 = {$data_offset} × 4 = " . ($data_offset * 4) . " 字节\n";
// ===================================================================
// 290. 最大段大小(MSS)
// ===================================================================
section("290", "最大段大小(MSS)");
/*
* 【大白话】
* MSS = Maximum Segment Size = "一个TCP包最多能装多少数据"
*
* 注意: MSS 指的是TCP数据部分的最大长度,不包括TCP首部和IP首部。
*
* 典型值:
* 以太网: MTU=1500, MSS=1460
* 计算: MTU - IP首部(20) - TCP首部(20) = 1500 - 40 = 1460
*
* MSS只在三次握手时协商,SYN包里携带MSS选项。
* 双方各自告诉对方自己的MSS,然后取较小的那个用。
*
* 比如:
* 客户端 SYN: "我的MSS=1460"
* 服务端 SYN+ACK: "我的MSS=1400"
* → 实际用的MSS = min(1460, 1400) = 1400
*/
echo "\n📌 MSS: 一个TCP段最大装多少数据\n\n";
function mss_example(): void {
echo "MSS 和 MTU 的关系:\n";
echo " 以太网 MTU = 1500 字节 (链路层)\n";
echo " 减 IP 首部 = 1500 - 20 = 1480 字节\n";
echo " 减 TCP首部 = 1480 - 20 = 1460 字节\n";
echo " → MSS = 1460 字节\n\n";
echo "常见网络的MSS:\n";
echo " 以太网(1500 MTU): MSS = 1460\n";
echo " PPPoE(1492 MTU): MSS = 1452\n";
echo " VPN/隧道: MSS可能更小(取决于隧道开销)\n";
echo " Jumbo Frame(9000): MSS = 8960\n";
}
mss_example();
// ---- PHP代码: MSS相关 ----
echo "\n📌 PHP代码: 获取和设置MSS\n\n";
function mss_demo(): void {
$server = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($server, SOL_SOCKET, SO_REUSEADDR, 1);
socket_bind($server, '127.0.0.1', 9096);
socket_listen($server, 1);
socket_set_nonblock($server);
$client = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
// 设置TCP_MAXSEG (MSS)
// 注意: 这个选项必须在connect之前设置
$mss_val = 1200;
socket_set_option($client, SOL_TCP, TCP_MAXSEG, $mss_val);
echo " 设置 TCP_MAXSEG = {$mss_val}\n";
echo " ↑ 告诉内核: 我的MSS是{$mss_val}字节\n";
socket_connect($client, '127.0.0.1', 9096);
$tries = 0;
while (!($conn = @socket_accept($server)) && $tries < 100) {
usleep(10000);
$tries++;
}
if (isset($conn) && $conn !== false) {
// 查看实际协商的MSS
$actual_mss = 0;
socket_get_option($conn, SOL_TCP, TCP_MAXSEG, $actual_mss);
// 注意: TCP_MAXSEG在不同系统上行为不同,可能不支持get
echo " 尝试读取 TCP_MAXSEG: " . ($actual_mss ?: '不支持读取') . "\n";
socket_close($conn);
}
socket_close($client);
socket_close($server);
echo "\n 实际开发建议:\n";
echo " - MSS由内核自动协商,一般不需要手动设置\n";
echo " - 除非你在做VPN/隧道,需要降低MSS来避免PMTU问题\n";
echo " - 如果设置,在 connect() 之前 set socket option\n";
}
mss_demo();
// ===================================================================
// 291. 窗口缩放选项
// ===================================================================
section("291", "窗口缩放选项");
/*
* 【大白话】
* TCP首部里的窗口大小只有16位,最大只能表示65535字节(约64KB)。
* 在高速网络(如10Gbps)上,64KB的窗口太小了,来不及填满管道。
*
* 窗口缩放就是给窗口值加一个"放大系数"。
* 缩放因子(shift count)范围: 0~14
* 实际窗口 = 首部窗口值 × 2^shift
* 最大窗口 = 65535 × 2^14 ≈ 1GB
*
* 注意:
* 窗口缩放选项只在SYN包里出现,连接建立后就不能改了。
* 如果一方不支持,缩放就不生效。
*/
echo "\n📌 窗口缩放: 让窗口从64KB扩大到1GB\n\n";
function window_scale_example(): void {
echo "缩放因子与实际窗口的关系:\n\n";
foreach ([0, 1, 2, 3, 7, 14] as $shift) {
$max_win = 65535 * (1 << $shift);
echo " shift={$shift}: 最大窗口 = 65535 × 2^{$shift} = ";
if ($max_win > 1024*1024*1024) {
echo sprintf("%.1f GB", $max_win / (1024*1024*1024));
} elseif ($max_win > 1024*1024) {
echo sprintf("%.1f MB", $max_win / (1024*1024));
} elseif ($max_win > 1024) {
echo sprintf("%.1f KB", $max_win / 1024);
} else {
echo "{$max_win} 字节";
}
echo "\n";
}
echo "\n 高带宽延迟网络需要大窗口:\n";
echo " 带宽延迟积(BDP) = 带宽 × RTT\n";
echo " 1Gbps × 100ms RTT = 12.5MB\n";
echo " → 需要窗口至少12.5MB,shift至少需要8\n";
}
window_scale_example();
// ---- PHP代码: 窗口缩放相关 ----
echo "\n📌 PHP代码: 查看窗口缩放相关设置\n\n";
function window_scale_demo(): void {
$sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
// 窗口缩放由内核自动处理,应用层通常不需要手动设置
// 但可以通过SO_RCVBUF影响窗口大小
$rcvbuf = 0;
socket_get_option($sock, SOL_SOCKET, SO_RCVBUF, $rcvbuf);
echo " SO_RCVBUF: {$rcvbuf} 字节\n";
// Linux上可以设置很大的接收缓冲来利用窗口缩放
$new_rcvbuf = 1 * 1024 * 1024; // 1MB
socket_set_option($sock, SOL_SOCKET, SO_RCVBUF, $new_rcvbuf);
socket_get_option($sock, SOL_SOCKET, SO_RCVBUF, $rcvbuf);
echo " 设置1MB后的 SO_RCVBUF: {$rcvbuf} 字节\n";
echo " ↑ 实际值可能翻倍(内核自动调整)\n";
echo " 当缓冲区>64KB时,内核会自动启用窗口缩放选项\n";
socket_close($sock);
}
window_scale_demo();
// ===================================================================
// 292. 时间戳选项
// ===================================================================
section("292", "时间戳选项");
/*
* 【大白话】
* 时间戳选项有两个功能:
* 1. 计算RTT(往返时间) —— 更精确
* 2. 防止序列号绕回(PAWS) —— 高速网络中序列号可能重复
*
* 时间戳选项格式:
* 类型=8, 长度=10字节
* TSval (4字节): 发送方的时间戳
* TSecr (4字节): 回显对方的时间戳
*
* 工作原理:
* 发送方: TSval=当前时间戳
* 接收方: 回复时把对方的TSval复制到TSecr里
* 发送方: 收到回复后 当前时间 - TSecr = RTT
*
* 跟SYN一样,时间戳选项在SYN包里协商。
*/
echo "\n📌 时间戳: 精确计算RTT + 防序列号绕回\n\n";
function timestamp_example(): void {
echo "时间戳计算RTT:\n";
echo " 时刻T1: A→B, TSval=T1\n";
echo " 时刻T2: B收到, 记住T1\n";
echo " 时刻T3: B→A, TSval=T3, TSecr=T1\n";
echo " 时刻T4: A收到回复\n";
echo " RTT = T4 - T1 (A自己就能算)\n\n";
echo "PAWS (Protect Against Wrapped Sequences):\n";
echo " 高速网络(10Gbps+)上,序列号可能很快用完42亿\n";
echo " 时间戳能区分新旧包: \n";
echo " → 收到的时间戳比自己新 = 新包\n";
echo " → 收到的时间戳比自己旧很多 = 可能是旧包重放\n";
}
timestamp_example();
// ---- PHP代码: 时间戳相关 ----
echo "\n📌 PHP代码: TCP_TIMESTAMP选项\n\n";
function timestamp_demo(): void {
$sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
// Linux上可以启用/禁用TCP时间戳
// TCP_TIMESTAMP 在某些系统上可用
$ts = 0;
if (@socket_get_option($sock, SOL_TCP, 24, $ts)) { // 24 = TCP_TIMESTAMP on some systems
echo " TCP_TIMESTAMP: " . ($ts ? '启用' : '禁用') . "\n";
} else {
echo " TCP_TIMESTAMP: 由内核自动管理(大多数系统默认启用)\n";
}
echo " ↑ 时间戳选项在SYN包中由内核自动协商\n";
socket_close($sock);
}
timestamp_demo();
// ===================================================================
// 293. 选择确认(SACK)选项
// ===================================================================
section("293", "选择确认(SACK)选项");
/*
* 【大白话】
* SACK = Selective Acknowledgment = "选择性确认"
*
* 普通的ACK只能告诉对方"我收到了连续的数据到第N号"。
* 但如果中间的包丢了,后面的包收到了,普通ACK没法告诉对方
* "我收到了后面的,就差中间这一段"。
*
* SACK就是解决这个问题的:
* 接收方可以告诉发送方:
* "我收到了 100~199, 300~399, 500~599"
* 发送方就知道: "只需要重发 200~299, 400~499"
*
* 没有SACK时: 丢了200~299,发送方得把200~599全部重发一遍
* 有SACK时: 只重发丢的那段,节省带宽
*
* SACK分两个选项:
* SACK-Permitted (类型4): 在SYN包里协商"我支持SACK"
* SACK (类型5): 在数据传输中告诉对方"我收到了哪些段"
*/
echo "\n📌 SACK: 精确告诉对方哪些数据收到了\n\n";
function sack_example(): void {
echo "普通ACK vs SACK:\n\n";
echo "场景: 发送方发了5个包: [100][200][300][400][500]\n";
echo " 接收方收到: [100][丢失][300][丢失][500]\n\n";
echo "普通ACK(没有SACK):\n";
echo " ACK=200 (告诉发送方100~199收到了)\n";
echo " 发送方必须重传: 200~599 (全部重传!)\n\n";
echo "SACK:\n";
echo " ACK=200, SACK=[300~399, 500~599]\n";
echo " ↑ 告诉发送方: 200前面我收了, 300~399和500~599也收了\n";
echo " 发送方只需要重传: 200~299, 400~499 (精准重传!)\n";
}
sack_example();
// ---- PHP代码: SACK相关 ----
echo "\n📌 PHP代码: SACK由内核自动处理\n\n";
function sack_demo(): void {
$sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
echo " SACK的协商和使用都是内核自动完成的:\n";
echo " 1. SYN包里自动带上 SACK-Permitted 选项\n";
echo " 2. 丢包时自动在ACK包里带上 SACK 信息\n";
echo " 3. 发送方内核根据SACK信息精准重传\n\n";
echo " 在PHP中你可以:\n";
echo " - Linux: /proc/sys/net/ipv4/tcp_sack (1=启用, 0=禁用)\n";
echo " - 一般不需要手动设置,默认就是启用的\n";
socket_close($sock);
}
sack_demo();
// ===================================================================
// 294. TCP三次握手
// ===================================================================
section("294", "TCP三次握手");
/*
* 【大白话】
* 三次握手就是建立TCP连接的"打招呼"过程。
*
* 为什么要三次?
* 因为网络是不可靠的,三次是保证双方都能收发的最小次数。
*
* 三次握手的过程:
*
* 客户端(主动打开) 服务端(被动打开)
* | |
* | ---- SYN, SEQ=x -----------> | 第一步: 客户端发起
* | | "我要连你"
* | <--- SYN+ACK, SEQ=y, ACK=x+1-- | 第二步: 服务端响应
* | | "收到,我也要连你"
* | ---- ACK, SEQ=x+1, ACK=y+1 --> | 第三步: 客户端确认
* | | "好的,开干"
* ESTABLISHED ESTABLISHED
*
* 每次握手:
* 第一步: 客户端说"嗨" (SYN=1)
* 第二步: 服务端说"嗨,收到" (SYN=1, ACK=1)
* 第三步: 客户端说"收到" (ACK=1)
*
* 三次之后,双方都确认了:
* 客户端能发(第一步证明)
* 客户端能收(第二步证明)
* 服务端能发(第二步证明)
* 服务端能收(第三步证明)
*/
echo "\n📌 三次握手: TCP建立连接的三次对话\n\n";
function three_way_handshake_explanation(): void {
echo "三次握手详细过程:\n\n";
echo "【第一步: SYN】\n";
echo " 客户端 → 服务端: SYN=1, SEQ=x\n";
echo " 含义: \"我想建立连接,我的初始序列号是x\"\n";
echo " 客户端状态: CLOSED → SYN_SENT\n\n";
echo "【第二步: SYN+ACK】\n";
echo " 服务端 → 客户端: SYN=1, ACK=1, SEQ=y, ACK=x+1\n";
echo " 含义: \"收到你的请求,我也要连接,我的初始序列号是y\"\n";
echo " 服务端状态: LISTEN → SYN_RCVD\n\n";
echo "【第三步: ACK】\n";
echo " 客户端 → 服务端: ACK=1, SEQ=x+1, ACK=y+1\n";
echo " 含义: \"收到你的确认\"\n";
echo " 双方状态: → ESTABLISHED\n\n";
echo "为什么是三次而不是两次?\n";
echo " 两次只能保证客户端→服务端的通道正常,\n";
echo " 无法保证服务端→客户端的通道正常。\n";
echo " 第三次ACK就是为了让客户端证明\"我能收到你的消息\"。\n";
}
three_way_handshake_explanation();
// ---- PHP代码: 三次握手完整实现 ----
echo "\n📌 PHP代码: 三次握手的完整实现\n\n";
function tcp_three_way_handshake(): void {
echo "=== TCP三次握手 完整PHP实现 ===\n\n";
// 记录状态
$client_state = 'CLOSED';
$server_state = 'CLOSED';
// 创建服务端 socket → LISTEN
$server = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($server, SOL_SOCKET, SO_REUSEADDR, 1);
socket_bind($server, '127.0.0.1', 9097);
socket_listen($server, 3);
socket_set_nonblock($server);
$server_state = 'LISTEN';
echo "服务端: 创建socket, 绑定, 监听 → 状态: {$server_state}\n\n";
// 创建客户端 socket → CLOSED
$client = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
echo "客户端: 创建socket → 状态: {$client_state}\n";
// ===== 第一步: 客户端发起SYN =====
echo "\n--- 第一步: SYN ---\n";
echo "客户端 → 服务端: SYN (内核自动发送)\n";
echo " 客户端状态: {$client_state} → SYN_SENT\n";
socket_connect($client, '127.0.0.1', 9097);
// connect() 会发SYN, 然后等待SYN+ACK
$client_state = 'SYN_SENT';
// ===== 第二步: 服务端收到SYN, 回复SYN+ACK =====
echo "\n--- 第二步: SYN+ACK ---\n";
$tries = 0;
while (!($conn = @socket_accept($server)) && $tries < 100) {
usleep(10000);
$tries++;
}
$server_state = 'SYN_RCVD';
echo "服务端: accept() 返回, 收到SYN, 内核自动回复SYN+ACK\n";
echo " 服务端状态: LISTEN → SYN_RCVD (但accept返回说明已完成握手)\n";
// ===== 第三步: 客户端收到SYN+ACK, 回复ACK =====
echo "\n--- 第三步: ACK ---\n";
echo "客户端: 收到SYN+ACK, 内核自动回复ACK, connect()返回\n";
$client_state = 'ESTABLISHED';
$server_state = 'ESTABLISHED';
echo "\n=== 三次握手完成 ===\n";
echo " 客户端状态: {$client_state}\n";
echo " 服务端状态: {$server_state}\n";
echo " 连接已建立, 可以开始传输数据!\n";
// 验证连接
$msg = "三次握手成功!";
socket_write($client, $msg, strlen($msg));
$buf = '';
socket_recv($conn, $buf, 1024, 0);
echo " 验证: 客户端发送 \"{$msg}\" → 服务端收到 \"{$buf}\"\n";
socket_close($conn);
socket_close($client);
socket_close($server);
}
tcp_three_way_handshake();
// ===================================================================
// 295. TCP四次挥手
// ===================================================================
section("295", "TCP四次挥手");
/*
* 【大白话】
* 四次挥手就是关闭TCP连接的"告别"过程。
*
* 为什么是四次不是三次?
* 因为TCP是全双工的(双方都能发能收),
* 一方发完FIN只表示"我不发了",另一方可能还有数据要发。
* 所以需要分开确认: 先确认对方的FIN,再发自己的FIN。
*
* 四次挥手过程:
*
* 主动关闭方 被动关闭方
* | |
* | ---- FIN, SEQ=u -----------> | 第一次: "我不发了"
* | |
* | <--- ACK, SEQ=v, ACK=u+1 ---- | 第二次: "知道了"
* | | (被动方可能继续发数据)
* | <--- FIN, ACK, SEQ=w, ACK=u+1- | 第三次: "我也不发了"
* | |
* | ---- ACK, SEQ=u+1, ACK=w+1 --> | 第四次: "知道了"
* | |
* TIME_WAIT (等2MSL) LAST_ACK → CLOSED
*/
echo "\n📌 四次挥手: TCP优雅关闭连接的四次对话\n\n";
function four_way_handshake_explanation(): void {
echo "四次挥手详细过程:\n\n";
echo "【第一次: FIN from 主动方】\n";
echo " 主动方 → 被动方: FIN=1, SEQ=u\n";
echo " 含义: \"我这边数据发完了,准备关连接\"\n";
echo " 主动方状态: ESTABLISHED → FIN_WAIT_1\n\n";
echo "【第二次: ACK from 被动方】\n";
echo " 被动方 → 主动方: ACK=1, SEQ=v, ACK=u+1\n";
echo " 含义: \"知道了,我收到了你的关闭请求\"\n";
echo " 被动方状态: ESTABLISHED → CLOSE_WAIT\n";
echo " 主动方收到后: FIN_WAIT_1 → FIN_WAIT_2\n\n";
echo "【半关闭状态】\n";
echo " 主动方→被动方 方向已关闭(主动方不再发数据)\n";
echo " 被动方→主动方 方向仍然畅通(被动方还能发数据)\n\n";
echo "【第三次: FIN from 被动方】\n";
echo " 被动方 → 主动方: FIN=1, ACK=1, SEQ=w, ACK=u+1\n";
echo " 含义: \"我这边数据也发完了\"\n";
echo " 被动方状态: CLOSE_WAIT → LAST_ACK\n\n";
echo "【第四次: ACK from 主动方】\n";
echo " 主动方 → 被动方: ACK=1, SEQ=u+1, ACK=w+1\n";
echo " 含义: \"好的,知道了,拜拜\"\n";
echo " 被动方收到后: LAST_ACK → CLOSED\n";
echo " 主动方: FIN_WAIT_2 → TIME_WAIT (等2MSL→CLOSED)\n";
}
four_way_handshake_explanation();
// ---- PHP代码: 四次挥手完整实现 ----
echo "\n📌 PHP代码: 四次挥手的完整实现\n\n";
function tcp_four_way_handshake(): void {
echo "=== TCP四次挥手 完整PHP实现 ===\n\n";
$server = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($server, SOL_SOCKET, SO_REUSEADDR, 1);
socket_bind($server, '127.0.0.1', 9098);
socket_listen($server, 1);
socket_set_nonblock($server);
$client = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_connect($client, '127.0.0.1', 9098);
$tries = 0;
while (!($conn = @socket_accept($server)) && $tries < 100) {
usleep(10000);
$tries++;
}
socket_set_block($conn);
echo "连接已建立 (ESTABLISHED)\n\n";
// ===== 第一次挥手: 客户端主动关闭 =====
echo "--- 第一次挥手: FIN ---\n";
echo "客户端 → 服务端: FIN (socket_shutdown 触发)\n";
socket_shutdown($client, 1); // SHUT_WR → 发送FIN
echo " 客户端状态: ESTABLISHED → FIN_WAIT_1\n\n";
// ===== 第二次挥手: 服务端收到FIN, 回复ACK =====
echo "--- 第二次挥手: ACK ---\n";
$buf = '';
$n = @socket_recv($conn, $buf, 1024, 0);
if ($n === 0) {
echo " 服务端: 读到0字节(EOF),说明收到了FIN\n";
echo " 服务端 → 客户端: ACK (内核自动发送)\n";
echo " 服务端状态: ESTABLISHED → CLOSE_WAIT\n";
echo " 客户端收到ACK后: FIN_WAIT_1 → FIN_WAIT_2\n\n";
}
// ===== 半关闭状态: 服务端还可以发数据 =====
echo "--- 半关闭状态 ---\n";
echo " 服务端→客户端方向仍可通信\n";
socket_write($conn, "我还有数据要发", 18);
echo " 服务端 → 客户端: \"我还有数据要发\"\n";
$buf2 = '';
socket_recv($client, $buf2, 1024, 0);
echo " 客户端收到: \"{$buf2}\"\n\n";
// ===== 第三次挥手: 服务端也关闭 =====
echo "--- 第三次挥手: FIN (服务端) ---\n";
echo " 服务端 → 客户端: FIN (内核自动发送)\n";
socket_shutdown($conn, 1); // SHUT_WR → 发送FIN
echo " 服务端状态: CLOSE_WAIT → LAST_ACK\n\n";
// ===== 第四次挥手: 客户端收到FIN, 回复ACK =====
echo "--- 第四次挥手: ACK ---\n";
$buf3 = '';
$n2 = @socket_recv($client, $buf3, 1024, 0);
if ($n2 === 0) {
echo " 客户端: 读到0字节(EOF),收到了FIN\n";
echo " 客户端 → 服务端: ACK (内核自动发送)\n";
echo " 客户端状态: FIN_WAIT_2 → TIME_WAIT\n";
echo " 服务端收到ACK后: LAST_ACK → CLOSED\n\n";
}
echo "=== 四次挥手完成 ===\n";
echo " 服务端: CLOSED\n";
echo " 客户端: TIME_WAIT (操作系统层面,持续约2MSL=60秒)\n";
socket_close($conn);
socket_close($client);
socket_close($server);
}
tcp_four_way_handshake();
// ===================================================================
// 296. 半关闭 (Half-Close)
// ===================================================================
section("296", "半关闭 (Half-Close)");
/*
* 【大白话】
* 半关闭就是"我可以不发了,但我还能收"的状态。
*
* 比如客户端调用 socket_shutdown($sock, 1) (SHUT_WR):
* 客户端: 发一个FIN给服务端 → "我不发了"
* 服务端: 回复ACK → "知道了"
* 此时: 客户端→服务端的通道关闭了
* 服务端→客户端的通道还开着
* 服务端可以继续发数据给客户端,客户端还能收
*
* 什么时候用到?
* 1. 客户端请求结束后说"我请求完了",服务端还能回复
* 2. HTTP/1.1 里,客户端关闭写端表示请求结束,服务端回复响应
* 3. 管道通信: 写端关闭表示输入结束,但还可以读输出
*/
echo "\n📌 半关闭: 只关一半,另一半还能用\n\n";
// ---- PHP代码: 半关闭完整示例 ----
echo "📌 PHP代码: 半关闭的完整示例\n\n";
function half_close_demo(): void {
$server = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($server, SOL_SOCKET, SO_REUSEADDR, 1);
socket_bind($server, '127.0.0.1', 9099);
socket_listen($server, 1);
socket_set_nonblock($server);
$client = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_connect($client, '127.0.0.1', 9099);
$tries = 0;
while (!($conn = @socket_accept($server)) && $tries < 100) {
usleep(10000);
$tries++;
}
socket_set_block($conn);
echo "1. 连接建立,双方都能收发\n\n";
// 客户端发一些数据
socket_write($client, "请求数据", 12);
$buf = '';
socket_recv($conn, $buf, 1024, 0);
echo "2. 客户端 → 服务端: \"{$buf}\"\n\n";
// 客户端半关闭: 关闭写端
socket_shutdown($client, 1); // SHUT_WR → 发FIN,但还能读
echo "3. 客户端调用 socket_shutdown(\$sock, 1) → 半关闭\n";
echo " 客户端: \"我不发了\"(FIN已发送)\n";
echo " 客户端: 还能接收数据 ✓\n\n";
// 服务端收到FIN
$buf2 = '';
$n = @socket_recv($conn, $buf2, 1024, 0);
if ($n === 0) {
echo "4. 服务端读到0字节 → 客户端写端已关闭\n";
}
// 服务端继续发数据 (半关闭状态下合法!)
socket_write($conn, "回复数据1", 12);
socket_write($conn, "回复数据2", 12);
echo "5. 服务端 → 客户端: 发送两段数据\n\n";
// 客户端仍然能收到!
$buf3 = '';
socket_recv($client, $buf3, 1024, 0);
echo "6. 客户端收到: \"{$buf3}\" ✓\n\n";
// 服务端发完也关闭
socket_shutdown($conn, 1);
echo "7. 服务端也调用 socket_shutdown → 发送FIN\n\n";
// 客户端收到FIN
$buf4 = '';
$n2 = @socket_recv($client, $buf4, 1024, 0);
if ($n2 === 0) {
echo "8. 客户端读到0字节 → 服务端也关了\n";
echo " 连接完全关闭\n\n";
}
echo "关键点:\n";
echo " socket_shutdown(\$sock, 0) = SHUT_RD → 关闭读端\n";
echo " socket_shutdown(\$sock, 1) = SHUT_WR → 关闭写端(发FIN)\n";
echo " socket_shutdown(\$sock, 2) = SHUT_RDWR → 读写都关\n";
echo " socket_close(\$sock) → 引用计数-1,引用为0才真正关闭\n";
socket_close($conn);
socket_close($client);
socket_close($server);
}
half_close_demo();
// ===================================================================
// 297. 同时打开 (Simultaneous Open)
// 298. 同时关闭 (Simultaneous Close)
// 299. 连接建立超时
// 300. TCP状态转换图
// 301-311: 各状态详解
// 312. 2MSL等待
// 313. TIME_WAIT的作用
// 314. 复位报文段
// 315. 半打开连接
// 316. 连接队列
// 317. SYN泛洪攻击
// 318. SYN Cookie
// 319. 初始序列号(ISN)
// 320. 端口号分配
// ===================================================================
// (由于文件长度限制,297-320章节的代码在第二部分中继续)
// ===================================================================
echo "\n\n";
echo "╔══════════════════════════════════════════════════╗\n";
echo "║ TCP 指南 第1部分 (274-296) 结束 ║\n";
echo "║ 继续运行第二部分查看 297-320 ║\n";
echo "╚══════════════════════════════════════════════════╝\n";
<?php
/**
* ===================================================================
* TCP 完整指南 (297-320) - 第二部分
* 大白话解释 + PHP Sockets 代码示例
* ===================================================================
* 承接 tcp_complete_guide.php 第一部分 (274-296)
*/
echo "╔══════════════════════════════════════════════════╗\n";
echo "║ TCP 完整指南 第二部分 —— 297~320 ║\n";
echo "╚══════════════════════════════════════════════════╝\n\n";
function section(string $num, string $title): void {
echo "\n" . str_repeat("━", 60) . "\n";
echo " {$num}. {$title}\n";
echo str_repeat("━", 60) . "\n";
}
// ===================================================================
// 297. 同时打开 (Simultaneous Open)
// ===================================================================
section("297", "同时打开 (Simultaneous Open)");
/*
* 【大白话】
* 同时打开就是两方同时向对方发SYN。
* 这是一种很罕见的情况,但TCP协议支持。
*
* 过程:
* 双方都是 CLOSED → SYN_SENT (同时发SYN)
* 双方都收到对方的SYN → 各自回复SYN+ACK
* 双方状态: SYN_SENT → SYN_RCVD → ESTABLISHED
*
* 实际场景: 两个程序同时想要连接对方。
* 比如P2P网络中,两个节点同时尝试连接对方。
*/
echo "同时打开过程 (罕见但合法):\n\n";
echo " 主机A 主机B\n";
echo " CLOSED CLOSED\n";
echo " | |\n";
echo " |-- SYN, SEQ=x ------------> | (A发起SYN)\n";
echo " | <----------- SYN, SEQ=y --| (B也发起SYN)\n";
echo " | |\n";
echo " 双方同时处于 SYN_SENT 状态\n";
echo " 双方都收到了对方的SYN\n";
echo " | |\n";
echo " |-- SYN+ACK, SEQ=x, ACK=y+1-> | (A回复SYN+ACK)\n";
echo " | <- SYN+ACK, SEQ=y, ACK=x+1-| (B回复SYN+ACK)\n";
echo " | |\n";
echo " 双方进入 SYN_RCVD → ESTABLISHED\n";
echo " 只交换了4个包(比普通三次握手多1个)\n\n";
echo "PHP中很难演示同时打开,因为 connect() 是阻塞的。\n";
echo "需要异步或多进程来实现。以下是概念代码:\n\n";
echo <<<'CODE'
// 同时打开概念 (需要两个进程同时connect对方)
// 进程A: bind 50001, connect 50002
// 进程B: bind 50002, connect 50001
// 两者几乎同时执行 → 同时打开
// 使用pcntl_fork或多线程来实现
CODE;
echo "\n";
// ===================================================================
// 298. 同时关闭 (Simultaneous Close)
// ===================================================================
section("298", "同时关闭 (Simultaneous Close)");
/*
* 【大白话】
* 同时关闭就是两方同时向对方发FIN。
* 这种情况比同时打开更常见一些。
*
* 过程:
* 双方都是 ESTABLISHED → FIN_WAIT_1 (同时发FIN)
* 双方都收到对方的FIN(而不是期望的ACK)
* 双方状态: FIN_WAIT_1 → CLOSING (一个特殊状态)
* 双方都收到ACK后: CLOSING → TIME_WAIT → CLOSED
*/
echo "同时关闭过程:\n\n";
echo " 主机A 主机B\n";
echo " ESTABLISHED ESTABLISHED\n";
echo " | |\n";
echo " |-- FIN, SEQ=x ------------> | (A说拜拜)\n";
echo " | <----------- FIN, SEQ=y --| (B也说拜拜)\n";
echo " | |\n";
echo " 双方状态: ESTABLISHED → FIN_WAIT_1\n";
echo " 双方都收到了对方的FIN\n";
echo " | |\n";
echo " 双方状态: FIN_WAIT_1 → CLOSING (特殊状态!)\n";
echo " | |\n";
echo " |-- ACK, ACK=y+1 ----------> | (A确认B的FIN)\n";
echo " | <----------- ACK, ACK=x+1-| (B确认A的FIN)\n";
echo " | |\n";
echo " 双方状态: CLOSING → TIME_WAIT\n";
echo " 等2MSL后: TIME_WAIT → CLOSED\n\n";
echo "CLOSING 是一个特殊状态:\n";
echo " 表示\"我发了FIN,也收到了FIN,但还没收到对我的FIN的ACK\"\n";
echo " 这个状态通常很快就过去了\n";
echo " 同时也说明了为什么TCP有11个状态而不是10个\n";
// ===================================================================
// 299. 连接建立超时
// ===================================================================
section("299", "连接建立超时");
/*
* 【大白话】
* 连接建立超时就是 connect() 等了太久连不上。
*
* 常见原因:
* 1. 服务器不在线/端口没开
* 2. 网络不通
* 3. 防火墙把SYN包丢了
*
* TCP会重试几次SYN:
* Linux默认重试5次,总共发6个SYN包
* 第一次: 立即发
* 重试1: 等1秒 → 重试2: 等2秒 → 重试3: 等4秒 →
* 重试4: 等8秒 → 重试5: 等16秒 → 重试6: 等32秒
* 总共约127秒后放弃
*
* 指数退避算法: 每次等待时间翻倍
*/
echo "SYN重试策略 (Linux 默认):\n";
echo " 第1次发SYN: 立即\n";
echo " 重试1: 等1秒 重试2: 等2秒 重试3: 等4秒\n";
echo " 重试4: 等8秒 重试5: 等16秒 重试6: 等32秒\n";
echo " 总共约127秒后放弃\n\n";
echo "PHP代码示例:\n\n";
// 非阻塞connect + select超时
$sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_nonblock($sock); // 设为非阻塞
// 尝试连接一个不可达的地址
@socket_connect($sock, '192.0.2.1', 80);
// 用socket_select设置超时
$write = [$sock];
$except = [$sock];
$timeout_sec = 5;
$start = microtime(true);
$ready = socket_select($read = null, $write, $except, $timeout_sec, 0);
$elapsed = microtime(true) - $start;
if ($ready === 0) {
echo "⏰ 连接超时! (超过{$timeout_sec}秒)\n";
echo "实际等待: " . round($elapsed, 2) . " 秒\n";
echo "↑ 这比默认的127秒快多了!\n";
echo " 非阻塞connect + select是控制超时的最佳方式\n";
} elseif ($ready === false) {
echo "错误: " . socket_strerror(socket_last_error()) . "\n";
}
socket_close($sock);
echo "\n【最佳实践】用非阻塞connect控制超时:\n";
echo <<<'CODE'
// 1. 创建socket,设为非阻塞
$sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_nonblock($sock);
// 2. 发起连接(立即返回)
@socket_connect($sock, $host, $port);
// 3. 用select等待,精确控制超时时间
$write = [$sock];
$except = [$sock];
$ready = socket_select($read=null, $write, $except, $timeout_sec);
if ($ready === 0) {
die("连接超时");
}
// 4. 连接成功,切回阻塞模式
socket_set_block($sock);
CODE;
echo "\n";
// ===================================================================
// 300. TCP状态转换图
// ===================================================================
section("300", "TCP状态转换图");
/*
* 【大白话】
* TCP连接从生到死会经历11种状态。
* 理解这些状态是调试网络问题的基础。
*/
echo "TCP 11种状态完整一览:\n\n";
echo " 客户端(主动打开) 服务端(被动打开)\n";
echo " CLOSED CLOSED\n";
echo " | socket_connect() | socket_listen()\n";
echo " ↓ ↓\n";
echo " SYN_SENT LISTEN\n";
echo " | 发SYN | 收到SYN\n";
echo " | ↓\n";
echo " | SYN_RCVD (发SYN+ACK)\n";
echo " | 收到SYN+ACK | 收到ACK\n";
echo " └────────┬───────────────────┘\n";
echo " ↓\n";
echo " ESTABLISHED ←── 数据传输阶段\n";
echo " |\n";
echo " 主动关闭(FIN) 被动关闭(收到FIN)\n";
echo " ↓ ↓\n";
echo " FIN_WAIT_1 CLOSE_WAIT\n";
echo " | 收到ACK | 发FIN\n";
echo " ↓ ↓\n";
echo " FIN_WAIT_2 LAST_ACK\n";
echo " | 收到FIN | 收到ACK\n";
echo " ↓ ↓\n";
echo " TIME_WAIT (2MSL) CLOSED\n";
echo " | 超时\n";
echo " ↓\n";
echo " CLOSED\n\n";
echo "同时关闭的额外路径:\n";
echo " ESTABLISHED → FIN_WAIT_1 → CLOSING → TIME_WAIT → CLOSED\n\n";
echo "PHP中查看TCP状态:\n";
echo " 在Windows: netstat -an | findstr <端口>\n";
echo " 在Linux: ss -tan | grep <端口>\n";
// ===================================================================
// 301-311: TCP各状态详解
// ===================================================================
section("301-311", "TCP各状态代码演示");
echo "各状态及触发方式:\n\n";
function demo_tcp_states(): void {
// LISTEN 状态
$server = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($server, SOL_SOCKET, SO_REUSEADDR, 1);
socket_bind($server, '127.0.0.1', 9100);
socket_listen($server, 3);
echo "✅ 服务端处于 LISTEN 状态 (127.0.0.1:9100)\n";
echo " 用 netstat -an | findstr 9100 可以看到 LISTENING\n\n";
// 创建客户端连接
$client = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_connect($client, '127.0.0.1', 9100); // 客户端经历 SYN_SENT
socket_set_nonblock($server);
$conn = @socket_accept($server); // 服务端经历 SYN_RCVD
usleep(10000);
echo "✅ 连接建立 → 双方 ESTABLISHED\n\n";
// 发送数据验证ESTABLISHED
socket_write($client, "hello", 5);
$b = '';
socket_recv($conn, $b, 1024, 0);
echo " 数据传输验证: 收到 '{$b}' (ESTABLISHED状态正常)\n\n";
// 观察各状态
echo "现在用 netstat -an | findstr 9100 你会看到:\n";
echo " TCP 127.0.0.1:9100 ... LISTENING (server socket)\n";
echo " TCP 127.0.0.1:9100 ... ESTABLISHED (conn socket)\n";
echo " TCP 127.0.0.1:XXXX ... ESTABLISHED (client socket)\n\n";
// 触发状态转换
echo "=== 触发状态转换演示 ===\n\n";
// 1. 客户端主动关闭 → TIME_WAIT
echo "关闭客户端 → 客户端进入 TIME_WAIT\n";
socket_close($client); // 客户端: FIN_WAIT_1 → FIN_WAIT_2 → TIME_WAIT
echo " 在netstat中你会看到客户端 TIME_WAIT\n";
echo " 服务端进入 CLOSE_WAIT (因为收到了FIN)\n\n";
// 2. 服务端关闭
echo "关闭服务端连接\n";
socket_close($conn); // 服务端: CLOSE_WAIT → ... → CLOSED
socket_close($server);
}
demo_tcp_states();
echo "\n各状态简要说明:\n";
echo " CLOSED - 连接不存在,起始/终点\n";
echo " LISTEN - 服务端等待连接(socket_listen之后)\n";
echo " SYN_SENT - 客户端发了SYN,等待SYN+ACK(connect中)\n";
echo " SYN_RCVD - 服务端收到SYN,已发SYN+ACK,等ACK\n";
echo " ESTABLISHED - 连接正常,数据通信中\n";
echo " FIN_WAIT_1 - 主动方发了FIN,等ACK\n";
echo " FIN_WAIT_2 - 主动方收到ACK,等对方FIN\n";
echo " CLOSE_WAIT - 被动方收到FIN,发ACK,等应用层关闭\n";
echo " CLOSING - 同时关闭场景,发FIN后收到FIN\n";
echo " LAST_ACK - 被动方发了FIN,等ACK\n";
echo " TIME_WAIT - 主动方完成所有操作,等2MSL\n";
// ===================================================================
// 312. 2MSL等待
// 313. TIME_WAIT的作用
// ===================================================================
section("312-313", "2MSL等待 与 TIME_WAIT的作用");
/*
* 【大白话】
* MSL = Maximum Segment Lifetime = TCP包最長存活时间
* Linux: 30秒, Windows: 120秒
*
* 2MSL = 2 × MSL
* Linux: 60秒, Windows: 240秒(4分钟!)
*
* TIME_WAIT持续2MSL的两个原因:
* 1. 确保最后的ACK能到达对方 —— 如果ACK丢了对方会重发FIN
* 2. 确保旧连接的所有残余包消失 —— 防止串到新连接
*/
echo "操作系统默认MSL值:\n";
echo " Linux: 30秒 → 2MSL = 60秒\n";
echo " Windows: 120秒 → 2MSL = 240秒(4分钟!)\n";
echo " BSD/macOS: 30秒 → 2MSL = 60秒\n\n";
echo "TIME_WAIT的两个作用:\n\n";
echo "作用1: 保证连接可靠终止\n";
echo " 如果最后一个ACK丢了:\n";
echo " → 被动方超时,重发FIN\n";
echo " → 主动方还在TIME_WAIT,能收到并回复ACK\n";
echo " → 如果主动方直接关了(CLOSED),收到重发的FIN会回RST\n\n";
echo "作用2: 防止旧包串到新连接\n";
echo " 旧连接: 客户端(IP_A:50000) ↔ 服务端(IP_B:80)\n";
echo " 新连接: 客户端(IP_A:50000) ↔ 服务端(IP_B:80) ← 完全相同的四元组!\n";
echo " 如果不等2MSL:\n";
echo " → 旧连接的延迟数据包到达 → 被当成新连接的数据 → 数据错乱\n";
echo " 等2MSL后: 所有旧包都超时消失了 → 安全\n\n";
echo "TIME_WAIT太多怎么办?\n";
echo " 1. socket_set_option(\$sock, SOL_SOCKET, SO_REUSEADDR, 1);\n";
echo " 2. 让客户端主动关闭(客户端端口随机,影响小)\n";
echo " 3. 使用连接池(减少建立/关闭次数)\n";
echo " 4. Linux: sysctl net.ipv4.tcp_tw_reuse=1\n\n";
// PHP代码演示
echo "📌 查看TIME_WAIT:\n";
echo " netstat -an | findstr TIME_WAIT # Windows\n";
echo " ss -tan state time-wait # Linux\n\n";
// SO_REUSEADDR 解决端口占用
echo "📌 SO_REUSEADDR 解决端口重用问题:\n";
echo <<<'CODE'
// 即使有TIME_WAIT残留,也能立即绑定同一端口
$server = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($server, SOL_SOCKET, SO_REUSEADDR, 1);
socket_bind($server, '0.0.0.0', 8080);
// ↑ 即使8080端口还在TIME_WAIT,也能绑定成功!
CODE;
echo "\n";
// ===================================================================
// 314. 复位报文段 (RST Segment)
// ===================================================================
section("314", "复位报文段 (RST Segment)");
/*
* 【大白话】
* RST = TCP的紧急停止按钮。收到RST连接立刻死。
*
* RST vs FIN:
* FIN: 正常关门,有来有回,优雅
* RST: 直接踹门,一步到位,暴力
*
* RST不需要ACK确认!收到RST立刻释放连接。
* 收到RST的一方不进入TIME_WAIT,直接CLOSED。
*/
echo "RST出现的典型场景 + PHP代码:\n\n";
// 场景1: 连不存在的端口
echo "场景1: 连接没有监听的端口 → RST\n";
$s1 = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if (@socket_connect($s1, '127.0.0.1', 56789)) {
echo " 意外连上了\n";
} else {
echo " ❌ " . socket_strerror(socket_last_error($s1)) . "\n";
echo " 内核收到了RST → \"连接被拒绝\"\n";
}
socket_close($s1);
// 场景2: SO_LINGER发RST
echo "\n场景2: SO_LINGER(l_onoff=1, l_linger=0) → close时发RST\n";
echo <<<'CODE'
$sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
// ... connect, 通信 ...
// 设置: close时直接发RST,不等缓冲区清空
socket_set_option($sock, SOL_SOCKET, SO_LINGER, ['l_onoff' => 1, 'l_linger' => 0]);
socket_close($sock); // ← 发送RST而不是FIN!
CODE;
echo "\n";
// 场景3: 对已关闭的连接发数据
echo "\n场景3: 对已关闭连接发数据 → 收到RST\n";
$server = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($server, SOL_SOCKET, SO_REUSEADDR, 1);
socket_bind($server, '127.0.0.1', 9101);
socket_listen($server, 1);
socket_set_nonblock($server);
$c2 = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_connect($c2, '127.0.0.1', 9101);
$tries = 0;
while (!($conn = @socket_accept($server)) && $tries < 100) { usleep(10000); $tries++; }
socket_set_block($conn);
// 服务端直接关闭
socket_close($conn);
usleep(50000);
// 客户端往已关闭的连接发数据
$result = @socket_write($c2, "hello?", 6);
if ($result === false) {
echo " ❌ 发送失败: " . socket_strerror(socket_last_error($c2)) . "\n";
echo " 连接已被RST重置\n";
} else {
echo " 写入成功(还没发现), 但读时会发现RST\n";
$tmp = '';
$n = @socket_recv($c2, $tmp, 1024, 0);
if ($n === false) {
echo " ❌ 读取失败: " . socket_strerror(socket_last_error($c2)) . "\n";
}
}
socket_close($c2);
socket_close($server);
// ===================================================================
// 315. 半打开连接 (Half-Open Connection)
// ===================================================================
section("315", "半打开连接 (Half-Open Connection)");
/*
* 【大白话】
* 半打开 = "我以为还连着,对方已经挂了"
* 这是TCP的幽灵状态,一方认为连接正常,另一方已经断开。
*
* 怎么发现半打开?
* 1. 发数据 → 对方回RST → 立刻知道
* 2. TCP Keep-Alive → 慢(默认2小时)
* 3. 应用层心跳 → 最佳方案
*/
echo "半打开连接的检测方式:\n\n";
echo "方式1: TCP Keep-Alive (系统级)\n";
echo " 优点: 不用写代码,内核帮你探测\n";
echo " 缺点: 默认2小时才探测第一个包,太慢了!\n\n";
echo "PHP设置Keep-Alive参数:\n";
echo <<<'CODE'
$sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($sock, SOL_SOCKET, SO_KEEPALIVE, 1);
// 调整Keep-Alive参数(Linux)
// TCP_KEEPIDLE: 空闲N秒后开始发探测包
// TCP_KEEPINTVL: 探测包间隔N秒
// TCP_KEEPCNT: 连续N个探测包无回复→判定断开
socket_set_option($sock, SOL_TCP, TCP_KEEPIDLE, 30); // 30秒空闲后探测
socket_set_option($sock, SOL_TCP, TCP_KEEPINTVL, 10); // 每10秒探测一次
socket_set_option($sock, SOL_TCP, TCP_KEEPCNT, 3); // 3次失败=断开
// 总共: 30 + 10*3 = 60秒 就可发现问题(比默认2小时好多了!)
CODE;
echo "\n\n";
echo "方式2: 应用层心跳 (推荐!)\n";
echo " 优点: 完全可控,及时发现,跨平台\n";
echo " 缺点: 需要写代码\n\n";
echo <<<'CODE'
// 应用层心跳 —— 最佳实践
class HeartbeatConnection {
private $sock;
public int $heartbeatInterval = 10; // 每10秒心跳
public function connect(string $host, int $port): void {
$this->sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($this->sock, SOL_SOCKET, SO_KEEPALIVE, 1);
socket_connect($this->sock, $host, $port);
socket_set_nonblock($this->sock);
}
public function sendHeartbeat(): bool {
$result = @socket_write($this->sock, "PING\n", 5);
if ($result === false) return false;
// 等PONG回复, 超时3秒
$read = [$this->sock];
$write = null;
$except = [$this->sock];
$ready = socket_select($read, $write, $except, 3, 0);
if ($ready === 0) return false; // 超时
if ($ready === false) return false; // 错误
$buf = '';
$n = @socket_recv($this->sock, $buf, 1024, 0);
if ($n === false || $n === 0) return false; // 连接断开
return trim($buf) === 'PONG';
}
public function runLoop(): void {
$missCount = 0;
while (true) {
sleep($this->heartbeatInterval);
if (!$this->sendHeartbeat()) {
$missCount++;
echo "心跳失败 ({$missCount}/3)\n";
if ($missCount >= 3) {
echo "连接断开,重连中...\n";
$this->reconnect();
$missCount = 0;
}
} else {
$missCount = 0;
}
}
}
}
CODE;
echo "\n";
// ===================================================================
// 316. 连接队列
// ===================================================================
section("316", "连接队列");
/*
* 【大白话】
* socket_listen() 背后有两个队列:
*
* 1. SYN队列(半连接队列): 放还在握手中的半连接(SYN_RCVD)
* 2. ACCEPT队列(全连接队列): 放已完成握手的连接,等accept()来取
*
* backlog 参数设置的是 ACCEPT队列 的大小。
*
* 如果ACCEPT队列满了:
* 新的完成握手的连接会被丢弃
* 客户端看到连接成功但立刻被断开
*/
echo "连接队列示意图:\n\n";
echo " 客户端SYN → ┌─────────┐ ┌──────────┐ ┌────────┐\n";
echo " │ SYN队列 │ ──→ │ ACCEPT队列│ ──→│ accept │\n";
echo " SYN_RCVD │(半连接) │ │ (全连接) │ │ 返回 │\n";
echo " └─────────┘ └──────────┘ └────────┘\n";
echo " 大小由内核 backlog参数 应用程序\n";
echo " tcp_max_syn_ 控制(默认128) 取走连接\n";
echo " backlog控制\n\n";
echo "PHP代码:\n";
echo <<<'CODE'
// backlog = 128 (推荐值)
$server = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($server, SOL_SOCKET, SO_REUSEADDR, 1);
socket_bind($server, '0.0.0.0', 8080);
socket_listen($server, 128); // ACCEPT队列最多128个
// 注意: 实际最大队列 = min(backlog, /proc/sys/net/core/somaxconn)
// 高并发服务:
// 1. 增大backlog: socket_listen($sock, 1024);
// 2. 增大somaxconn: echo 1024 > /proc/sys/net/core/somaxconn (Linux)
// 3. 增大SYN队列: echo 1024 > /proc/sys/net/ipv4/tcp_max_syn_backlog (Linux)
// 4. 启用SYN Cookie防攻击
CODE;
echo "\n";
echo "队列满的后果:\n";
echo " SYN队列满 + 无SYN Cookie → 新SYN被丢弃(不回复) → SYN Flood\n";
echo " SYN队列满 + SYN Cookie → 用Cookie绕过 → OK\n";
echo " ACCEPT队列满 → 已完成握手的连接被丢弃 → 客户端看到reset\n";
// ===================================================================
// 317. SYN泛洪攻击 (SYN Flood)
// 318. SYN Cookie
// ===================================================================
section("317-318", "SYN泛洪攻击 与 SYN Cookie防御");
/*
* 【大白话 - SYN Flood】
* 攻击者发大量SYN,但不回最后的ACK。
* 服务端每个SYN都要分配内存、进SYN队列。
* 队列满了,正常用户连不进来。
*
* 【大白话 - SYN Cookie】
* 收到SYN不分配内存,而是把连接信息加密后放进SYN+ACK的序列号里。
* 收到最后的ACK时,从ACK号反解出信息验证。
* 零内存开销,完美防御SYN Flood。
*/
echo "=== SYN Flood 攻击原理 ===\n\n";
echo " 攻击者: 服务端:\n";
echo " 发SYN(源IP伪造) ──────────→ 存半连接, 回SYN+ACK\n";
echo " 不回ACK(源IP伪造的) 半连接一直挂着\n";
echo " 发SYN(新伪造IP) ──────────→ 又存半连接...\n";
echo " 发SYN × 1000... SYN队列满了!\n";
echo " ↓\n";
echo " 正常用户发SYN ──────────→ 被丢弃!服务不可用!\n\n";
echo "=== SYN Cookie 防御原理 ===\n\n";
echo " 普通方式: 收到SYN → 分配TCB(内存) → 存状态 → 回SYN+ACK\n";
echo " SYN Cookie: 收到SYN → 不分配内存! → 算出Cookie → 当ISN回\n\n";
echo "SYN Cookie工作流程:\n";
echo " 1. 客户端→SYN→服务端\n";
echo " 2. 服务端: 计算 Cookie = hash(源IP,源端口,目的IP,目的端口,时间戳,密钥)\n";
echo " Cookie 放到 ISN (序列号) 里 → SYN+ACK\n";
echo " 3. 客户端→ACK (ACK号=ISN+1)→服务端\n";
echo " 4. 服务端: 从 ACK号-1 反出 Cookie, 重新hash验证\n";
echo " 通过 → 直接进入ESTABLISHED (跳过SYN_RCVD!)\n";
echo " 不通过 → 丢弃 (可能是伪造的ACK)\n\n";
echo "PHP概念实现SYN Cookie:\n";
echo <<<'CODE'
// SYN Cookie 概念演示
define('SYN_COOKIE_SECRET', 'your_secret_key_' . date('YmdH')); // 每小时换密钥
function make_syn_cookie(string $src_ip, int $src_port, string $dst_ip, int $dst_port): int {
// 把连接信息和时间窗口哈希成一个32位的Cookie
$input = "{$src_ip}:{$src_port}->{$dst_ip}:{$dst_port}:" . floor(time() / 64);
$hash = crc32($input . SYN_COOKIE_SECRET);
// 低5位存储MSS索引,高27位存哈希
$mss_idx = 3; // MSS=1460对应索引3
return (($hash & 0xFFFFFFE0) | ($mss_idx & 0x1F)) & 0xFFFFFFFF;
}
function verify_syn_cookie(int $cookie, string $src_ip, int $src_port,
string $dst_ip, int $dst_port): bool {
$expected = make_syn_cookie($src_ip, $src_port, $dst_ip, $dst_port);
// 允许时间窗口差异
return ($cookie & 0xFFFFFFE0) === ($expected & 0xFFFFFFE0);
}
// 模拟
$cookie = make_syn_cookie('10.0.0.1', 12345, '192.168.1.1', 80);
echo "生成Cookie(ISN): $cookie\n";
// 客户端回ACK时, ACK号 = cookie + 1
$ack = $cookie + 1;
$extracted_cookie = $ack - 1;
$valid = verify_syn_cookie($extracted_cookie, '10.0.0.1', 12345, '192.168.1.1', 80);
echo "验证结果: " . ($valid ? "✅ 通过" : "❌ 失败") . "\n";
CODE;
echo "\n\n";
echo "Linux启用SYN Cookie:\n";
echo " sysctl net.ipv4.tcp_syncookies=1 → SYN队列满时才用\n";
echo " sysctl net.ipv4.tcp_syncookies=2 → 无条件启用\n\n";
echo "SYN Cookie的缺点:\n";
echo " 1. 不能携带TCP选项(MSS、窗口缩放等) → 会降低性能\n";
echo " 2. 序列号空间被占用一部分\n";
echo " 3. 但被攻击时,有缺点也比服务完全不可用强!\n";
// ===================================================================
// 319. 初始序列号(ISN)
// ===================================================================
section("319", "初始序列号(ISN)");
/*
* 【大白话】
* ISN = 每个TCP连接起始的序列号。
*
* 不能从0开始的原因:
* 1. 安全: 如果ISN可预测,攻击者可以伪造TCP包、RST你的连接
* 2. 可靠: 防止旧连接的包串到新连接
*
* 现代ISN生成:
* 基于时间(每4微秒+1) + 随机扰动 + 加密哈希
* 每个连接ISN都不同,难以预测
*/
echo "ISN生成原则:\n";
echo " 1. 每个连接的ISN应该不同\n";
echo " 2. ISN应该不可预测(防止攻击)\n";
echo " 3. ISN随时间递增(防止旧包混淆)\n\n";
echo "PHP模拟安全的ISN生成:\n";
echo <<<'CODE'
function generate_secure_isn(): int {
// 微秒时间戳 × 随机种子 + 随机偏移
$time_us = (int)(microtime(true) * 1000000);
$random = random_int(0, 0xFFFF);
$isn = (($time_us * 31337 + $random * 65537) & 0xFFFFFFFF);
// 再加一层哈希(真实实现用MD5/SHA1)
$isn = crc32($isn . random_int(0, PHP_INT_MAX));
return $isn & 0xFFFFFFFF; // 保证32位
}
echo "ISN1 = " . generate_secure_isn() . "\n";
usleep(100);
echo "ISN2 = " . generate_secure_isn() . "\n"; // 每次都不一样
CODE;
echo "\n\n";
echo "ISN在PHP中:\n";
echo " ISN由内核在 connect() 时自动生成\n";
echo " 应用层无法设置(也不需要设置)\n";
echo " 即使PHP用raw socket,ISN也由内核设定\n";
// ===================================================================
// 320. 端口号分配
// ===================================================================
section("320", "端口号分配");
/*
* 【大白话】
* IANA(互联网号码分配机构)把端口分成三块:
*
* 0~1023: 知名端口 - 需要root/管理员权限
* 1024~49151: 注册端口 - 普通用户可用
* 49152~65535: 动态端口 - 系统自动分配给客户端
*/
echo "端口号三段式 (IANA管理):\n\n";
echo "【知名端口 0-1023】\n";
$well_known = [
20 => 'FTP数据', 21 => 'FTP控制', 22 => 'SSH',
23 => 'Telnet', 25 => 'SMTP', 53 => 'DNS',
80 => 'HTTP', 110 => 'POP3', 143 => 'IMAP',
443 => 'HTTPS', 993 => 'IMAPS', 995 => 'POP3S',
];
foreach ($well_known as $p => $s) echo " {$p} → {$s}\n";
echo "\n【注册端口 1024-49151】\n";
$registered = [
3306 => 'MySQL', 5432 => 'PostgreSQL',
6379 => 'Redis', 27017 => 'MongoDB',
8080 => 'HTTP备', 8443 => 'HTTPS备',
9092 => 'Kafka', 11211 => 'Memcached',
];
foreach ($registered as $p => $s) echo " {$p} → {$s}\n";
echo "\n【动态端口 49152-65535】\n";
echo " 客户端connect时系统自动从这里面选\n";
// 演示: 看实际分配的端口
echo "\n📌 看看系统给你分配了什么临时端口:\n";
$demo_sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
$server = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($server, SOL_SOCKET, SO_REUSEADDR, 1);
socket_bind($server, '127.0.0.1', 9200);
socket_listen($server, 1);
socket_connect($demo_sock, '127.0.0.1', 9200);
socket_getsockname($demo_sock, $addr, $port);
echo " 临时端口 = {$port} (应在49152-65535之间)\n";
// 查看五个连接的临时端口分配
echo "\n 连续5个连接的临时端口:\n";
for ($i = 0; $i < 5; $i++) {
$cs = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_connect($cs, '127.0.0.1', 9200);
socket_getsockname($cs, $addr, $p);
echo " 连接{$i}: {$p}\n";
// accept 防止队列满
socket_set_nonblock($server);
$ac = @socket_accept($server);
if ($ac) socket_close($ac);
socket_close($cs);
}
echo "\n 这些端口都是系统自动分配的临时端口\n";
socket_close($demo_sock);
socket_close($server);
// ===================================================================
// 总结: 完整TCP通信示例 (融合所有概念)
// ===================================================================
section("总结", "完整TCP通信示例 (融合所有概念)");
echo "📌 一个生产级的TCP服务器模板:\n\n";
echo <<<'CODE'
<?php
// 生产级TCP服务器 (融合了几乎所有TCP概念)
class RobustTCPServer {
private $server;
private array $clients = [];
private array $clientInfo = []; // 存客户端信息
public function __construct(string $host, int $port, int $backlog = 128) {
$this->server = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
// 端口重用 (解决TIME_WAIT问题)
socket_set_option($this->server, SOL_SOCKET, SO_REUSEADDR, 1);
// TCP Keep-Alive (防半打开连接)
socket_set_option($this->server, SOL_SOCKET, SO_KEEPALIVE, 1);
socket_bind($this->server, $host, $port);
socket_listen($this->server, $backlog); // 连接队列
socket_set_nonblock($this->server); // 非阻塞
echo "服务器启动: {$host}:{$port}\n";
}
public function run(): void {
while (true) {
$read = array_merge([$this->server], $this->clients);
$write = null;
$except = null;
// select多路复用 —— TCP高效处理的核心
if (socket_select($read, $write, $except, 1) > 0) {
// 新连接 (三次握手完成)
if (in_array($this->server, $read)) {
$conn = @socket_accept($this->server);
if ($conn) {
socket_set_nonblock($conn);
socket_set_option($conn, SOL_SOCKET, SO_KEEPALIVE, 1);
$id = (int)$conn;
$this->clients[$id] = $conn;
socket_getpeername($conn, $addr, $port);
$this->clientInfo[$id] = ['addr' => $addr, 'port' => $port, 'last_active' => time()];
echo "[连接] {$addr}:{$port} (当前在线: " . count($this->clients) . ")\n";
}
}
// 处理客户端数据
foreach ($this->clients as $id => $client) {
if (in_array($client, $read)) {
$buf = '';
$n = @socket_recv($client, $buf, 8192, 0);
if ($n === false) {
// 读取错误 → 关闭
$this->closeClient($id);
} elseif ($n === 0) {
// 读到0 = 对方发了FIN (半关闭)
echo "[关闭] 客户端{$id} 发送了FIN\n";
$this->closeClient($id);
} else {
// 正常数据
$this->clientInfo[$id]['last_active'] = time();
// Echo回去
socket_write($client, "Echo: " . $buf);
}
}
}
}
// 心跳检测 (检查半打开连接)
$now = time();
foreach ($this->clientInfo as $id => $info) {
if ($now - $info['last_active'] > 300) { // 5分钟无活动
echo "[超时] 客户端{$id} ({$info['addr']}:{$info['port']}) 无活动,关闭\n";
$this->closeClient($id);
}
}
}
}
private function closeClient(int $id): void {
if (isset($this->clients[$id])) {
// 优雅关闭
@socket_shutdown($this->clients[$id], 1); // 发FIN → 四次挥手
socket_close($this->clients[$id]);
unset($this->clients[$id], $this->clientInfo[$id]);
}
}
}
// 启动
$server = new RobustTCPServer('0.0.0.0', 8080, 256);
$server->run();
CODE;
echo "\n";
// ===================================================================
// 最终参考速查表
// ===================================================================
section("速查表", "TCP核心概念速查");
echo <<<'TABLE'
┌────────────────────┬──────────────────────────────────────────────┐
│ TCP首部固定长度 │ 20字节 │
│ TCP首部最大长度 │ 60字节 (20固定 + 40选项) │
│ 源/目的端口 │ 各16位,范围0~65535 │
│ 序列号(SEQ) │ 32位,字节编号 │
│ 确认号(ACK号) │ 32位,期望收到的下一个字节号 │
│ 数据偏移 │ 4位,单位4字节,范围5~15 │
│ 标志位 │ 9个: NS,CWR,ECE,URG,ACK,PSH,RST,SYN,FIN │
│ 窗口大小 │ 16位,最大65535(可窗口缩放扩大) │
│ 检验和 │ 16位,反码求和,端到端校验 │
│ 紧急指针 │ 16位,指向紧急数据位置 │
├────────────────────┼──────────────────────────────────────────────┤
│ 三次握手 │ SYN → SYN+ACK → ACK │
│ 四次挥手 │ FIN → ACK → FIN → ACK │
│ SYN标志 │ 只在建立连接时使用,消耗1个序列号 │
│ FIN标志 │ 关闭连接,也消耗1个序列号 │
│ RST标志 │ 异常终止,不消耗序列号 │
│ ACK标志 │ 连接建立后几乎所有包都带ACK │
│ PSH标志 │ 催促接收方尽快交给应用 │
│ URG标志 │ 紧急数据,几乎不用 │
├────────────────────┼──────────────────────────────────────────────┤
│ MSS │ 最大段大小,典型值1460(以太网) │
│ 窗口缩放 │ 让窗口从64KB扩大到1GB │
│ 时间戳 │ 精确RTT计算 + 防序列号绕回(PAWS) │
│ SACK │ 选择性确认,精准重传丢包段 │
│ SYN Cookie │ 防SYN Flood,零内存开销 │
├────────────────────┼──────────────────────────────────────────────┤
│ TIME_WAIT │ 主动关闭方等2MSL(60s Linux, 240s Windows) │
│ 2MSL作用 │ 1.确保最后ACK到达 2.防止旧包串新连接 │
│ SO_REUSEADDR │ 允许重用TIME_WAIT的端口 │
│ SO_KEEPALIVE │ TCP层心跳探测(默认2小时,太慢) │
│ TCP_NODELAY │ 禁用Nagle算法,立即发送 │
│ backlog │ socket_listen参数,控制ACCEPT队列大小 │
├────────────────────┼──────────────────────────────────────────────┤
│ socket_create() │ 创建socket (相当于买电话) │
│ socket_bind() │ 绑定端口 (相当于分配电话号码) │
│ socket_listen() │ 开始监听 (等待来电) │
│ socket_accept() │ 接受连接 (接电话) │
│ socket_connect() │ 发起连接 → 三次握手 (拨号) │
│ socket_write/send()│ 发送数据 (内核封装TCP首部) │
│ socket_recv/read() │ 接收数据 (内核处理ACK,去首部) │
│ socket_shutdown() │ 半关闭 → 发FIN (我打完了) │
│ socket_close() │ 完全关闭 → 四次挥手 │
│ socket_select() │ 多路复用 → 同时处理多个连接 │
└────────────────────┴──────────────────────────────────────────────┘
TABLE;
echo "\n\n";
echo "╔══════════════════════════════════════════════════╗\n";
echo "║ TCP 完整指南 274~320 全部完毕! ║\n";
echo "║ 47个知识点 + PHP Sockets 代码示例 ║\n";
echo "╚══════════════════════════════════════════════════╝\n";
/**
* ===================================================================
* 参考:
* - RFC 793: TCP协议规范 (原始)
* - RFC 1323: TCP高性能扩展 (窗口缩放、时间戳)
* - RFC 2018: TCP SACK选项
* - RFC 4987: TCP SYN Flood攻击与防御
* - RFC 6298: TCP重传超时计算
*
* PHP Socket 官方文档:
* - https://www.php.net/manual/en/book.sockets.php
* ===================================================================
*/
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)