Ориентация главы
Кадры и сообщения главы 95 — машины состояний (транспортно-независимый уровень библиотеки); Stream этой главы (xwsstream) подключает их к реальному транспорту: xrtWsStreamAttach/AttachTls перехватывают ссылку вызывающего на TCP/TLS-поток уже после завершённого обновления (с точной стыковкой «в буфере остались данные кадров»), события публикуются как MessageBegin → MessageData×N → MessageEnd — представление тела действительно только внутри текущего обратного вызова; Stream не агрегирует целое сообщение и не выделяет фиксированный буфер на каждое соединение — существующая цепочка блоков TCP/TLS входит прямо в машину состояний кадров. Отправка даёт пути copy и ref по ссылке (ссылка освобождается только после фактического завершения записи в сокет — в групповой рассылке одна ссылка висит на N соединениях со счётным освобождением); SendLimit — единая жёсткая граница ожидания WebSocket и нижележащего транспорта, Backpressure/Writable/Drain дают фронты восстановления. Ping отвечает автоматически, Close гарантированно одноразово — полный жизненный цикл объекта соединения.
96.1引入
Упражнение в конце главы 95 предлагало собрать кадрировочный цикл руками — вы обнаружили, что 80% кода возит «транспортную коммутацию»: в буфере после обновления уже могут лежать данные кадров, заголовок кадра может пересекать TLS-record, противодавление записи надо смотреть в двух слоях сразу, Ping просит автоответа, Close можно отправить лишь раз... Эти трубопроводы, переписываемые каждой реализацией, слой Stream собирает в один вызов Attach плюс четыре обратных вызова событий.
Первое, что стоит понять, — стыковка перехвата: ответ обновления только что отправлен, а в приёмном буфере уже может ждать первый кадр от нетерпеливого клиента (браузеры так делают часто). Параметр iPrefix сообщает Stream «длину уже проверенного, но не потреблённого HTTP-заголовка» — при перехвате копируется согласованный подпротокол, заголовок потребляется точно, и остаток кадров в том же буфере входит прямо в машину состояний кадров — ни один байт не пропадает и не смещается. Отказ никогда не перехватывает наполовину (ссылка транспорта возвращается нетронутой).
96.2概念
Перехват: точная стыковка Attach
Особенность TLS: заголовок кадра может пересекать границу TLS-record — Stream накапливает до 14 байтов полного заголовка кадра (максимальный размер заголовка) через путь ReadMore, ограниченный PlainLimit, — «не останавливаться из-за сохранения неполного префикса» — стыковка деталей TLS-потока главы 87 с машиной состояний кадров этой главы уже отлажена.
Модель событий: представление использовал — выбросил
Представление тела MessageData действительно только внутри текущего синхронного обратного вызова — Stream не агрегирует целое сообщение (большое сообщение не занимает память) и не копирует (цепочка блоков проходит насквозь). Логика потребления обязана «завершиться внутри обратного вызова» (разбор, пересылка, запись на диск); нужно сохранить — копируйте сами. Это та же дисциплина, что потребление открытого текста TLS-потока главы 87 и представления Data тела главы 91 — в мире заимствованных представлений сохранение это явное действие.
Три формы отправки и единое противодавление
- copy (
Text/Binary/Send): нагрузка копируется в очередь — простой путь малых сообщений. - ref (
TextRef/BinaryTake/SendRefи др.): передаётсяxnetref{данные, длина, освобождающий обратный вызов, контекст} — ноль копирования, пока стоит в очереди; освобождающий обратный вызов вызывается только после фактической записи в сокет. В групповой рассылке одна ссылка висит на N соединениях (счётное освобождение) — одно распределение, N отправок. - сжатая отправка (
TextCompressedи др.): Deflater создаётся по надобности — путь после согласования permessage-deflate (глава 97).
Противодавление — единый уровень воды: SendLimit одновременно считает ожидание WebSocket и ожидание нижележащего транспорта — достижение потолка возвращает XNET_RESULT_AGAIN; события Backpressure/Writable/Drain дают фронты восстановления (вход в противодавление, запись снова возможна, осушение). В одном ряду с бюджетом записи TCP главы 68 и SendLimit TLS главы 87 — по уровню воды на слой, семантика одна.
Контрольные кадры и протокол закрытия
Ping отвечает Pong автоматически (приложение не замечает); Ping можно отправить и по своей инициативе. Гарантии Close: одноразовая отправка (повторный Close не выходит в сеть), механизм тайм-аута (молчащая противоположная сторона завершается по тайм-ауту), снимок финального состояния противоположной стороны (CloseInfo: код + причина, для чтения приложением). Семантические домены кодов закрытия и проверка на стороне записи — материал главы 97; здесь запомните «Close вызывается один раз, протокол ведает машина состояний».
Жизненный цикл и самосознание
Ref/Destroy со счётом ссылок (как у прочих объектов соединения); семейство самосознания: State/Role/Protocol/Worker/Tcp/TcpRef/Tls/TlsRef/Deflate/Pending/Writable — все публичные факты соединения; Pause/Paused/Resume — управление потоком (приостановка приёма в паре с ограничением скорости ниже по течению).
96.3示例
Первая полная программа: отправка по ссылке — освобождение после записи
Программа ниже из examples/websocket/stream_ref/main.c — стандартный образец пути отправки без копий:
#include <stdio.h> #include <string.h> #include <xrt.h> */ /* 引用发送完成后归还业务负载。 */ static void releasePayload( ptr pContext, cbytes pData, size_t iSize ) { (void)pContext; (void)iSize; xrtFree((ptr)pData); } /* 在 Connection 所属 Worker 上发送一个可延迟释放的业务负载。 */ static xnetresult sendPayload( xwsstream* pConnection, xbytesview Payload ) { bytes pOwned; xnetref Ref; xnetresult Result; pOwned = (bytes)xrtMalloc(Payload.Size); if ( pOwned == NULL ) { return XNET_RESULT_ERROR; } memcpy(pOwned, Payload.Data, Payload.Size); Ref = (xnetref) { pOwned, Payload.Size, releasePayload, NULL }; Result = xrtWsStreamBinaryRef(pConnection, &Ref); if ( Result != XNET_RESULT_OK ) { xrtFree(pOwned); } return Result; } /* 示例函数由已经建立连接的 Worker 回调调用。 */ int main(void) { (void)sendPayload; printf("WebSocket Connection reference example is ready\n"); return 0; }
Что только что произошло. (1) Бизнес-нагрузка выделена xrtMalloc-ом (имитация сообщения от верхнего потока); xnetref пакует {указатель, длина, обратный вызов releasePayload, контекст}. (2) xrtWsStreamBinaryRef(连接, 负载, Ref) (соединение, нагрузка, Ref) ставит в очередь по ссылке — нагрузка не копируется, пока стоит в очереди Worker; только после фактического завершения отправки (записи в сокет) вызывается releasePayload и xrtFree возвращает память. (3) Почему это важно: в высокопроизводительных сценах путь copy — это распределение + копия на каждую отправку; путь ref — одно распределение, ноль транзитных копий.Групповое продвижение (шапка комментария указывает путь): одна ссылка вешается на N соединений — внутреннее счётное освобождение; только последний дописавшийся действительно освобождает. (4) Pending/Writable в паре ждут осушения — так читается единое противодавление.
Сопроводительный examples/websocket/stream_tour — полный обход возможностей слоя Stream (петля, оба конца): проверка конфигурации, Attach на обоих концах, эхо 64 байтов, автоматический и ручной Pong, приостановка-восстановление, три формы сжатия, чистое закрытие (code=1000) — прогоните его как исполняемую таблицу контрактов главы.
Вторая полная программа: предвестие согласования расширений
Вторая программа из examples/websocket/extension/main.c — обход параметров расширений после обновления, подготовка главы 97:
#include <stdio.h> #include <xrt.h> */ /* 展示逐项读取 Sec-WebSocket-Extensions 和扩展参数。 */ int main(void) { xstrview Text = XRT_STR_LITERAL( "permessage-deflate; client_max_window_bits, x-trace" ); xwsextension Extension; xhttpnext Next; size_t iOffset = 0; for ( ;; ) { xhttpparam Param; size_t iParam = 0; Next = xrtWsExtensionNext( Text, &iOffset, &Extension ); if ( Next == XHTTP_NEXT_END ) { break; } if ( Next == XHTTP_NEXT_ERROR ) { return 1; } printf( "extension=%.*s\n", (int)Extension.Name.Size, Extension.Name.Data ); while ( xrtWsExtensionParamNext( &Extension, &iParam, &Param ) == XHTTP_NEXT_ITEM ) { printf( " parameter=%.*s\n", (int)Param.Name.Size, Param.Name.Data ); } } return 0; }
Что только что произошло. (1) Двухуровневая структура Sec-WebSocket-Extensions: внешний уровень режет расширения по ,, внутренний режет параметры по ; — изоморфно грамматике семейств полей главы 92, но у уровня расширений свой итератор (пользовательские расширения вроде x-trace тоже выражаются как name (имя) + параметры). (2) client_max_window_bits — один из параметров согласования permessage-deflate; глава 97 полностью разбирает согласование и цепочку сжатия; здесь закладывается понимание «расширения — свойство соединения после обновления, и согласование решает поведение Stream (пути сжатия, биты RSV)». (3) Итерация расширений — чисто полевая операция: отладочные прокси и контролёры соответствия аудируют результат согласования, не строя соединения.
96.4契约
- Атомарность перехвата: неудавшийся Attach никогда не перехватывает наполовину (ссылка транспорта возвращается нетронутой);
iPrefixточно потребляет проверенный заголовок, остаток кадров того же буфера входит в машину состояний напрямую. - Представления событий: тело
MessageDataдействительно только внутри текущего обратного вызова; без агрегации целых сообщений, без фиксированного буфера на соединение — цепочка блоков проходит прямо в машину состояний кадров. - TLS через record: заголовки кадров, пересекающие TLS-record, накапливаются через
ReadMore(≤14 байтов, под ограничением PlainLimit) — без остановки и без потерь. - Три формы отправки: copy (копия в очередь) / ref (ссылка + отложенный освобождающий обратный вызов) / compressed (Deflater по надобности); семантика ёмкости и бюджета общебиблиотечная.
- Единое противодавление:
SendLimitсчитает вместе WS и транспорт; AGAIN + события фронтов Backpressure/Writable/Drain. - Отправка по ссылке:
xnetref{данные, длина, освобождение, контекст}; освобождение только после записи в сокет; одна ссылка может висеть на N соединениях со счётным освобождением. - Ping/Pong: полученный Ping автоматически отвечает Pong; активный Ping отправить можно; управление потоком Pause/Paused/Resume.
- Гарантии Close: одноразовая отправка, механизм тайм-аута, снимок финального состояния противоположной стороны (CloseInfo); Abort прерывает немедленно.
- Семейство самосознания: State/Role/Protocol/Worker/Tcp(+Ref)/Tls(+Ref)/Deflate/Pending/Writable.
- Обрезка:
WEBSOCKET_STREAMнезависимо от слоя кадров; под-возможности_REF/_TLS/_DEFLATEпо надобности.
Типичные ошибки
Симптом: в середине обработки асинхронной очереди крушение или мусор — представление умерло по возвращении обратного вызова.
Причина: Stream не агрегирует и не копирует — представление указывает на текущую позицию цепочки приёмных блоков, которую следующее же событие может перезаписать. Асинхронная обработка обязана скопировать данные внутри обратного вызова и ставить в очередь копию.
onMessageData(pStream, Data, ...) { queue_push(g_Queue, Data); /* сохранили представление: висяче по возвращении */ }
onMessageData(pStream, Data, ...) { bytes pCopy = (bytes)xrtMalloc(Data.Size); memcpy(pCopy, Data.Data, Data.Size); queue_push(g_Queue, pCopy); /* копия может идти асинхронно */ } /* ещё лучше: завершить потребление синхронно (разбор/пересылка), не вводя очередь */
Симптом: противоположная сторона изредка получает перепутанные кадры — нагрузку освободили, пока она стояла в очереди, до записи в сокет.
Причина: контракт пути ref — «освобождающий обратный вызов вызывает Stream»: после постановки в очередь вызывающий больше не владеет нагрузкой. Ранний free или двойной free — равно разрушение владения.
xrtWsStreamBinaryRef(pStream, PayloadView, &Ref); xrtFree(pBuffer); /* освободили сразу после постановки: ссылка в очереди висит */
/* владение переходит со ссылкой; освобождение - внутри обратного вызова releasePayload */ xrtWsStreamBinaryRef(pStream, PayloadView, &Ref); /* с этого момента pBuffer - забота Stream: не трогать */
Симптом: вы сами считаете «ожидание WS», чтобы судить о противодавлении, и это не сходится с фактическими моментами AGAIN — ожидание слоёв TLS/TCP тоже занимает бюджет.
Причина: SendLimit — единый уровень воды (WS + нижележащий транспорт). Послойный учёт пропускает очереди транспортной стороны; события Pending/Writable — это и есть вся правда.
if ( my_ws_queue_size() < MY_LIMIT ) { send_more(); /* транспортный слой мог давно перейти предел */ }
if ( xrtWsStreamSend(pStream, ...) == XNET_RESULT_AGAIN ) { /* ждать события Writable/Drain - фронты восстановления единого уровня */ return; }
Практические задания
Прогоните stream_tour и stream_ref; по шапке комментария stream_tour отметьте каждый проверенный поведенческий пункт (эхо/Pong/приостановка/сжатие/закрытие). Критерии приёмки: можете указать событие или API за каждым пунктом.
Реализуйте эхо на событиях Stream: версия copy (после MessageEnd отправить Text обратно) и версия ref (представление MessageData копируется в ссылку и отправляется обратно). Нагрузочным тестом сравните счётчики распределений двух версий (глава 6). Критерии приёмки: семантика одинакова; версия ref распределяет на сообщение существенно меньше; под противодавлением (медленный клиент) обе стабильны.
Реализуйте broadcast(连接数组, 消息) (массив соединений, сообщение): одно сообщение отправляется ссылкой на N соединений — единственная нагрузка xrtMalloc, N ссылок xnetref со счётным освобождением; обработайте частичное противодавление (соединения с AGAIN пропустить или кешировать для повтора — стратегию выбрать и обосновать комментарием). Критерии приёмки: рассылка на 8 соединений без дополнительных копий (проверка статистикой); соединение под противодавлением не мешает отправке остальных; после всех записей нагрузка освобождается ровно один раз.
Итоги
| Тема | Кратко |
|---|---|
| Перехват | Attach/AttachTls принимают транспорт после обновления; iPrefix точно стыкует остаток буфера; отказ никогда наполовину |
| Поток событий | MessageBegin → MessageData×N (представления действительны только внутри обратного вызова) → MessageEnd; без агрегации, ноль предварительных распределений |
| Стыковка TLS | заголовки кадров через record накапливаются ReadMore (≤14B, под PlainLimit) |
| Три формы отправки | copy (копия) / ref (ссылка с отложенным освобождением) / compressed (Deflater по надобности) |
| Единое противодавление | SendLimit считает вместе WS + транспорт; AGAIN + Backpressure/Writable/Drain |
| Отправка ссылкой | xnetref{данные, длина, освобождение, контекст}; освобождение после записи; возможно счётное освобождение на N соединений |
| Контрольные кадры | Ping автоматически отвечает Pong; Close одноразово + тайм-аут + снимок противоположной стороны |
| Управление потоком и самосознание | Pause/Resume; семейство State/Role/Protocol/Pending/Writable |
| Свойства расширений | результат согласования итерируется (permessage-deflate главы 97) |
| Обрезка | STREAM независимо от слоя кадров; под-возможности REF/TLS/DEFLATE |
📖 Справочники API в этой главе:websocket (86 записей)websocket_stream (45 записей)net (247 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки