<?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
 * ===================================================================
 */
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐