本章导读
认证(第 124 章)之后连接是"你的"了——而 SSH 的强大之处在于一条连接上开多条通道(channel):session(shell/exec/sftp)、direct-tcpip(第 126 章转发的基础)、自定义类型。本章讲数据面三件套:窗口(ssh_channel_window:RFC 4254 双向流控——远端窗口+max-packet 限发送、本地窗口按消费返还,无分配无网络所有权);通道核心(ssh_channel_core:open/confirm/failure、EOF/CLOSE 状态、数据提交与窗口事务——不拥有缓冲不碰 transport,可放数组/哈希按 recipient O(1) 路由);通道集合(ssh_channels:动态所有权层——按需创建节点、稳定地址、硬上限,无固定数组零空闲成本)。窗口机制是 SSH 版的"背压"——与第 68 章写预算、第 96 章流控同一家族,但双向且各通道独立。
127.1引入
为什么通道要窗口?一条 SSH 连接的 TCP 缓冲是全连接共享的——如果通道 A 的对端不读,TCP 窗口收缩,通道 B 的数据也发不动(队头阻塞)。通道级窗口把流控细化到每通道:发送方最多发"对端给的窗口"字节,收到 WINDOW_ADJUST 才继续;接收方消费数据后返还窗口。效果:慢通道只堵自己(自己的窗口耗尽),快通道照跑——多路复用的公平性由协议保证。
实现细节的讲究处:两阶段返还——应用消费数据后额度先进 ReceivePending(还没告诉远端);达到阈值或窗口耗尽时组装 WINDOW_ADJUST、消息可靠排队后才 AdjustCommit——"避免发送失败却提前扩大本地窗口"(提前通告=超发窗口=协议错乱)。uint64 统计:内部未消费/待返还字节用 64 位计数(长连接不因累计超 4 GiB 回绕),线路窗口仍严格 uint32(RFC 要求)。
127.2概念
窗口:双向流控状态
零窗口启动是个精致能力:初始本地窗口可以设 0——等应用真正备好缓冲再 Grant——"先建通道后配预算"的懒初始化。
通道核心:open 与状态机
EOF 与 CLOSE 分离:EOF 只关一个数据方向("我发完了"但还能收)——第 97 章关闭语义的通道版;CLOSE 双向终结。close 后的数据:此前已交给应用的数据仍可消费,但不再产生 WINDOW_ADJUST(返还机制随通道死亡)。request 能力独立查询(CanSendRequest/CanReceiveRequest)——通道上的 shell/exec/x11 请求与数据面是两个正交能力轴。无内置 request token:每个通道可独立组合 xsshreplyqueue(want-reply 的关联队列)——容量与存储位置由应用按并发定,空闲通道零 FIFO 成本。
数据事务:接收提交与消费分离
收到 data:DataReceiveCommit(应用接管视图前提交——窗口记账)→ 应用消费 → DataConsume(额度 Pending)→ 窗口事务返还。发送侧对称:SendLimit 限长 → packet 可靠排队 → DataSendCommit。提交与消费分离正是背压的机制化:不消费=不返还=远端窗口耗尽=远端停发——第 110 章 pause/resume 的窗口版,协议自动执行。
通道集合:动态所有权
ssh_channels 按本端 channel id 存稳定地址的 xsshchannel(每项=core+可选动态 I/O+回复 FIFO):按需创建(Open/Accept 才建映射节点——空连接零成本、空通道零固定缓冲);地址稳定(删除前可安全借用——connection session/异步等待/应用状态挂指针);硬上限(默认 1024 通道/每通道 64 待回复/2 MiB 窗口/32 KiB packet/收发各 2 MiB 预算——按负载调,不依赖无限增长);可直接作 resolveproc(recipient→channel 的 O(1) 路由)。
127.3示例
第一个完整程序:窗口三件套
下面的程序来自 examples/channel_window——流控状态的直接操作:
#include <stdio.h> #include <xssh.h> /* 演示按远端窗口和 max-packet 对待发送数据分片。 */ int main(void) { xsshchannelwindow Window; uint32 iChunk; if ( !xrtSshChannelWindowInit( &Window, 100000u, 32768u, 100000u, 32768u, 50000u ) ) { return 1; } iChunk = xrtSshChannelSendLimit(&Window); if ( xrtSshChannelSendCommit(&Window, iChunk) != XSSH_OK ) { return 1; } printf("chunk=%u remaining=%u\n", iChunk, Window.SendWindow); return 0; }
刚才发生了什么。 ① xrtSshChannelWindowInit(&窗, 100000, 32768, 100000, 32768, 50000) 六参数:发送窗口/发送 max-packet/接收窗口/接收 max-packet/返还阈值——本地与远端能力一次声明。② xrtSshChannelSendLimit 取首笔发送限额——min(远端窗口 100000, 远端 max-packet 32768)=32768:max-packet 是每报文上限、窗口是总量,两者取小决定单次可发量。③ 打印 chunk 与剩余窗口——发送循环的"一次能发多少"由这个查询驱动。真实序列(send→commit→adjust→consume→adjust-ready→adjust-commit)在测试里全路径覆盖;本示例验证的是限额计算的确定形态。
第二个完整程序:通道核心的 open 事务
第二个程序来自 examples/channel_core——open 与预算声明:
#include <stdio.h> #include <xssh.h> /* 建立一个等待 confirmation 且不携带数据缓冲的本端 channel。 */ int main(void) { xsshchannelcore Channel; if ( !xrtSshChannelCoreOpenInit( &Channel, 3u, 1024u * 1024u, 32u * 1024u, 512u * 1024u ) ) { return 1; } printf( "channel=%zu local=%u phase=%d\n", sizeof(Channel), Channel.Local, (int)xrtSshChannelCorePhase(&Channel) ); return 0; }
刚才发生了什么。 ① 初始化入口(参数含本地 recipient 起始、初始窗口/max-packet、request 预算)——栈上通道核心,sizeof 输出(第 118-121 章同款零负担声明)。② 打印 local(本端编号)与 phase(初始阶段)——核心的公开状态就是路由与调度的输入。③ open 事务的形状:OpenInit → 构建线路 CHANNEL_OPEN → 提交后等 confirmation → ConfirmationCommit 进 OPEN——数据面只在 OPEN 开放,阶段查询(phase)就是发送前的检查。配套示例族:channels(集合与稳定地址)、channel_request(shell/exec 请求)、channel_pty(终端协商)、channel_io(动态 I/O staging)、channel_state/channel_message(状态与报文)、reply_queue(want-reply FIFO)。
127.4契约
- 窗口无分配:不保存 payload、不设固定缓冲——视图直交消费方/环形缓冲/零拷贝队列。
- 发送限额:
SendLimit=min(远端窗口, 远端 max-packet);可靠排队后才SendCommit;回绕即协议错误。 - 两阶段返还:消费→Pending→(阈值/耗尽)→AdjustLimit→可靠排队后AdjustCommit——失败不提前扩窗。
- 动态容量:Grant 新增接收额度;零窗口启动;不消耗已消费额度。
- 64 位统计:内部字节 uint64 计数不回绕;线路窗口严格 uint32(RFC)。
- open 事务:OpenInit→线路提交→Confirmation/FailureCommit;Accept/Reject 对称——每步可靠提交后才推进。
- 阶段纪律:数据面仅 OPEN 开放;EOF 单向关;CLOSE 双向;close 后已有数据可消费但不再 ADJUST。
- request 正交:CanSend/ReceiveRequest 独立查询;token 由 replyqueue 按需组合(空闲零成本)。
- 集合:按需创建、地址稳定(删除前)、硬上限(默认 1024/64/2MiB/32KiB/2MiB+2MiB)、O(1) 路由。
- 不越界:core 不碰 transport;集合不建 socket/Engine/future/队列——层间只经显式调用。
常见错误
症状:对端发一批后"卡住"——它窗口耗尽等你 ADJUST,你消费了却没触发返还逻辑。
原因:Consume 只记账不返还——返还要等阈值或耗尽触发(也可主动查 AdjustReady)。忘了这层,流控单边失效。
while ( receive_commit(&Ch, Data) == OK ) { consume_app(Data); xrtSshChannelCoreDataConsume(&Ch->Core, Data.Size); /* 没查 AdjustReady:远端窗口枯死 */ }
while ( ... ) { consume_app(Data); xrtSshChannelCoreDataConsume(&Ch->Core, Data.Size); if ( xrtSshChannelCoreAdjustReady(&Ch->Core) ) { n = xrtSshChannelCoreAdjustLimit(&Ch->Core); if ( send_window_adjust(&Conn, Ch, n) ) { xrtSshChannelCoreAdjustSendCommit(&Ch->Core, n); } } }
症状:偶发对端超发——本地窗口被"扩大"了但 ADJUST 丢在排队失败里。
原因:契约明确两阶段——消息可靠排队后才 AdjustCommit。先 Commit 后排队失败=窗口已扩、消息没到——远端按扩后窗口发,本地校验拒绝,协议错乱。
AdjustCommit(&Ch, n); /* 先扩窗 */ if ( !send_adjust(n) ) { /* 失败:窗口已回不去 */ }
if ( send_window_adjust(&Conn, Ch, n) ) { /* 先可靠排队 */ xrtSshChannelCoreAdjustSendCommit(&Ch->Core, n); /* 成功才记账 */ }
症状:Remove/Discard 后访问通道崩溃——稳定地址的"稳定"有边界。
原因:地址稳定到对应项删除为止——集合清理(连接关闭)后全部失效。跨连接保存通道指针是设计外用法。
g_SavedChannel = xrtSshChannelsGet(&Set, Id); /* 跨连接保存 */ /* 连接关闭、集合清理后仍用 g_SavedChannel */
连接关闭回调里完成清理——不留悬挂引用 */编程练习
窗口 100000/max-packet 32768 下模拟发送:循环 SendLimit→扣减→直到窗口耗尽;注入 ADJUST 后恢复。验收标准:每次限额=min 正确;耗尽与恢复路径清晰;回绕注入被拒。
模拟"消费一批→阈值触发→排队成功/失败"两路——验证 Commit 只在排队成功后推进(失败后 Pending 保留、可重试)。验收标准:两路的 Pending/窗口内部状态与契约一致。
一条连接开三条通道(不同窗口),A 对端慢消费(大 ADJUST 延迟)、B/C 正常——并发灌数据。验收标准:B/C 吞吐不受 A 窗口耗尽影响(通道级隔离);A 恢复后继续;全程集合路由无错(recipient O(1))。
小结
| 知识点 | 速查 |
|---|---|
| 三件套 | window(流控)/core(open+状态+数据事务)/channels(集合) |
| 发送限额 | min(远端窗口, 远端 max-packet);排队后 Commit;回绕即错 |
| 两阶段返还 | Consume→Pending→阈值→排队→Commit——失败不提前扩窗 |
| 零窗口启动 | 初始 0 窗口+Grant 懒配——先建通道后备缓冲 |
| 统计位宽 | 内部 uint64 不回绕;线路 uint32 严格 RFC |
| open 事务 | OpenInit→提交→Confirm/Failure;Accept/Reject 对称 |
| EOF/CLOSE | EOF 单向;CLOSE 双向;close 后可消费不再 ADJUST |
| request 正交 | 能力独立查询;token 由 replyqueue 按需组合 |
| 集合 | 按需创建/地址稳定(删除前)/硬上限默认 1024·64·2MiB·32KiB |
| 流控本质 | 不消费=不返还=远端停发——pause/resume 的协议自动化 |
📖 本章涉及的 API 参考手册:xssh-ssh_channel_core(26 个条目)xssh-ssh_channel_window(10 个条目)xssh-ssh_channels(16 个条目)
点击查看完整签名、参数约束、返回值与错误说明