欢迎光临
我们一直在努力

TCP/ip详解=TCP数据流与窗口管理

<?php
/**
* ===================================================================
* TCP 数据流与窗口管理 (323-342)
* 大白话解释 + PHP Sockets 完整代码示例
* ===================================================================
*
* 运行环境: PHP 7.4+ (extension=sockets)
*
* 本文件涵盖:
* 323. 交互式数据流 324. 成块数据流
* 325. 经受时延的确认 326. Nagle算法
* 327. 滑动窗口协议 328. 窗口通告
* 329. 通告窗口 330. 有效窗口
* 331. 发送窗口 332. 接收窗口
* 333. 零窗口 334. 窗口探测
* 335. 糊涂窗口综合征 336. 接收方SWS避免
* 337. 发送方SWS避免 338. PUSH标志的使用
* 339. 滑动窗口动态变化 340. 窗口左右边沿移动
* 341. 关闭窗口 342. 数据缓存
*/

echo "╔══════════════════════════════════════════════════╗\\n";
echo "║ TCP 数据流与窗口管理 323~342 ║\\n";
echo "║ 大白话 + PHP Sockets 完整代码 ║\\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";
}

function check_sockets(): void {
if (!extension_loaded('sockets')) {
die("❌ 请开启 sockets 扩展: php.ini 中 extension=sockets\\n");
}
}
check_sockets();

function create_server(int $port): \\Socket {
$srv = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($srv, SOL_SOCKET, SO_REUSEADDR, 1);
socket_bind($srv, '127.0.0.1', $port);
socket_listen($srv, 3);
socket_set_nonblock($srv);
return $srv;
}

function wait_accept($server, int $timeout_ms = 2000): \\Socket|false {
$tries = 0;
$max = $timeout_ms / 10;
while (!($conn = @socket_accept($server)) && $tries < $max) {
usleep(10000);
$tries++;
}
if ($conn) socket_set_block($conn);
return $conn ?: false;
}

function echo_result(string $label, $result): void {
if ($result === false) {
echo " {$label}: ❌ " . socket_strerror(socket_last_error()) . "\\n";
} elseif ($result === 0 || $result === '') {
echo " {$label}: (空/0字节)\\n";
} else {
echo " {$label}: \\"{$result}\\"\\n";
}
}

// ===================================================================
// 323. 交互式数据流
// ===================================================================

section("323", "交互式数据流");

/*
* 【大白话】
* 交互式数据流 = 小数据包、频繁发送的模式。
*
* 典型场景:
* – Telnet/SSH: 你每敲一个键就发一个包(1字节数据)
* – 在线游戏: 操作指令频繁发送,每个包很小
* – 聊天工具: 每条消息独立发送
*
* 问题:
* 每个小包都有 20字节IP首部 + 20字节TCP首部 = 40字节开销
* 你发1字节数据,实际在网络上走了41字节!
* 这叫"糊涂窗口综合征"的前兆。
*
* 解决方案:
* – Nagle算法(默认开启): 攒够数据再发
* – 但如果延迟敏感(游戏),通常要关掉Nagle
*/

echo "交互式数据流特征:\\n";
echo " – 包很小(可能只有1~几十字节)\\n";
echo " – 发送频繁\\n";
echo " – 延迟敏感\\n";
echo " – 头部开销占比很大(40字节头部 vs 1字节数据 = 97.5%浪费!)\\n";

echo "\\n📌 PHP代码: 模拟交互式数据流 (Telnet风格)\\n\\n";

function interactive_flow_demo(): void {
$server = create_server(9100);
$client = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_connect($client, '127.0.0.1', 9100);
$conn = wait_accept($server);

if (!$conn) { socket_close($client); socket_close($server); return; }

echo "模拟 Telnet: 每次敲一个字符就发送\\n\\n";

$keystrokes = ['H', 'e', 'l', 'l', 'o', "\\r", "\\n"];
$total_sent = 0;
$packet_count = 0;

foreach ($keystrokes as $char) {
$len = socket_write($client, $char, 1);
$total_sent += $len;
$packet_count++;
echo " 发送 '{$char}' → 1字节数据 + 40字节头部 = 41字节在线路上\\n";
}

echo "\\n总计: {$packet_count} 个包\\n";
echo "实际数据: {$total_sent} 字节\\n";
echo "线路消耗: " . ($packet_count * 41) . " 字节 (含IP+TCP头)\\n";
echo "效率: " . round($total_sent / ($packet_count * 41) * 100, 1) . "%\\n";
echo "↑ 这就是交互式数据流的痛点: 效率极低!\\n";

// 服务端接收
$buf = '';
for ($i = 0; $i < count($keystrokes); $i++) {
$b = '';
@socket_recv($conn, $b, 1, 0);
$buf .= $b;
}
echo "\\n服务端收到: \\"" . str_replace(["\\r","\\n"], ['\\\\r','\\\\n'], $buf) . "\\"\\n";

socket_close($client);
socket_close($conn);
socket_close($server);
}
interactive_flow_demo();

// ===================================================================
// 324. 成块数据流
// ===================================================================

section("324", "成块数据流");

/*
* 【大白话】
* 成块数据流 = 大数据块、一次发完的模式。
*
* 典型场景:
* – 文件传输(FTP): 一次性发送整个文件
* – 网页加载(HTTP): 服务器把整个HTML页面发给你
* – 邮件发送(SMTP): 整封邮件一起发
*
* 特点:
* – 数据量大(几KB到几GB)
* – 不频繁发送
* – 头部开销占比很低(40字节头 vs 1460字节数据 = 2.7%)
* – 窗口管理才是关键
*/

echo "成块数据流 vs 交互式数据流:\\n\\n";
echo " 交互式 成块\\n";
echo " 每次发送量 1~几十字节 KB~MB\\n";
echo " 频率 高 低\\n";
echo " 头部开销占比 很高(97%+) 很低(<3%)\\n";
echo " 延迟敏感度 高 低\\n";
echo " 吞吐量敏感度 低 高\\n";
echo " Nagle算法 建议禁用 建议启用(默认)\\n";

echo "\\n📌 PHP代码: 模拟成块数据流 (文件传输风格)\\n\\n";

function bulk_flow_demo(): void {
$server = create_server(9101);
$client = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_connect($client, '127.0.0.1', 9101);
$conn = wait_accept($server);
if (!$conn) { socket_close($client); socket_close($server); return; }

// 构造一个大块数据(模拟文件内容)
$big_data = str_repeat("ABCDEFGHIJ", 1000); // 10KB数据
echo "准备发送: " . strlen($big_data) . " 字节 (成块数据)\\n";

// 一次性发送(内核会分片成MSS大小的多个TCP段)
$start = microtime(true);
$sent = socket_write($client, $big_data, strlen($big_data));
$elapsed = round((microtime(true) $start) * 1000, 2);

echo "发送完成: {$sent} 字节, 耗时 {$elapsed} ms\\n";
echo "内核分成约 " . ceil(strlen($big_data) / 1460) . " 个TCP段(MSS=1460)\\n";
echo "每个段: 1460数据 + 40头部 = 1500字节(刚好填满一个以太网帧!)\\n";

// 服务端读数据
$received = '';
$rounds = 0;
while (strlen($received) < $sent) {
$buf = '';
$n = socket_recv($conn, $buf, 8192, 0);
if ($n === false || $n === 0) break;
$received .= $buf;
$rounds++;
}
echo "\\n接收完成: " . strlen($received) . " 字节, 读了 {$rounds} 次\\n";
echo "↑ 虽然应用层发了1次write, 收到几次取决于内核如何分片\\n";
echo " 应用层不需要关心分片, TCP保证数据完整有序到达\\n";

socket_close($client);
socket_close($conn);
socket_close($server);
}
bulk_flow_demo();

// ===================================================================
// 325. 经受时延的确认 (Delayed ACK)
// ===================================================================

section("325", "经受时延的确认 (Delayed ACK)");

/*
* 【大白话】
* 经受时延的确认 = 收到数据不立刻回复ACK,等一等再说。
*
* 为什么要等?
* 1. 可能很快就有数据要回给对端,ACK可以"搭便车"
* (ACK放在数据包里一起发,省一个包)
* 2. 可能很快就有更多数据到达,一个ACK确认多段数据
*
* 等多久?
* 一般为 200ms (最大500ms)
* 如果200ms内没有数据要发,就单独发ACK
* 如果有第二个数据段到达,立刻发ACK(不继续等)
*
* 与Nagle算法的关系:
* 延迟ACK + Nagle = 可能造成严重延迟!
* 所以很多应用禁用Nagle(TCP_NODELAY)
*/

echo "延迟ACK工作原理:\\n\\n";
echo " 收到数据段 → 启动200ms定时器\\n";
echo " ├→ 等到了数据要回 → ACK和数据一起发(省包!)\\n";
echo " ├→ 又收到第二个段 → 立刻发ACK(不等了)\\n";
echo " └→ 200ms超时了 → 单独发ACK\\n\\n";

echo "延迟ACK的问题:\\n";
echo " Nagle(攒数据) + 延迟ACK(攒确认) = 互相等待 → 严重延迟!\\n";
echo " → 延迟敏感应用要禁用Nagle(TCP_NODELAY=1)\\n";

echo "\\n📌 PHP代码: 观察延迟ACK的效果\\n\\n";

function delayed_ack_demo(): void {
$server = create_server(9102);
$client = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);

// 关键: 禁用Nagle,让每次write立刻发送
socket_set_option($client, SOL_TCP, TCP_NODELAY, 1);

socket_connect($client, '127.0.0.1', 9102);
$conn = wait_accept($server);
if (!$conn) { socket_close($client); socket_close($server); return; }

echo "发送两段小数据(间隔很近):\\n\\n";

$start = microtime(true);
socket_write($client, "AAAA", 4);
$t1 = microtime(true);
usleep(50000); // 50ms后发第二段 (小于200ms延迟)
socket_write($client, "BBBB", 4);
$t2 = microtime(true);

echo " 第1段: AAAA @ " . round(($t1$start)*1000, 1) . "ms\\n";
echo " 第2段: BBBB @ " . round(($t2$start)*1000, 1) . "ms\\n";
echo " 间隔: 50ms (小于延迟ACK的200ms)\\n\\n";
echo " 服务端可能:\\n";
echo " 1) 收到AAAA后等200ms → 然后BBBB到了 → 一个ACK确认两个\\n";
echo " 2) 收到AAAA → 200ms超时 → 单独ACK → 收到BBBB → 单独ACK\\n";
echo " ↑ 具体行为取决于操作系统实现\\n";

// 收数据
$buf = '';
socket_recv($conn, $buf, 1024, 0);
echo "\\n 服务端收到: \\"{$buf}\\"\\n";

socket_close($client);
socket_close($conn);
socket_close($server);

echo "\\n在Linux上查看延迟ACK设置:\\n";
echo " cat /proc/sys/net/ipv4/tcp_delack_min\\n";
}
delayed_ack_demo();

// ===================================================================
// 326. Nagle算法
// ===================================================================

section("326", "Nagle算法");

/*
* 【大白话】
* Nagle算法 = "别急着发小包,攒一攒再发"
*
* 规则很简单:
* 1. 如果发送缓冲区有未确认的数据,新的小数据不能发
* 2. 只能满足以下任一条件才能发:
* a) 之前的数据全部被确认了
* b) 攒够了一个MSS(1460字节)的数据
* c) 这是最后一段数据了
*
* Nagle解决的问题:
* Telnet场景: 你敲了 'H' 'e' 'l' 'l' 'o'
* 没有Nagle: 发5个包,每个41字节(1数据+40头) = 205字节
* 有Nagle: 攒成一个包 1数据+40头 + 4数据 = 45字节
* 省了160字节! (但牺牲了一点延迟)
*
* Nagle vs TCP_NODELAY:
* Nagle 开启(默认): 攒小包,省带宽,可能增加延迟
* TCP_NODELAY=1: 禁用Nagle,立刻发,低延迟
*/

echo "Nagle算法核心规则:\\n\\n";
echo " ┌─────────────────────────────────────────┐\\n";
echo " │ 有未确认的小包? → 不能发新的小包! │\\n";
echo " │ 攒够MSS(1460)? → 可以发! │\\n";
echo " │ 之前数据确认了? → 可以发! │\\n";
echo " │ 数据是最后一段? → 可以发! │\\n";
echo " └─────────────────────────────────────────┘\\n\\n";

echo "Nagle算法效果示例:\\n";
echo " 无Nagle: 'H' 'e' 'l' 'l' 'o' → 5个包, 205字节\\n";
echo " 有Nagle: 'Hello' → 1个包, 45字节\\n";
echo " 节省: 160字节 (78%!)\\n";

echo "\\n📌 PHP代码: Nagle vs TCP_NODELAY 对比\\n\\n";

function nagle_vs_nodelay_demo(): void {
$results = [];

foreach ([true, false] as $nodelay) {
$label = $nodelay ? 'TCP_NODELAY=1 (禁用Nagle)' : 'TCP_NODELAY=0 (启用Nagle,默认)';

$server = create_server($nodelay ? 9103 : 9104);
$client = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);

if ($nodelay) {
$flag = 1;
socket_set_option($client, SOL_TCP, TCP_NODELAY, $flag);
}

socket_connect($client, '127.0.0.1', $nodelay ? 9103 : 9104);
$conn = wait_accept($server);
if (!$conn) { socket_close($client); socket_close($server); continue; }

echo "{$label}:\\n";

// 发送5个小字节(模拟Telnet)
$start = microtime(true);
$bytes = ['A', 'B', 'C', 'D', 'E'];
foreach ($bytes as $b) {
socket_write($client, $b, 1);
}
$send_time = round((microtime(true) $start) * 1000, 2);

// 接收
$received = '';
$read_count = 0;
socket_set_nonblock($conn);
for ($i = 0; $i < 20; $i++) { // 最多等200ms
$buf = '';
$n = @socket_recv($conn, $buf, 1024, 0);
if ($n > 0) { $received .= $buf; $read_count++; }
if (strlen($received) >= 5) break;
usleep(10000); // 10ms
}

echo " 发送5字节耗时: {$send_time}ms\\n";
echo " 服务端读了: {$read_count} 次\\n";
echo " 收到数据: \\"{$received}\\"\\n";

if ($nodelay) {
echo " → 禁用Nagle: 可能收到≥1次(每个小包立刻发)\\n";
} else {
echo " → 启用Nagle: 可能收到1次(小包被合并了)\\n";
echo " (本地回环太快,效果不明显;跨网络效果显著)\\n";
}
echo "\\n";

$results[] = ['label' => $label, 'reads' => $read_count, 'time' => $send_time];

socket_close($client);
socket_close($conn);
socket_close($server);
}

echo "【结论】\\n";
echo " 延迟敏感(游戏/SSH) → TCP_NODELAY=1 → 立刻发送\\n";
echo " 吞吐敏感(文件传输) → 默认Nagle即可 → 节省带宽\\n";
}
nagle_vs_nodelay_demo();

// ===================================================================
// 327. 滑动窗口协议
// ===================================================================

section("327", "滑动窗口协议");

/*
* 【大白话】
* 滑动窗口 = TCP的流量控制核心机制。
*
* 想象一个"窗口"在数据流上滑动:
* – 窗口里的数据 = 可以发送但还没收到确认的
* – 窗口左边的 = 已经发送+确认了的
* – 窗口右边的 = 还不能发送的
*
* 窗口大小 = 对端通告的接收窗口
* "你能收多少,我最多就发多少"
*
* 滑动:
* 收到ACK → 窗口右移 → 可以发新数据了
*
* 这就像传送带: 你把东西放上传送带(发送),
* 对端取走(确认),传送带空出位置,你可以放更多。
*/

echo "滑动窗口可视化:\\n\\n";

echo " 数据流: [已确认| 窗口内(可发) | 不可发 ]\\n";
echo " ← 左边界 右边界 →\\n\\n";

echo " 示例: 窗口大小=8\\n";
echo " 初始: [ |80|81|82|83|84|85|86|87| 88|89… ]\\n";
echo " ↑窗口起始 ↑窗口结束\\n\\n";

echo " 发了4个: [ |80|81|82|83|84|85|86|87| 88|89… ]\\n";
echo " ↑已发未确认\\n\\n";

echo " 收到80-81的ACK: [ | | |82|83|84|85|86|87|88|89| 90… ]\\n";
echo " ↑窗口右移了2位! 现在能发88,89了\\n\\n";

echo "滑动窗口的核心思想:\\n";
echo " 1. 不用等每个包确认再发下一个(停等协议)\\n";
echo " 2. 一次发一堆(流水线),效率高\\n";
echo " 3. 窗口大小由接收方控制(流量控制)\\n";
echo " 4. 发送方不能超过窗口大小\\n";

echo "\\n📌 PHP代码: 滑动窗口模拟\\n\\n";

function sliding_window_sim(): void {
echo "=== 滑动窗口协议模拟 ===\\n\\n";

// 配置
$window_size = 8; // 通告窗口8字节
$total_data = "HELLOWORLD!!"; // 12字节要发送
$mss = 4; // 每段最多发4字节

// 发送方视角
$send_base = 0; // 窗口左边界(已确认的最小序号)
$next_seq = 0; // 下一个要发的序号
$sent_until = 0; // 已发到的序号
$data = $total_data;

echo "总数据: \\"{$data}\\" ({strlen($data)}字节)\\n";
echo "窗口大小: {$window_size}\\n";
echo "MSS: {$mss}\\n\\n";

$round = 0;
while ($send_base < strlen($data)) {
$round++;
$window_end = $send_base + $window_size;
echo "第{$round}轮: 窗口 = [{$send_base}, {$window_end}), 已发到={$sent_until}\\n";

// 能发多少?
while ($sent_until < $window_end && $sent_until < strlen($data)) {
$chunk_len = min($mss, strlen($data) $sent_until);
$chunk = substr($data, $sent_until, $chunk_len);
echo " 发送 SEQ={$sent_until}: \\"{$chunk}\\" ({$chunk_len}字节)\\n";
$sent_until += $chunk_len;
}

// 模拟收到ACK (简化: 每次确认前2字节)
$ack = min($send_base + 3, $sent_until);
if ($ack > $send_base) {
echo " 收到 ACK={$ack} → 窗口右移到{$ack}\\n";
$send_base = $ack;
} else {
// 没收到ACK,可能会超时重传
echo " 未收到新ACK, 窗口不动\\n";
$send_base = min($send_base + 2, strlen($data)); // 模拟推进
}
echo " 当前窗口位置: 左边界={$send_base}\\n\\n";

if ($round > 20) { echo "…超时\\n"; break; }
}

echo "发送完成!\\n";
}
sliding_window_sim();

// ===================================================================
// 328. 窗口通告 / 329. 通告窗口
// ===================================================================

section("328-329", "窗口通告 (Window Advertisement)");

/*
* 【大白话】
* 窗口通告 = 接收方告诉发送方"我还能收多少"
*
* 每个TCP包的头部都有"窗口大小"字段(16位),
* 接收方每次回复ACK都带上这个值。
*
* 通告窗口 = 接收缓冲区的剩余空间
* = 总缓冲区大小 – 已接收但应用还没读走的数据
*
* 通告窗口会动态变化:
* – 应用读走得快 → 窗口变大(或不变)
* – 应用读得慢 → 窗口变小
* – 应用完全没读 → 窗口→0 (零窗口!)
*
* 类比:
* 你的碗(缓冲区)大小是固定的,
* 朋友给你夹菜(发送数据),
* 你吃掉(应用读取)一些,碗里就有空间了,
* 你告诉朋友"还能放N口" = 通告窗口。
*/

echo "通告窗口的含义:\\n\\n";
echo " 接收方每次ACK都告诉发送方:\\n";
echo " \\"我的接收缓冲区还剩 X 字节\\"\\n";
echo " 发送方就据此控制发送速度\\n\\n";

echo "通告窗口的计算:\\n";
echo " ┌────────── 总接收缓冲区 ──────────┐\\n";
echo " │ 已收未读 │ 空闲空间 │\\n";
echo " └───────────┴──────────────────────┘\\n";
echo " ↑ 这个就是通告窗口\\n";
echo " 通告窗口 = 总缓冲 – 已收未读\\n";

echo "\\n📌 PHP代码: 接收方窗口通告模拟\\n\\n";

function window_advertise_demo(): void {
$server = create_server(9105);
$client = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
// 禁用Nagle,确保每次write都能看到独立的包
socket_set_option($client, SOL_TCP, TCP_NODELAY, 1);
socket_connect($client, '127.0.0.1', 9105);
$conn = wait_accept($server);
if (!$conn) { socket_close($client); socket_close($server); return; }

// 查看接收缓冲区大小 (决定最大通告窗口)
$rcvbuf = 0;
socket_get_option($conn, SOL_SOCKET, SO_RCVBUF, $rcvbuf);
echo "接收缓冲区大小: {$rcvbuf} 字节 (最大通告窗口)\\n\\n";

// 查看发送缓冲区大小
$sndbuf = 0;
socket_get_option($client, SOL_SOCKET, SO_SNDBUF, $sndbuf);
echo "发送缓冲区大小: {$sndbuf} 字节 (发送窗口上限)\\n\\n";

// 发一大块数据,填满接收方缓冲区
$big = str_repeat('X', 10000);
socket_write($client, $big, 10000);
echo "发送了10000字节\\n";

// 服务端慢慢读(模拟应用处理慢)
$total_rcv = 0;
for ($i = 0; $i < 5; $i++) {
$buf = '';
socket_recv($conn, $buf, 500, 0);
$total_rcv += strlen($buf);
$remaining = 10000 $total_rcv;
$est_buf_free = $rcvbuf $remaining;
if ($est_buf_free < 0) $est_buf_free = 0;
echo " 第" . ($i+1) . "次读: 读了500, 共{$total_rcv}, ";
echo "缓冲剩余约{$est_buf_free}字节(通告窗口)\\n";
usleep(50000); // 模拟处理延迟
}

echo "\\n每次服务端读了数据, 缓冲空闲变大, 通告窗口就变大\\n";
echo "客户端看到窗口变大, 就能继续发了(流量控制)\\n";

socket_close($client);
socket_close($conn);
socket_close($server);
}
window_advertise_demo();

// ===================================================================
// 330. 有效窗口 / 331. 发送窗口 / 332. 接收窗口
// ===================================================================

section("330-332", "有效窗口 / 发送窗口 / 接收窗口");

/*
* 【大白话】
* 这三个概念互相关联:
*
* 接收窗口(receive window):
* 接收方实际能用的窗口 = 通告窗口
* 由接收方的缓冲区剩余空间决定
*
* 发送窗口(send window):
* 发送方当前能发送的数据范围
* 由接收方通告的窗口大小决定
*
* 有效窗口(usable window):
* = 发送窗口 – 已发送但未确认的数据
* = 发送方现在还能立即发送的数据量
*
* 三者关系:
* 接收窗口 → 通告 → 发送窗口 → 减去已发未确认 → 有效窗口
*
* 发送方视角:
* ┌──────┬──────────┬──────────┬────────────┐
* │已确认│已发未确认 │可立即发送 │ 还不能发送 │
* └──────┴──────────┴──────────┴────────────┘
* ↑有效窗口 ↑发送窗口到此
*/

echo "发送方看到的窗口:\\n\\n";
echo " ┌────────────────── 发送窗口 ──────────────────┐\\n";
echo " │ 已发未确认 │ 有效窗口 │\\n";
echo " │ (在路上) │ (现在就能发的) │\\n";
echo " └──────────────┴───────────────────────────────┘\\n";
echo " ← 左边界 右边界 →\\n\\n";

echo "具体例子:\\n";
echo " 通告窗口 = 8000 字节\\n";
echo " 已发未确认 = 3000 字节(发了3个1KB的包)\\n";
echo " 有效窗口 = 8000 – 3000 = 5000 字节\\n";
echo " \\"我还能立刻发5000字节\\"\\n";

echo "\\n📌 PHP代码: 发送窗口和有效窗口计算\\n\\n";

function send_window_demo(): void {
echo "=== 发送方窗口计算模拟 ===\\n\\n";

// 模拟发送方状态
class SendWindow {
public int $left = 0; // 窗口左边界 (已确认)
public int $next = 0; // 下一个要发的序号
public int $right = 0; // 窗口右边界
public int $windowSize = 0;// 对端通告的窗口大小
public array $inFlight = []; // 已发未确认的包 [start=>len]

public function updateWindow(int $advertised): void {
$this->windowSize = $advertised;
$this->right = $this->left + $advertised;
}

public function usableWindow(): int {
$unacked = array_sum($this->inFlight);
return $this->windowSize $unacked;
}

public function canSend(int $len): bool {
return $this->usableWindow() >= $len && ($this->next + $len) <= $this->right;
}

public function send(int $len): int {
if (!$this->canSend($len)) return 0;
$seq = $this->next;
$this->inFlight[(string)$seq] = $len;
$this->next += $len;
return $seq;
}

public function ackReceived(int $ack): void {
// 检查哪些包被确认了
foreach ($this->inFlight as $seqStr => $len) {
$seq = (int)$seqStr;
if ($seq + $len <= $ack) {
unset($this->inFlight[$seqStr]);
}
}
if ($ack > $this->left) {
$this->left = $ack;
}
$this->right = $this->left + $this->windowSize;
}

public function status(): string {
return "左边界={$this->left} 下一个={$this->next} 右边界={$this->right} " .
"窗口={$this->windowSize} 有效={$this->usableWindow()} 飞行中包=" .
count($this->inFlight);
}
}

$sw = new SendWindow();

// 收到通告窗口=8000
$sw->updateWindow(8000);
echo "初始: 收到通告窗口=8000 → {$sw->status()}\\n";

// 发3个包
for ($i = 0; $i < 3; $i++) {
$seq = $sw->send(1460);
echo "发送 SEQ={$seq}, 1460字节 → {$sw->status()}\\n";
}

// 收到ACK确认了前两个包
$ack = 2920;
$sw->ackReceived($ack);
echo "收到 ACK={$ack} (确认了前2个包) → {$sw->status()}\\n";

// 现在能发更多了
echo "\\n窗口右移了! 可以继续发送\\n";
$seq = $sw->send(1460);
echo "发送 SEQ={$seq}, 1460字节 → {$sw->status()}\\n";

echo "\\n总结:\\n";
echo " 接收窗口 = 接收方通告的大小(8000)\\n";
echo " 发送窗口 = 发送方根据通告设置的窗口(8000)\\n";
echo " 有效窗口 = 发送窗口 – 已发未确认(实时变化)\\n";
}
send_window_demo();

// ===================================================================
// 333. 零窗口 (Zero Window)
// ===================================================================

section("333", "零窗口 (Zero Window)");

/*
* 【大白话】
* 零窗口 = 接收方通告窗口=0
* 意思是"我的缓冲区满了,别再发了!"
*
* 发生原因:
* 应用层来不及读数据(处理太慢)
* → 接收缓冲区塞满
* → 通告窗口=0
* → 发送方暂停发送
*
* 发送方收到零窗口后:
* 1. 停止发送数据
* 2. 开始发"窗口探测包"(Window Probe)
* 3. 等对端窗口重新打开
*
* 类比:
* 你碗满了 → 跟朋友说"碗满了别夹了" → 窗口=0
* 你吃掉一些 → 碗有空间了 → 但不主动通知
* 朋友过一会儿问"有空间了吗?" → 窗口探测
*/

echo "零窗口的形成:\\n\\n";
echo " 正常: 通告窗口=8000 → 发送方可以发8000字节\\n";
echo " 吃紧: 通告窗口=1000 → 应用读得慢, 缓冲快满了\\n";
echo " 满了: 通告窗口=0 → 发送方必须停止!\\n\\n";

echo "零窗口的处理:\\n";
echo " 发送方收到零窗口 → 停止发送 → 启动窗口探测定时器\\n";
echo " → 每隔一段时间发一个小探测包\\n";
echo " → 对端ACK, 如果窗口>0就恢复发送\\n";
echo " → 如果还是0, 继续等\\n";

echo "\\n📌 PHP代码: 零窗口场景模拟\\n\\n";

function zero_window_demo(): void {
// 设置一个很小的接收缓冲区来触发零窗口
$server = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($server, SOL_SOCKET, SO_REUSEADDR, 1);

// 设置很小的接收缓冲区 (1024字节)
socket_set_option($server, SOL_SOCKET, SO_RCVBUF, 1024);
socket_bind($server, '127.0.0.1', 9106);
socket_listen($server, 1);
socket_set_nonblock($server);

$client = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($client, SOL_TCP, TCP_NODELAY, 1);
socket_connect($client, '127.0.0.1', 9106);
$conn = wait_accept($server);
if (!$conn) { socket_close($client); socket_close($server); return; }

// 查看实际接收缓冲区
$rcvbuf = 0;
socket_get_option($conn, SOL_SOCKET, SO_RCVBUF, $rcvbuf);
echo "接收缓冲区: {$rcvbuf} 字节 (故意设很小)\\n\\n";

// 发送超过缓冲区容量的数据
$overload = str_repeat('X', 3000);
socket_write($client, $overload, 3000);
echo "发送了3000字节 (> 缓冲区的 {$rcvbuf})\\n";
echo "→ 发送方的发送缓冲区会填满\\n";
echo "→ 接收方缓冲区满了后通告窗口=0\\n";
echo "→ 发送方暂停! 等接收方读走数据\\n\\n";

// 服务端慢慢读
for ($i = 0; $i < 3; $i++) {
echo " 服务端读第" . ($i+1) . "次…\\n";
$buf = '';
socket_recv($conn, $buf, 500, 0);
echo " 读了" . strlen($buf) . "字节 → 释放缓冲区 → 窗口扩大\\n";
usleep(100000); // 100ms
}

echo "\\n这就是零窗口机制:\\n";
echo " 窗口=0 → 暂停发 → 应用读取 → 窗口恢复 → 继续发\\n";

socket_close($client);
socket_close($conn);
socket_close($server);
}
zero_window_demo();

// ===================================================================
// 334. 窗口探测 (Window Probe)
// ===================================================================

section("334", "窗口探测 (Window Probe)");

/*
* 【大白话】
* 窗口探测 = 发送方收到零窗口后,定期发的"还有空间吗?"包。
*
* 为什么需要窗口探测?
* 因为窗口重新打开时,接收方不一定有数据要回,
* 纯ACK包可能丢失。如果丢了,发送方永远不知道窗口开了。
* 窗口探测就是为了打破这个僵局。
*
* 探测过程:
* 1. 收到零窗口通告
* 2. 启动持续定时器(persist timer)
* 3. 每隔一段时间发1字节的探测包
* 4. 接收方回复ACK + 新的窗口大小
* 5. 窗口>0 → 恢复发送; 窗口=0 → 继续等
*
* 注意: 窗口探测和重传超时是不同的!
* 重传超时: 等待ACK, 超时重传
* 窗口探测: 等待窗口打开, 定期探测
*/

echo "窗口探测(Persist Timer) vs 重传超时:\\n\\n";
echo " 重传超时: 发了数据没收到ACK → 认为是丢了 → 重发\\n";
echo " 窗口探测: 收到ACK但窗口=0 → 不是丢了是满了 → 探测\\n\\n";

echo "窗口探测过程:\\n";
echo " 发送方 接收方\\n";
echo " | |\\n";
echo " | ← ACK, 窗口=0 ———— | (满了!)\\n";
echo " | 启动持续定时器 |\\n";
echo " | …等待… | (应用在读数据)\\n";
echo " | —— 窗口探测(1字节) ——→ | (有空间了吗?)\\n";
echo " | ← ACK, 窗口=2000 — | (有了!)\\n";
echo " | 恢复发送数据 ————————→ |\\n";

echo "\\n📌 PHP代码: 窗口探测机制模拟\\n\\n";

function window_probe_sim(): void {
echo "=== 窗口探测模拟 ===\\n\\n";

class WindowProbeSim {
public int $recv_buf_total = 1000;
public int $recv_buf_used = 0;
public int $advertised_window = 1000;

public function appRead(int $bytes): void {
$this->recv_buf_used = max(0, $this->recv_buf_used $bytes);
$this->advertised_window = $this->recv_buf_total $this->recv_buf_used;
echo " [应用读取{$bytes}字节] 缓冲使用={$this->recv_buf_used}, 通告窗口={$this->advertised_window}\\n";
}

public function dataArrive(int $bytes): void {
if ($bytes <= $this->advertised_window) {
$this->recv_buf_used += $bytes;
$this->advertised_window = $this->recv_buf_total $this->recv_buf_used;
echo " [收到{$bytes}字节] 缓冲使用={$this->recv_buf_used}, 通告窗口={$this->advertised_window}\\n";
}
}
}

$sim = new WindowProbeSim();

// 发送方发数据填满缓冲区
echo "阶段1: 发送方正常发送\\n";
for ($i = 0; $i < 5; $i++) {
$sim->dataArrive(250); // 每次250
}
echo "→ 窗口=0! 发送方收到零窗口通告\\n\\n";

// 发送方进入窗口探测模式
echo "阶段2: 发送方开始窗口探测\\n";
$probe_interval = 1; // 每秒探测一次(演示加速)
for ($round = 1; $round <= 5; $round++) {
echo "探测{$round}: 发送窗口探测包…\\n";
echo " 接收方回复: 窗口={$sim->advertised_window}\\n";

if ($sim->advertised_window > 0) {
echo " → 窗口重新打开了! 恢复发送!\\n";
break;
}

// 模拟应用慢慢读数据
$sim->appRead(250);
echo " 窗口仍为0, 继续等待…\\n\\n";
}

echo "\\n窗口探测的关键:\\n";
echo " – 不是重传数据, 而是发小探测包\\n";
echo " – 防止ACK丢失导致的死锁\\n";
echo " – 使用'持续定时器'(Persist Timer)\\n";
}
window_probe_sim();

// ===================================================================
// 335. 糊涂窗口综合征(SWS)
// ===================================================================

section("335", "糊涂窗口综合征 (Silly Window Syndrome)");

/*
* 【大白话】
* SWS = "明明可以一次发一大块,偏要一小口一小口地发"
*
* 两种情况都会导致SWS:
*
* 1. 发送方SWS:
* 应用每次只写1字节 → TCP发1字节 → 40字节头+1字节数据
* → 效率惨不忍睹
*
* 2. 接收方SWS:
* 应用每次只读1字节 → 窗口通告只增加1 → 发送方只能发1字节
* → 又变成了小包通信
*
* SWS的本质:
* 窗口管理太"糊涂"了,通告/发送的数据块太小
*
* 类比:
* 货车每次只拉一个包裹就上路,而不是等装满再走。
*/

echo "糊涂窗口综合征 (SWS) 的两种表现:\\n\\n";

echo "发送方SWS: 应用每次写一小块\\n";
echo " 应用write(1) → TCP发1字节\\n";
echo " 应用write(1) → TCP发1字节\\n";
echo " → 每个包都是 40字节头+1字节数据 = 97.5%开销!\\n\\n";

echo "接收方SWS: 应用每次读一小块\\n";
echo " 应用read(1) → 窗口+1 → 通告窗口=1\\n";
echo " 发送方只能发1字节 → 又一个小包!\\n";
echo " → 形成了恶性循环\\n\\n";

echo "👇 下面两节讲怎么避免SWS。\\n";

// ===================================================================
// 336. 接收方SWS避免
// ===================================================================

section("336", "接收方SWS避免");

/*
* 【大白话】
* 接收方怎么避免SWS?
* 通告窗口太小时(只增加了几个字节),别立刻通告!
* 等窗口足够大(至少1个MSS或缓冲区的一半)再通告。
*
* Clark算法:
* 接收方: 窗口小于 min(MSS, 缓冲区/2) 时,通告窗口=0
* 直到应用读够了,窗口≥阈值,才通告真实窗口
*
* 效果:
* 没有Clark: 读1字节 → 窗口=1 → 发1字节 → 读1字节 → …
* 有Clark: 读1字节 → 窗口=1但通告0 → 读更多 →
* 窗口=1460 → 通告1460 → 发一大块!
*/

echo "接收方SWS避免 —— Clark算法:\\n\\n";
echo " 规则: 只要可用窗口 < min(MSS, 缓冲区/2),就通告窗口=0\\n";
echo " 即使有空间也不说,等攒够了再说!\\n\\n";

echo "效果对比:\\n";
echo " 无Clark: 应用读1字节 → 通告1 → 收到1字节包 → 低效!\\n";
echo " 有Clark: 应用读1000字节 → 窗口够1460了 → 通告1460\\n";
echo " → 收到一个大包 → 高效!\\n";

echo "\\n📌 PHP代码: Clark算法模拟\\n\\n";

function clark_algorithm_demo(): void {
echo "=== Clark算法 (接收方SWS避免) ===\\n\\n";

function clark_advertise(int $real_window, int $mss, int $buf_total): int {
$threshold = min($mss, (int)($buf_total / 2));
if ($real_window > 0 && $real_window < $threshold) {
echo " Clark: 真实窗口={$real_window} < 阈值={$threshold} → 通告0\\n";
return 0; // 故意通告0!
}
echo " Clark: 真实窗口={$real_window} >= 阈值={$threshold} → 通告{$real_window}\\n";
return $real_window;
}

$buf_total = 4096;
$buf_used = 4000;
$mss = 1460;

echo "缓冲区: {$buf_total}, 初始使用: {$buf_used}\\n";
echo "MSS: {$mss}, Clark阈值: min(1460, 4096/2) = 2048\\n\\n";

// 应用慢慢读数据
for ($i = 0; $i < 6; $i++) {
$buf_used -= 500; // 每次读500
if ($buf_used < 0) $buf_used = 0;
$real_window = $buf_total $buf_used;
echo "应用读500 → 缓冲使用{$buf_used} → 真实窗口{$real_window}\\n";
clark_advertise($real_window, $mss, $buf_total);
echo "\\n";
}

echo "结论: 应用读了3000字节,但前几次都通告0\\n";
echo " 直到窗口≥2048才通告真实值 → 避免小包!\\n";
}
clark_algorithm_demo();

// ===================================================================
// 337. 发送方SWS避免
// ===================================================================

section("337", "发送方SWS避免");

/*
* 【大白话】
* 发送方怎么避免SWS?
* 不要每次应用write()一小块就立刻发送!
* 等攒够了再发。
*
* Nagle算法就是发送方SWS避免的一种实现。
*
* 发送方SWS避免规则:
* 1. 等之前的数据被确认了再发新的(除非攒够了)
* 2. 攒到MSS大小再发
* 3. 如果这是最后一段数据(没有更多了) → 立刻发
*
* 注意: 如果发送方不避免SWS,即使接收方用了Clark,
* 窗口足够大,发送方还是可能发小包(SWS是从发送端产生的)。
*/

echo "发送方SWS避免 —— 三不原则:\\n\\n";
echo " 规则1: 不要发小于MSS的包,除非:\\n";
echo " a) 这是最后一段数据(应用close/socket_shutdown了)\\n";
echo " b) 之前的数据都确认了(可以发了)\\n";
echo " c) 启用了TCP_NODELAY(Nagle关闭)\\n\\n";

echo " 规则2: 如果Nagle开启(默认):\\n";
echo " 有未确认数据 → 不发小包 → 攒到MSS或确认回来\\n";
echo " 没有未确认数据 → 可以发\\n\\n";

echo " 规则3: 如果Nagle关闭(TCP_NODELAY=1):\\n";
echo " 有数据就立刻发,不管多小\\n\\n";

echo "📌 PHP代码: 发送方SWS避免策略\\n\\n";

function sender_sws_avoid_demo(): void {
echo "=== 发送方SWS避免 两种策略对比 ===\\n\\n";

$server = create_server(9107);
$servers2 = []; // 保存引用

// === 策略1: Nagle开启(SWS避免) ===
$c1 = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
// Nagle默认开启 → 自动避免SWS
socket_connect($c1, '127.0.0.1', 9107);
$conn1 = wait_accept($server);

if ($conn1) {
echo "策略1: Nagle开启 (默认) — 发送方SWS自动避免\\n";

$start = microtime(true);
// 快速写5个小块
for ($i = 0; $i < 5; $i++) {
socket_write($c1, "X", 1);
}
$c1_time = round((microtime(true) $start) * 1000, 2);

// 服务端读多少包?
socket_set_nonblock($conn1);
$rcv = '';
$packets = 0;
for ($i = 0; $i < 50; $i++) {
$b = '';
$n = @socket_recv($conn1, $b, 1024, 0);
if ($n > 0) { $rcv .= $b; $packets++; }
if (strlen($rcv) >= 5) break;
usleep(10000);
}
echo " 发送5字节耗时: {$c1_time}ms\\n";
echo " 服务端收到: \\"{$rcv}\\" ({$packets}次读取)\\n";
echo " → Nagle可能把小包合并, 减少包数量\\n\\n";

socket_close($c1);
socket_close($conn1);
}

// === 策略2: TCP_NODELAY (禁用SWS避免) ===
$server2 = create_server(9108);
$c2 = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
$flag = 1;
socket_set_option($c2, SOL_TCP, TCP_NODELAY, $flag); // 禁用Nagle
socket_connect($c2, '127.0.0.1', 9108);
$conn2 = wait_accept($server2);

if ($conn2) {
echo "策略2: TCP_NODELAY=1 (禁用Nagle) — 可能产生SWS\\n";

$start = microtime(true);
for ($i = 0; $i < 5; $i++) {
socket_write($c2, "Y", 1);
}
$c2_time = round((microtime(true) $start) * 1000, 2);

socket_set_nonblock($conn2);
$rcv2 = '';
$packets2 = 0;
for ($i = 0; $i < 50; $i++) {
$b = '';
$n = @socket_recv($conn2, $b, 1024, 0);
if ($n > 0) { $rcv2 .= $b; $packets2++; }
if (strlen($rcv2) >= 5) break;
usleep(10000);
}
echo " 发送5字节耗时: {$c2_time}ms\\n";
echo " 服务端收到: \\"{$rcv2}\\" ({$packets2}次读取)\\n";
echo " → 每个小包立即发送, 延迟低但包多\\n";

socket_close($c2);
socket_close($conn2);
socket_close($server2);
}

socket_close($server);

echo "\\n【选择建议】\\n";
echo " 低延迟需求(游戏/交易): TCP_NODELAY=1\\n";
echo " 高吞吐需求(文件传输): 默认Nagle即可\\n";
echo " 混合场景: 小消息禁用Nagle, 大块数据启用\\n";
}
sender_sws_avoid_demo();

// ===================================================================
// 338. PUSH标志的使用
// ===================================================================

section("338", "PUSH标志的使用");

/*
* 【大白话】
* PUSH标志 = "收到数据立刻给应用程序,别等!"
*
* 发送方设PSH:
* "这是我最后一段数据了,收到就推给应用"
*
* 接收方看到PSH:
* 不等待缓冲区填满,立刻把数据交给应用程序
*
* 什么时候会设置PSH:
* 1. 发送方: 发送缓冲区被清空时(数据都发出去了)
* 2. 发送方: 应用程序调用了类似flush的操作
* 3. 应用层: 通过TCP_NODELAY间接导致每个包都带PSH
*
* PHP中:
* – socket_write 后内核通常会自动在最后包设PSH
* – 不能用socket函数直接设PSH(内核层的活)
* – TCP_NODELAY=1 时每个write都可能触发PSH
*/

echo "PUSH标志的作用:\\n\\n";
echo " 没PSH: 内核攒够数据再给应用 → 可能有延迟\\n";
echo " 有PSH: 立刻交给应用 → 低延迟\\n\\n";

echo "PSH设置时机:\\n";
echo " – 应用写完所有数据,发送缓冲区空了 → 自动PSH\\n";
echo " – TCP_NODELAY=1 → 每个包都可能带PSH\\n";
echo " – send() 时可以传 MSG_PUSH (某些系统)\\n";

echo "\\n📌 PHP代码: PUSH的行为演示\\n\\n";

function push_flag_demo(): void {
$server = create_server(9109);
$c_slow = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
$c_fast = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);

// 慢速接收方 — 用默认Nagle
socket_connect($c_slow, '127.0.0.1', 9109);
$conn_slow = wait_accept($server);
socket_set_block($conn_slow);

if (!$conn_slow) { socket_close($c_slow); socket_close($server); return; }

echo "场景: 发送方发完数据 → 自动PSH → 接收方立刻收到\\n\\n";

// 发送方发数据
socket_write($c_slow, "First chunk. ", 13);
socket_write($c_slow, "Second chunk. ", 14);
socket_write($c_slow, "Last chunk.", 11); // ← 最后一个包内核会设PSH
echo "发送方发了3段数据:\\n";
echo " 1) \\"First chunk. \\" (13字节)\\n";
echo " 2) \\"Second chunk. \\" (14字节)\\n";
echo " 3) \\"Last chunk.\\" (11字节) ← 此包带PSH!\\n\\n";

// 接收方
/*
* 可能读到的情况:
* 1) 三段分开收(如果TCP_NODELAY)
* 2) 合并收到(如果Nagle合并了)
* 3) 前两段合并,第三段单独(因为PSH)
*/

$buf = '';
$n = socket_recv($conn_slow, $buf, 8192, 0);
echo "接收方1次读: \\"" . $buf . "\\" (" . strlen($buf) . "字节)\\n";

// 可能还有剩余
$remaining = '';
socket_set_nonblock($conn_slow);
for ($i = 0; $i < 20; $i++) {
$b = '';
$n2 = @socket_recv($conn_slow, $b, 1024, 0);
if ($n2 > 0) $remaining .= $b;
usleep(10000);
}
$total = $buf . $remaining;
echo "总共收到: \\"" . $total . "\\" (" . strlen($total) . "字节)\\n";
echo "↑ 38字节全部到齐,PSH确保最后一个块不会在缓冲区等太久\\n";

socket_close($c_slow);
socket_close($conn_slow);

// === TCP_NODELAY 场景 ===
echo "\\n— TCP_NODELAY场景 (每个包都可能带PSH) —\\n";
$server2 = create_server(9110);
$c_fast2 = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($c_fast2, SOL_TCP, TCP_NODELAY, 1);
socket_connect($c_fast2, '127.0.0.1', 9110);
$conn_fast = wait_accept($server2);
socket_set_nonblock($conn_fast);

if ($conn_fast) {
// 快速写三次
socket_write($c_fast2, "A", 1);
socket_write($c_fast2, "B", 1);
socket_write($c_fast2, "C", 1);

// 逐次读取
$pieces = [];
for ($i = 0; $i < 3; $i++) {
$b = '';
for ($j = 0; $j < 100; $j++) {
$n = @socket_recv($conn_fast, $b, 1, 0);
if ($n > 0) { $pieces[] = $b; break; }
usleep(5000);
}
}
echo "收到" . count($pieces) . "个独立片段: ";
echo implode(", ", array_map(fn($s) => "\\"{$s}\\"", $pieces)) . "\\n";
echo "↑ 每个包都被PSH立刻推给应用!\\n";

socket_close($c_fast2);
socket_close($conn_fast);
socket_close($server2);
}

socket_close($server);
}
push_flag_demo();

// ===================================================================
// 339. 滑动窗口动态变化
// ===================================================================

section("339", "滑动窗口动态变化");

/*
* 【大白话】
* 滑动窗口不是固定的,它在不断变化:
*
* 窗口变大的情况:
* – 应用快速读取数据 → 缓冲空闲变多 → 通告窗口变大
* – 对端窗口扩容(窗口缩放)
*
* 窗口变小的情况:
* – 应用读取缓慢 → 缓冲塞满 → 通告窗口变小
* – 网络拥塞 → 拥塞窗口减小
*
* 窗口左移(关闭/收缩):
* – ACK到达 → 左边界右移(窗口滑动了)
*
* 窗口右移(打开/扩张):
* – 接收方通告更大窗口 → 右边界右移
*
* 实际发送窗口 = min(通告窗口, 拥塞窗口)
*/

echo "滑动窗口的动态变化:\\n\\n";

echo " 时间线示例:\\n";
echo " 时刻1: 通告窗口=8000, 发送窗口=8000\\n";
echo " 时刻2: 发了4000, 飞行中=4000, 有效窗口=4000\\n";
echo " 时刻3: 收到ACK=2000, 窗口左移, 飞行中=2000, 有效=6000\\n";
echo " 时刻4: 接收方应用变慢, 通告窗口=3000\\n";
echo " → 发送窗口=3000, 有效窗口=1000\\n";
echo " 时刻5: 接收方应用追上, 通告窗口=10000\\n";
echo " → 窗口扩大! 发送窗口=10000\\n";

echo "\\n📌 PHP代码: 滑动窗口动态变化模拟\\n\\n";

function window_dynamics_demo(): void {
echo "=== 滑动窗口动态变化 ===\\n\\n";

class DynamicWindow {
public int $send_base = 0;
public int $next_seq = 0;
public int $advertised = 8000;
public int $in_flight = 0;
public int $cwnd = 10000; // 拥塞窗口(先不管)
public int $data_sent = 0;

public function sendWindow(): int {
return min($this->advertised, $this->cwnd);
}

public function usable(): int {
return $this->sendWindow() $this->in_flight;
}

public function send(int $bytes): bool {
if ($this->usable() < $bytes) return false;
$this->next_seq += $bytes;
$this->in_flight += $bytes;
$this->data_sent += $bytes;
return true;
}

public function onAck(int $ack, int $new_window): void {
// 左边界右移
if ($ack > $this->send_base) {
$acked = $ack $this->send_base;
$this->send_base = $ack;
$this->in_flight = max(0, $this->in_flight $acked);
}
// 窗口大小更新
$old = $this->advertised;
$this->advertised = $new_window;
$diff = $new_window $old;
if ($diff > 0) echo " ↑ 窗口扩大了{$diff}字节!\\n";
if ($diff < 0) echo " ↓ 窗口缩小了" . ($diff) . "字节!\\n";
}

public function status(): string {
return sprintf("base=%d next=%d adv=%d cwnd=%d inflight=%d usable=%d",
$this->send_base, $this->next_seq, $this->advertised,
$this->cwnd, $this->in_flight, $this->usable());
}
}

$dw = new DynamicWindow();
echo "初始: {$dw->status()}\\n\\n";

// 时刻1: 正常发送
echo "时刻1: 发送3000字节\\n";
$dw->send(3000);
echo " {$dw->status()}\\n\\n";

// 时刻2: 收到部分ACK, 同时通告窗口变小(接收方应用慢了)
echo "时刻2: ACK=1500, 新通告窗口=5000 (应用变慢)\\n";
$dw->onAck(1500, 5000);
echo " {$dw->status()}\\n";
echo " → 左边界右移(ACK确认), 但窗口缩小了\\n\\n";

// 时刻3: 收到更多ACK, 通告窗口继续缩小
echo "时刻3: ACK=3000, 新通告窗口=2000 (应用严重滞后)\\n";
$dw->onAck(3000, 2000);
echo " {$dw->status()}\\n";
echo " → 窗口严重缩小! 只能发很少了\\n\\n";

// 时刻4: 应用追上了, 通告窗口恢复
echo "时刻4: ACK=3000, 新通告窗口=10000 (应用追上了!)\\n";
$dw->onAck(3000, 10000);
echo " {$dw->status()}\\n";
echo " → 窗口恢复且扩大! 正常发送\\n\\n";

echo "结论: 通告窗口就像一个'阀门'\\n";
echo " – 应用慢 → 关小阀门(窗口缩小)\\n";
echo " – 应用快 → 开大阀门(窗口扩大)\\n";
echo " – 动态调整, 完美匹配消费速度!\\n";
}
window_dynamics_demo();

// ===================================================================
// 340. 窗口左右边沿移动
// ===================================================================

section("340", "窗口左右边沿移动");

/*
* 【大白话】
* 窗口有两个边界: 左边界和右边界。
*
* 左边界移动(窗口关闭):
* 收到ACK → 左边界右移
* 左边界永远不会左移(数据一旦确认就确认了)
*
* 右边界移动(窗口打开):
* 接收方通告更大的窗口 → 右边界右移
* 右边界可以左右移动!
* 右边界右移: 窗口扩大(应用读得快了)
* 右边界左移: 窗口缩小(应用读得慢了)
* 但右边界不能小于左边界(窗口不能为负)
*
* 三种移动:
* 1. 窗口关闭(close): 左边界右移, 右边界不动 → 窗口变小
* 2. 窗口打开(open): 左边界不动, 右边界右移 → 窗口变大
* 3. 窗口收缩(shrink):左边界不动, 右边界左移 → 窗口变小
* (TCP强烈不建议收缩!)
*
* 注意: TCP不建议窗口收缩!
* RFC 793说: 右边界不应该左移(不收缩)
* 如果必须缩小, 可以使用更小的通告窗口但不缩小已有承诺
*/

echo "窗口边沿移动的三种情况:\\n\\n";

echo "情况1: 窗口关闭 (左移)\\n";
echo " 收到ACK → 左边界: 100→200 (右移了100)\\n";
echo " 右边界: 不变\\n";
echo " [200, 900) ← 窗口变小了100\\n\\n";

echo "情况2: 窗口打开 (右扩展)\\n";
echo " 通告窗口变大: 700→1200\\n";
echo " 左边界: 不变\\n";
echo " 右边界: 900→1100 (右移了200)\\n";
echo " [200, 1100) ← 窗口变大了200\\n\\n";

echo "情况3: 窗口收缩 (不建议!)\\n";
echo " 通告窗口变小: 700→500\\n";
echo " 但之前已经承诺了700! 收缩意味着毁约\\n";
echo " TCP不推荐这么做\\n";

echo "\\n📌 PHP代码: 窗口边沿移动模拟\\n\\n";

function window_edges_demo(): void {
echo "=== 窗口左右边沿移动 ===\\n\\n";

class WindowEdgeTracker {
public int $left = 0;
public int $right;
public int $mss = 1460;

public function __construct(int $init_window) {
$this->right = $init_window;
}

public function windowSize(): int {
return $this->right $this->left;
}

public function close(int $ack): void {
if ($ack > $this->left) {
$moved = $ack $this->left;
$this->left = $ack;
// 保持右边界不变 → 窗口变小了
echo " 窗口关闭: 左移+{$moved} → [{$this->left}, {$this->right})\\n";
}
}

public function open(int $new_window): void {
$new_right = $this->left + $new_window;
if ($new_right > $this->right) {
$expanded = $new_right $this->right;
$this->right = $new_right;
echo " 窗口打开: 右移+{$expanded} → [{$this->left}, {$this->right})\\n";
}
}

public function shrink(int $new_window): void {
$new_right = $this->left + $new_window;
if ($new_right < $this->right) {
echo " ⚠️ 窗口收缩: 右边界左移 → [{$this->left}, {$new_right}) (不推荐!)\\n";
$this->right = $new_right;
}
}

public function show(): string {
$size = $this->windowSize();
$bar = str_repeat('█', min($size / 100, 50));
return "[{$this->left}, {$this->right}) 大小={$size} {$bar}";
}
}

$tracker = new WindowEdgeTracker(8000);
echo "初始窗口: {$tracker->show()}\\n\\n";

// 1. 收到ACK → 左边界右移
echo "1. 收到ACK=2000:\\n";
$tracker->close(2000);
echo " {$tracker->show()}\\n\\n";

// 2. 通告窗口变大 → 右边界右移
echo "2. 对端通告窗口=10000:\\n";
$tracker->open(10000);
echo " {$tracker->show()}\\n\\n";

// 3. 再收ACK
echo "3. 收到ACK=4000:\\n";
$tracker->close(4000);
echo " {$tracker->show()}\\n\\n";

// 4. 通告窗口缩小
echo "4. 对端通告窗口=5000 (缩小了!):\\n";
$tracker->shrink(5000);
echo " {$tracker->show()}\\n\\n";

echo "关键规则:\\n";
echo " 左边界: 只能右移(随ACK前进)\\n";
echo " 右边界: 可以左右移动(随通告窗口变化)\\n";
echo " 但右边界 ≥ 左边界(窗口不能为负)\\n";
}
window_edges_demo();

// ===================================================================
// 341. 关闭窗口
// ===================================================================

section("341", "关闭窗口 (Closing the Window)");

/*
* 【大白话】
* 关闭窗口 = 接收方通告窗口=0, 让发送方停止发送。
*
* 这和TCP连接的"关闭"是两码事:
* – 连接关闭: 发FIN, 结束通信
* – 窗口关闭: 通告窗口=0, 暂停发送(连接还在!)
*
* 窗口关闭流程:
* 1. 接收方缓冲满了 → 通告窗口=0
* 2. 发送方收到窗口=0 → 停止发送数据
* 3. 发送方开始窗口探测(见334)
* 4. 接收方应用读数据 → 缓冲有空间了
* 5. 接收方通告新窗口>0 → 窗口打开
* 6. 发送方恢复发送
*
* 类比:
* 水管 → 接收方是水池, 水池满了 → 关闭进水阀(窗口=0)
* 水池的水被抽走 → 重新开水阀(窗口>0)
*
* 注意: FIN/ACK是不受窗口限制的!
* 即使窗口=0, FIN和ACK包仍然可以发送和接收
*/

echo "窗口关闭 vs 连接关闭:\\n\\n";
echo " 窗口关闭: 通告窗口=0 → 暂停数据发送 → 连接正常\\n";
echo " 连接关闭: 发FIN → 结束通信 → 连接终止\\n\\n";

echo "窗口关闭的过程:\\n";
echo " 1. 接收方缓冲满 → 通告窗口递减 → 最终=0\\n";
echo " 2. 发送方收到窗口=0 → 停止发送应用数据\\n";
echo " 3. 但仍可发窗口探测(1字节)和ACK\\n";
echo " 4. 接收方应用读取 → 缓冲空出 → 通告窗口>0\\n";
echo " 5. 发送方收到新窗口 → 恢复发送!\\n";

echo "\\n📌 PHP代码: 窗口关闭场景\\n\\n";

function window_close_demo(): void {
echo "=== 窗口关闭与重新打开 ===\\n\\n";

// 创建一个小缓冲区的服务器
$server = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($server, SOL_SOCKET, SO_REUSEADDR, 1);
socket_set_option($server, SOL_SOCKET, SO_RCVBUF, 2048); // 2KB缓冲区
socket_bind($server, '127.0.0.1', 9111);
socket_listen($server, 1);

$client = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_connect($client, '127.0.0.1', 9111);

socket_set_nonblock($server);
$conn = wait_accept($server);
if (!$conn) { socket_close($client); socket_close($server); return; }
socket_set_block($conn);

// 查看缓冲区
$rcvbuf = 0;
socket_get_option($conn, SOL_SOCKET, SO_RCVBUF, $rcvbuf);
$sndbuf = 0;
socket_get_option($client, SOL_SOCKET, SO_SNDBUF, $sndbuf);
echo "接收缓冲区: {$rcvbuf} 字节\\n";
echo "发送缓冲区: {$sndbuf} 字节\\n\\n";

// 发送超过接收缓冲区的数据
$overflow = str_repeat('X', 5000);
echo "发送5000字节 (超过接收缓冲区{$rcvbuf})\\n";

$start = microtime(true);
$written = socket_write($client, $overflow, 5000);
$write_time = round((microtime(true) $start) * 1000, 2);
echo "socket_write 返回: {$written} (耗时 {$write_time}ms)\\n";

// socket_write 可能不会阻塞很久(发送缓冲区大于接收缓冲区)
// 但接收方已经开始通告窗口=0了

echo "\\n模拟零窗口场景:\\n";
echo " 状态: 接收方缓冲满 → 通告窗口=0\\n";
echo " 发送方: 发送缓冲有数据在排队\\n";
echo " 发送方: 收到零窗口 → 暂停新的socket_write\\n";
echo " (应用层再写就会阻塞或返回EWOULDBLOCK)\\n\\n";

// 应用层慢慢读
echo "接收方应用开始读取…\\n";
$total_read = 0;
for ($i = 0; $i < 5; $i++) {
$buf = '';
$n = socket_recv($conn, $buf, 1000, 0);
$total_read += $n;
echo " 读第" . ($i+1) . "次: {$n}字节, 共{$total_read}/5000\\n";
echo " → 缓冲区释放, 通告窗口恢复, 发送方继续\\n";
}

socket_close($client);
socket_close($conn);
socket_close($server);

echo "\\n总结:\\n";
echo " 窗口关闭(零窗口) ≠ 连接关闭\\n";
echo " 它是一种'背压'机制: 用窗口控制发送速度\\n";
echo " 窗口短暂关闭是正常的, 持续关闭说明应用处理不过来\\n";
}
window_close_demo();

// ===================================================================
// 342. 数据缓存
// ===================================================================

section("342", "数据缓存");

/*
* 【大白话】
* TCP在发送方和接收方都有缓冲区(缓存):
*
* 发送缓冲区(Send Buffer):
* 应用层调用 socket_write() → 数据先存到这里
* 内核从这里取数据、打包成TCP段再发出去
* 如果满了, socket_write() 会阻塞(阻塞模式)
*
* 接收缓冲区(Receive Buffer):
* 内核收到TCP段 → 数据暂存到这里
* 应用层调用 socket_recv() → 从这里取数据
* 如果满了, 通告窗口=0 → 对方暂停发送
*
* 缓冲区大小:
* – 由操作系统自动管理
* – 可以用 SO_SNDBUF / SO_RCVBUF 调整
* – 实际值通常是请求值的2倍(内核自动)
* – 缓冲区越大 = 窗口越大 = 吞吐量越高
*/

echo "TCP数据缓存(内核缓冲区):\\n\\n";

echo " 应用层 内核(TCP) 网络\\n";
echo " ┌──────┐ write ┌──────────────────┐ 发TCP段 ──→\\n";
echo " │ 你的 │ ────→ │ 发送缓冲区 │\\n";
echo " │ PHP │ │ (SO_SNDBUF) │\\n";
echo " │ 代码 │ ←──── │ 接收缓冲区 │\\n";
echo " └──────┘ read │ (SO_RCVBUF) │ ←── TCP段\\n";
echo " └──────────────────┘\\n\\n";

echo "缓冲区满了怎么办:\\n";
echo " 发送缓冲满 → write()阻塞(阻塞socket)\\n";
echo " → EAGAIN错误(非阻塞socket)\\n";
echo " 接收缓冲满 → 通告窗口=0 → 发送方暂停\\n";

echo "\\n📌 PHP代码: 数据缓存全面演示\\n\\n";

function buffer_demo(): void {
echo "=== TCP数据缓存 ===\\n\\n";

// 1. 查看默认缓冲区
$sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);

$sndbuf = 0;
socket_get_option($sock, SOL_SOCKET, SO_SNDBUF, $sndbuf);
echo "默认发送缓冲区(SO_SNDBUF): {$sndbuf} 字节\\n";

$rcvbuf = 0;
socket_get_option($sock, SOL_SOCKET, SO_RCVBUF, $rcvbuf);
echo "默认接收缓冲区(SO_RCVBUF): {$rcvbuf} 字节\\n\\n";

// 2. 调整缓冲区
echo "调整缓冲区大小:\\n";
$new_snd = 512 * 1024; // 512KB
socket_set_option($sock, SOL_SOCKET, SO_SNDBUF, $new_snd);
socket_get_option($sock, SOL_SOCKET, SO_SNDBUF, $sndbuf);
echo " 设置发送缓冲512KB → 实际: {$sndbuf} (内核可能翻倍)\\n";

$new_rcv = 512 * 1024;
socket_set_option($sock, SOL_SOCKET, SO_RCVBUF, $new_rcv);
socket_get_option($sock, SOL_SOCKET, SO_RCVBUF, $rcvbuf);
echo " 设置接收缓冲512KB → 实际: {$rcvbuf} (内核可能翻倍)\\n\\n";

socket_close($sock);

// 3. 体验缓冲阻塞/非阻塞的区别
echo "=== 发送缓冲满: 阻塞 vs 非阻塞 ===\\n\\n";

$server = create_server(9112);
$c_block = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);

// 设置非常小的发送缓冲区
socket_set_option($c_block, SOL_SOCKET, SO_SNDBUF, 1024);
$actual = 0;
socket_get_option($c_block, SOL_SOCKET, SO_SNDBUF, $actual);
echo "发送缓冲区设为: {$actual} 字节\\n\\n";

socket_connect($c_block, '127.0.0.1', 9112);
$conn = wait_accept($server);
if (!$conn) { socket_close($c_block); socket_close($server); return; }

// 阻塞模式: 写大量数据
echo "【阻塞模式】\\n";
$big_data = str_repeat('X', 10000);
$start = microtime(true);
$written = socket_write($c_block, $big_data, 10000);
$elapsed = round((microtime(true) $start) * 1000, 2);
echo " 发送10000字节到1024字节缓冲区\\n";
echo " socket_write阻塞了 {$elapsed} ms\\n";
echo " 返回: {$written} 字节\\n";
echo " ↑ 阻塞直到有空间(接收方在收数据)\\n\\n";

// 非阻塞模式
echo "【非阻塞模式】\\n";
$c_nb = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($c_nb, SOL_SOCKET, SO_SNDBUF, 1024);
socket_set_nonblock($c_nb);
socket_connect($c_nb, '127.0.0.1', 9112);
$conn2 = wait_accept($server);
socket_set_nonblock($conn2);

if ($conn2) {
$total_written = 0;
$attempts = 0;
$max_attempts = 50;

while ($total_written < 5000 && $attempts < $max_attempts) {
$chunk = substr($big_data, $total_written, 500);
$n = @socket_write($c_nb, $chunk, strlen($chunk));
if ($n === false) {
echo " 第{$attempts}次写: 缓冲区满了(EWOULDBLOCK)! 等待中…\\n";
usleep(50000);
} else {
$total_written += $n;
echo " 第{$attempts}次写: +{$n}字节, 共{$total_written}\\n";
}
$attempts++;

// 服务端读一点
$tmp = '';
@socket_recv($conn2, $tmp, 200, 0);
}
}

// 缓冲区大小建议
echo "\\n【缓冲区大小建议】\\n";
echo " 高吞吐(文件传输): 256KB ~ 1MB\\n";
echo " 低延迟(游戏): 保持默认(64KB~128KB)\\n";
echo " BDP公式: 缓冲区 = 带宽 × RTT\\n";
echo " 例: 1Gbps × 100ms = 12.5MB 缓冲区最理想\\n";

// 清理
socket_close($c_block);
if (isset($c_nb)) socket_close($c_nb);
if (isset($conn)) socket_close($conn);
if (isset($conn2)) socket_close($conn2);
socket_close($server);
}
buffer_demo();

// ===================================================================
// 总结速查表
// ===================================================================

section("总结", "数据流与窗口管理速查表");

echo <<<'TABLE'
┌───────────────────────┬─────────────────────────────────────────┐
│ 交互式数据流 │ 小包频繁, 延迟敏感, 头部开销大 │
│ 成块数据流 │ 大块传输, 吞吐敏感, 头部开销小 │
│ 延迟ACK │ 收到数据等200ms再ACK, 可搭便车 │
│ Nagle算法 │ 攒小包合并发送, 默认开启 │
│ TCP_NODELAY │ 禁用Nagle, 立刻发送, 低延迟 │
├───────────────────────┼─────────────────────────────────────────┤
│ 滑动窗口协议 │ 流量控制核心, 发N包不等N个ACK │
│ 通告窗口 │ 接收方告诉发送方"我能收多少"
│ 有效窗口 │ 发送窗口 已发未确认 = 现在能发的 │
│ 发送窗口 =min(通告窗口, 拥塞窗口)
├───────────────────────┼─────────────────────────────────────────┤
│ 零窗口 │ 通告=0 → 缓冲区满了 → 暂停发送 │
│ 窗口探测 │ 发送方定期问"有空间了吗?" 防死锁 │
│ 窗口关闭 │ 通告窗口→0, 不等于连接关闭 │
│ 窗口打开 │ 通告窗口变大, 右边界右移 │
├───────────────────────┼─────────────────────────────────────────┤
│ SWS发送方 │ 别发太小包, 攒到MSS │
│ SWS接收方(Clark) │ 窗口不够大时故意通告0
│ PUSH标志 │ 提醒接收方立刻交应用 │
├───────────────────────┼─────────────────────────────────────────┤
│ SO_SNDBUF │ 发送缓冲区, 满了write()阻塞 │
│ SO_RCVBUF │ 接收缓冲区, 决定了最大通告窗口 │
│ 缓冲区大小建议 │ 高吞吐: 256KB~1MB, 低延迟: 默认即可 │
└───────────────────────┴─────────────────────────────────────────┘
TABLE;

echo "\\n\\n";
echo "╔══════════════════════════════════════════════════╗\\n";
echo "║ TCP 数据流与窗口管理 323~342 全部完毕! ║\\n";
echo "║ 20个知识点 + PHP Sockets 完整可运行代码 ║\\n";
echo "╚══════════════════════════════════════════════════╝\\n";

赞(0)
未经允许不得转载:171主机测评 » TCP/ip详解=TCP数据流与窗口管理
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址