Ориентация главы

После аутентификации (глава 124) соединение «ваше» — а сила SSH в открытии множества каналов на одном соединении: session (shell/exec/sftp), direct-tcpip (основа переадресации главы 126), собственные типы. В главе триада плоскости данных: окно (ssh_channel_window: двунаправленное управление потоком RFC 4254 — удалённое окно + max-packet ограничивают отправку, локальное окно возвращается по потреблению; без выделений и владения сетью); ядро канала (ssh_channel_core: open/confirm/failure, состояния EOF/CLOSE, фиксация данных и оконные транзакции — без собственных буферов и без транспорта; кладётся в массивы/хеши для O(1)-маршрутизации по recipient); набор каналов (ssh_channels: слой динамического владения — узлы по мере надобности, стабильные адреса, жёсткие потолки; без фиксированных массивов, ноль простоя). Оконный механизм — SSH-редакция «противодавления» — то же семейство, что бюджеты записи главы 68 и управление потоком главы 96, но двунаправленное и на каждый канал отдельно.

127.1引入

Зачем каналам окна? TCP-буферы одного SSH-соединения разделяются всем соединением — если противоположная сторона канала A не читает, TCP-окно сжимается и данные канала B тоже встают (блокировка головы очереди). Окна на уровне каналов уточняют управление потоком до каждого канала: отправитель может послать максимум «окна, выданного противоположной стороной» байтов, продолжение — только по WINDOW_ADJUST; приёмник возвращает окно по мере потребления приложением. Эффект: медленный канал затыкает только себя (исчерпывает своё окно), быстрые бегут дальше — справедливость мультиплексирования гарантирована протоколом.

Тонкое место реализации — двухфазный возврат: после потребления данных приложением квота сперва входит в ReceivePending (удалённой стороне ещё не сообщили); по достижении порога или исчерпании окна собирается WINDOW_ADJUST, и лишь после надёжной постановки сообщения в очередь исполняется AdjustCommit — «избегая увеличить локальное окно при провале отправки» (заранее оповестить = перегранть окно = сломать протокол). uint64-статистика: внутренние непотреблённые/ожидающие возврата байты считают 64-битно (долгие соединения не заворачиваются от накопления за 4 ГиБ), линейное окно строго uint32 (требование RFC).

127.2概念

Окно: состояние двунаправленного управления потоком

1ОтправкаSendLimit = min(удалённое окно, удалённый max-packet) -> данные в надёжную очередь -> SendCommit списывает
2Приёмданные прибыли -> ReceiveCommit (проверка локального окна и max-packet) -> приложение потребляет -> ReceiveConsume
3> сообщение надёжно в очереди -> AdjustCommit (две фазыокно растёт только после успеха очереди)
4Динамическая ёмкостьReceiveGrantCommit добавляет приёмную ёмкость - старт с нулевым окном и динамические бюджеты памяти

Старт с нулевым окном — изящная способность: начальное локальное окно можно поставить в 0 — подождать, пока приложение действительно подготовит буферы, и лишь тогда Grant — ленивая инициализация «сначала канал, потом бюджет».

Ядро канала: open и машина состояний

CLOSEDOpenInit (своя сторона инициирует; после фиксации на линии ждёт ответа peer) →OPEN_PENDING
OPEN_PENDINGConfirmationCommit (peer подтверждает) / FailureCommit (провал - сразу финал) →OPEN
CLOSEDAcceptInit (инициирует peer; слой политики копирует поля типа) →ACCEPT_PENDING
ACCEPT_PENDINGAcceptCommit / RejectCommit →OPEN
OPENдвунаправленное закрытие завершено (CloseSend + CloseReceive) →CLOSED

EOF и CLOSE разделены: EOF закрывает только одно направление данных («я закончил отправлять», но принимать могу) — семантика закрытия главы 97 в редакции каналов; CLOSE завершает оба направления. Данные после close: прежде отданное приложению остаётся потребляемым, но WINDOW_ADJUST больше не порождается (механизм возврата умирает вместе с каналом). Способность запросов запрашивается независимо (CanSendRequest/CanReceiveRequest) — запросы shell/exec/x11 на канале и плоскость данных суть две ортогональные оси способностей. Без встроенного request token: каждый канал может независимо компоновать xsshreplyqueue (корреляционная очередь want-reply) — ёмкость и место хранения определяет приложение по конкурентности; простаивающие каналы платят ноль FIFO.

Транзакции данных: фиксация приёма отделена от потребления

Прибыли data: DataReceiveCommit (фиксация до того, как приложение возьмёт представление — оконный учёт) → приложение потребляет → DataConsume (квота в Pending) → оконная транзакция возвращает. Отправка симметрична: SendLimit ограничивает длину → пакет надёжно в очереди → DataSendCommit. Разделение фиксации и потребления и есть механизация противодавления: не потребляешь = не возвращаешь = окно удалённой стороны исчерпано = удалённая сторона перестаёт слать — pause/resume главы 110 в оконной редакции, исполняемое протоколом автоматически.

Набор каналов: динамическое владение

ssh_channels хранит по локальному channel id стабильные по адресу xsshchannel (каждый = ядро + опциональный динамический I/O + FIFO ответов): создание по мере надобности (узлы отображения только при Open/Accept — пустое соединение стоит ноль, пустой канал без фиксированных буферов); стабильные адреса (безопасно заимствовать до удаления — сессии соединения/асинхронные ожидания/состояние приложения вешают указатели); жёсткие потолки (по умолчанию 1024 канала / 64 ожидающих ответа на канал / окно 2 МиБ / пакет 32 КиБ / бюджеты приёма-отправки по 2 МиБ — настраивается под нагрузку, но не полагаться на безграничный рост); можно напрямую использовать как resolveproc (O(1)-маршрутизация recipient→channel).

127.3示例

Первая полная программа: оконная триада

Программа ниже из examples/channel_window — прямые операции над состоянием управления потоком:

extlibs/xssh/examples/channel_window/main.c源码 ↗
#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;
}
$ gcc -O1 -DXRT_MODULE_ALL -DXSSH_MODULE_ALL -I single -I single/extlibs -include xrt.h -include xssh.h impl.c extlibs/xssh/examples/channel_window/main.c -lws2_32 -lbcrypt -ladvapi32 -liphlpapi(输出 chunk=32768 remaining=100000 形态的发送限额自检结果)

Что только что произошло. (1) xrtSshChannelWindowInit(&窗, 100000, 32768, 100000, 32768, 50000) (окно) — шесть параметров: окно отправки/max-packet отправки/окно приёма/max-packet приёма/порог возврата — способности локальной и удалённей сторон объявляются за раз. (2) xrtSshChannelSendLimit берёт первую квоту отправки — min(远端窗口 100000, 远端 max-packet 32768)=32768 (min удалённого окна 100000 и удалённого max-packet 32768): max-packet — потолок на сообщение, окно — общий объём; меньшее из двух решает размер одной отправки. (3) Печать chunk и оставшегося окна — «сколько можно отправить за раз» в цикле отправки ведёт этот запрос. Реальная последовательность (send→commit→adjust→consume→adjust-ready→adjust-commit) покрыта тестами по всем путям; образец проверяет детерминированную форму вычисления квоты.

Вторая полная программа: open-транзакция ядра канала

Вторая программа из examples/channel_core — open и объявление бюджета:

extlibs/xssh/examples/channel_core/main.c源码 ↗
#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;
}
$ gcc -O1 -DXRT_MODULE_ALL -DXSSH_MODULE_ALL -I single -I single/extlibs -include xrt.h -include xssh.h impl.c extlibs/xssh/examples/channel_core/main.c -lws2_32 -lbcrypt -ladvapi32 -liphlpapi(输出 channel 结构尺寸、本地编号与阶段的自检结果)

Что только что произошло. (1) Вход инициализации (параметры включают старт локального recipient, начальные окно/max-packet, бюджеты request) — ядро канала на стеке; вывод sizeof (то же заявление о нулевой обузе, что в главах 117–120). (2) Печать local (локальный номер) и phase (начальная стадия) — публичное состояние ядра и есть вход маршрутизации и планирования. (3) Форма 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契约

  • Окно без выделений: нагрузку не хранит, фиксированных буферов не ставит — представления напрямую потребителю / кольцевой буфер / очередь без копий.
  • Квота отправки: SendLimit=min(远端窗口, 远端 max-packet) (удалённое окно, удалённый max-packet); SendCommit только после надёжной постановки в очередь; заворот — протокольная ошибка.
  • Двухфазный возврат: потребление→Pending→(порог/исчерпание)→AdjustLimit→AdjustCommit после надёжной очереди — провал никогда не увеличивает окно заранее.
  • Динамическая ёмкость: Grant добавляет приёмную квоту; старт с нулевым окном; уже потреблённая квота повторно не тратится.
  • 64-битная статистика: внутренние байты считаются uint64 без заворота; линейное окно строго uint32 (RFC).
  • open-транзакция: OpenInit→фиксация на линии→Confirmation/FailureCommit; Accept/Reject симметричны — каждый шаг продвигается только после надёжной фиксации.
  • Дисциплина стадий: плоскость данных открыта только в OPEN; EOF закрывает одно направление; CLOSE оба; после close данные потребляемы, но ADJUST больше нет.
  • Запросы ортогональны: CanSend/ReceiveRequest запрашиваются независимо; token компонуется по мере надобности из replyqueue (простой стоит ноль).
  • Набор: создание по мере надобности, адреса стабильны (до удаления), жёсткие потолки (умолчания 1024/64/2МиБ/32КиБ/2МиБ+2МиБ), O(1)-маршрутизация.
  • Без пересечения границ: ядро не трогает transport; набор не строит socket/Engine/future/очередь — слои встречаются только явными вызовами.

Типичные ошибки

Ловушка 1: потребили данные — окно не вернули:Ловушка 1: потребили данные — окно не вернули

Симптом: после партии противоположная сторона «застревает» — её окно исчерпано, она ждёт вашего 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);
		}
	}
}
Ловушка 2: возврат окна — сначала Commit, потом очередь (порядок перепутан):Ловушка 2: возврат окна — сначала Commit, потом очередь (порядок перепутан)

Симптом: редкие перегранты противоположной стороны — локальное окно «увеличено», а ADJUST потерялся в провале постановки в очередь.

Причина: контракт прямо требует двух фаз — AdjustCommit только после надёжной постановки сообщения в очередь. Commit вперёд, затем провал очереди = окно уже велико, сообщение не дошло — удалённая сторона шлёт по расширенному окну, локальная проверка отвергает, протокол ломается.

错误写法
AdjustCommit(&Ch, n);      /* сперва расширить */
if ( !send_adjust(n) ) { /* провал: окно уже не вернуть */ }
正确写法
if ( send_window_adjust(&Conn, Ch, n) ) {   /* сперва надёжно в очередь */
	xrtSshChannelCoreAdjustSendCommit(&Ch->Core, n);  /* учитывать только при успехе */
}
Ловушка 3: сохранён указатель канала после удаления из набора:Ловушка 3: сохранён указатель канала после удаления из набора

Симптом: обращение к каналу после Remove/Discard падает — «стабильность» стабильного адреса имеет границу.

Причина: адреса стабильны до удаления соответствующего элемента — после очистки набора (закрытие соединения) всё недействительно. Хранить указатель канала между соединениями — вне проектного употребления.

错误写法
g_SavedChannel = xrtSshChannelsGet(&Set, Id);   /* сохранено между соединениями */
/* g_SavedChannel используется и после закрытия соединения и очистки набора */
正确写法
   очистка в callback закрытия соединения - не оставлять висячих ссылок */

Практические задания

Базовое: развёртка квоты

При окне 100000 / max-packet 32768 сымитировать отправку: цикл SendLimit→списание→до исчерпания окна; внедрить ADJUST и восстановить. Критерии приёмки: каждая квота — верный min; пути исчерпания и восстановления ясны; внедрённый заворот отвергается.

Продвинутое: тайминг двухфазного возврата

Сымитировать оба пути «потребили партию→порог сработал→очередь успешна/неуспешна» — проверить, что Commit продвигается только после успеха очереди (при провале Pending сохраняется, повтор возможен). Критерии приёмки: внутренние состояния Pending/окна обоих путей сходятся с контрактом.

Вызов: справедливое мультиплексирование трёх каналов

Три канала на одном соединении (разные окна); противоположная сторона A потребляет медленно (большая задержка ADJUST), B/C нормально — качать данные конкурентно. Критерии приёмки: пропускная способность B/C не зависит от исчерпания окна A (изоляция уровня каналов); A продолжается после восстановления; маршрутизация набора без ошибок весь путь (recipient O(1)).

Итоги

ТемаКратко
Триадаокно (управление потоком) / ядро (open + состояния + транзакции данных) / channels (набор)
Квота отправкиmin(удалённое окно, удалённый max-packet); Commit после очереди; заворот — ошибка
Двухфазный возвратConsume→Pending→порог→очередь→Commit — провал не расширяет окно заранее
Старт с нулевым окномначальное окно 0 + ленивый Grant — сначала канал, потом буфер
Разрядность статистикивнутреннее uint64 без заворота; линия uint32 строго по RFC
open-транзакцияOpenInit→фиксация→Confirm/Failure; Accept/Reject симметричны
EOF/CLOSEEOF однонаправленный; CLOSE двунаправленный; после close потребляемо, но ADJUST больше нет
Запросы ортогональныспособность запрашивается независимо; token по мере надобности из replyqueue
Наборсоздание по мере надобности / адреса стабильны (до удаления) / жёсткие потолки, умолчания 1024·64·2МиБ·32КиБ
Суть управления потокомне потребляешь = не возвращаешь = удалённая сторона стоп — pause/resume, автоматизированное протоколом

📖 Справочники API в этой главе:xssh-ssh_channel_core (26 записей)xssh-ssh_channel_window (10 записей)xssh-ssh_channels (16 записей)

Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки