Ориентация главы
Сессионная машина главы 86 сама не трогает сеть; клиентский дозвон главы 82 уже тайно пользовался слоем Stream. Эта глава разбирает слой Stream (xtlsstream) в лоб: серверная форма — в колбэке TCP Accept xrtTlsStreamAccept одним шагом строит комбинированный объект «TCP-поток + TLS-сессионная машина», после чего бизнес видит только чтение-запись открытого текста; событийная модель — четыре колбэка Open/Read/Writable/Close (та же, что у клиента главы 82); противодавление — приём открытого текста через Available + ожидание Writable при короткой записи: медленный клиент не разрывает память сервера; жизненный цикл — разделяемая конфигурация с подсчётом ссылок, единый выпуск после закрытия слушателя и всех соединений. Это очередное исполнение общебиблиотечного паттерна «комбинация скелетов»: Engine (глава 64) + Listener/Stream (глава 67) + TLS-сессионная машина (глава 86) слагаются в полный сервис, к которому можно подключиться напрямую openssl s_client.
87.1引入
Превратить спарринг-эхо главы 86 в настоящий сервис — не хватает лишь «транспортной разводки»: Accept даёт TCP-поток → строится комбинированный объект → рукопожатие бежит само → событийные колбэки разносят открытый текст. Звучит как тонкий слой, но каждая точка — место ловушки: кто кормит байты во время рукопожатия? Где останавливается потребление открытого текста, когда запись упёрлась в противодавление? Как согласовать закрытие соединения и закрытие слушателя, чтобы не возникло утечки памяти? Слой Stream стандартизует эту разводку — вы пишете бизнес-логику в колбэке Echo, жизненным циклом и противодавлением ведает комбинированный объект.
Форма сервиса тоже заслуживает взгляда: один процесс, один Engine, один Listener, N TLS-соединений разделяют одну и ту же конфигурацию сервера (идентичность/ALPN) и таблицу событий — подсчёт ссылок конфигурации делает «собрали раз, N соединений переиспользуют» без единой копии. Это продолжение одной линии с разделением идентичности главы 83 и верификатора главы 85: все тяжёлые объекты TLS — неизменяемые разделяемые значения.
87.2概念
Построение комбинированного объекта: стороны Accept и Dial
- Сервер:
xrtTlsStreamAccept(TCP流, 服务端配置, 流配置, 事件表, 用户数据, &流)— в колбэке TCP Accept одним шагом строит комбинированный объект; рукопожатие бежит само, по успеху срабатывает Open. - Клиент:
xrtTlsDial/DialAsync(глава 82) внутри и есть «TCP-дозвон + StreamAttach в клиентском направлении» — у клиента нет отдельного образца Attach, потому что дозвон уже встроил его.
Обе стороны делят тип xtlsstream и весь прикладной интерфейс — Send/Available/Buffer/Consume/Close/Abort/State (шпаргалка главы 82). Различие только в способе построения и направлении.
Событийная модель и жёсткое противодавление
Четыре обратных вызова (колбэка) полностью изоморфны TCP-потоку (глава 67): Open (рукопожатие завершено/READY), Read (открытый текст прибыл), Writable (бюджет отправки восстановлен), Close (единственное конечное состояние: xnetresult + структурная ошибка). Механизм жёсткого противодавления: у зоны приёма открытого текста есть уровень воды (Available не ноль — есть необработанное); пока бизнес не потребил, приёмная сторона не продвигается — эхо-служба связывает «потребление открытого текста» и «отправку открытого текста» в один цикл: Span за Span Send; короткая запись (XTLS_AGAIN) — пауза потребления и ожидание Writable для продолжения. Окно TCP медленного клиента сжимается → записи TLS не уходят → потребление открытого текста на паузе → уровень приёма неподвижен → память постоянна. Это исполнение обещания «насос без блокировок» главы 62 на TLS.
Совместное использование конфигурации и жизненный цикл
Разделяемая конфигурация не освобождается до закрытия Listener и всех соединений — образец оркестрирует этот пункт сходки атомарными счётчиками (Connections/ListenerClosed). Правило одиночного соединения то же, что в главе 81: Close/Abort → дождаться XTLS_STREAM_CLOSED/FAILED → Destroy.
Три стыковочные детали, специфичные для TLS
Три стыковки реализации, о которых стоит знать: байты рукопожатия кормит комбинированный слой автоматически (вы ни разу не звали Feed — после StreamAccept рукопожатие «дело транспорта»); транспортный запрос xrtTlsStreamTransport(流) одалживает нижележащий TCP-поток — запросить адрес партнёра в Open (xrtNetStreamRemote) для журнала — стандартное движение; двухуровневое закрытие — StreamClose идёт со стеканием close_notify (глава 82), Close-колбэк срабатывает один раз на конечном состоянии комбинированного объекта. Конфигураций две: xtlsserverconfig (протокольная грань: идентичность/ALPN) и xtlsstreamconfig (грань потока: уровни воды буферов и пр.) — разделение забот протокола и транспорта доведено до конфигурационного слоя.
87.3示例
Первая полная программа: событийная TLS-служба эха
Программа ниже из examples/tls/stream/main.c — полный Echo на TLS 1.3, к которому напрямую подключается openssl s_client:
#include "../common.h" */ /* Echo 服务共享配置只在 Listener 和全部连接关闭后释放。 */ typedef struct example_tls_stream { xtlsserverconfig ServerConfig; xtlsstreamconfig StreamConfig; xtlsstreamevents StreamEvents; xatomic32 Connections; xatomic32 ListenerClosed; } example_tls_stream; /* 输出当前线程保存的结构化根错误。 */ static void exampleTlsStreamError(cstr sPrefix) { const xerror* pError = xrtGetError(); fprintf( stderr, "%s: %s\n", sPrefix, pError != NULL ? xrtErrorMessage(pError) : "unknown error" ); } /* 从借用明文块逐 Span 回送;短写等待 Writable 后继续。 */ static void exampleTlsStreamEcho(xtlsstream* pStream) { while ( xrtTlsStreamAvailable(pStream) != 0 ) { const xnetbuf* pBuffer = xrtTlsStreamBuffer(pStream); xnetspan Span; size_t iWritten = 0; xtlsresult Result; if ( (pBuffer == NULL) || !xrtNetBufFront(pBuffer, &Span) ) { (void)xrtTlsStreamAbort(pStream); return; } Result = xrtTlsStreamSend( pStream, Span.Data, Span.Size, &iWritten ); if ( (iWritten != 0) && !xrtTlsStreamConsume(pStream, iWritten) ) { (void)xrtTlsStreamAbort(pStream); return; } if ( Result == XTLS_AGAIN ) { return; } if ( Result != XTLS_OK ) { (void)xrtTlsStreamAbort(pStream); return; } } } /* TLS READY 后记录活动连接。 */ static void exampleTlsStreamOpen(xtlsstream* pStream, ptr pData) { example_tls_stream* pExample = (example_tls_stream*)pData; xnetaddr Remote; char Address[128]; (void)xrtAtomic32FetchAdd( &pExample->Connections, 1, XMEMORY_RELEASE ); if ( xrtNetStreamRemote( xrtTlsStreamTransport(pStream), &Remote ) && xrtNetAddrText( &Remote, Address, sizeof(Address) ) ) { printf("TLS client open: %s\n", Address); } } /* 新明文和写背压解除都进入同一个无重复 Echo 路径。 */ static void exampleTlsStreamRead( xtlsstream* pStream, const xnetbuf* pBuffer, ptr pData ) { (void)pBuffer; (void)pData; exampleTlsStreamEcho(pStream); } /* 写入队列重新可用时继续尚未消费的借用明文。 */ static void exampleTlsStreamWritable(xtlsstream* pStream, ptr pData) { (void)pData; exampleTlsStreamEcho(pStream); } /* 记录组合连接终态;失败根因只在回调期间打印。 */ static void exampleTlsStreamClose( xtlsstream* pStream, xnetresult Result, const xerror* pError, ptr pData ) { example_tls_stream* pExample = (example_tls_stream*)pData; (void)pStream; if ( (Result != XNET_RESULT_OK) && (pError != NULL) ) { fprintf( stderr, "TLS client closed: %s\n", xrtErrorMessage(pError) ); } (void)xrtAtomic32FetchSub( &pExample->Connections, 1, XMEMORY_RELEASE ); } /* 在 TCP Accept 回调内一步建立服务端 TLS 组合对象。 */ static bool exampleTlsStreamAccept( xnetlistener* pListener, xnetstream* pTransport, ptr pData ) { example_tls_stream* pExample = (example_tls_stream*)pData; xtlsstream* pStream = NULL; bool bAccepted; (void)pListener; bAccepted = xrtTlsStreamAccept( pTransport, &pExample->ServerConfig, &pExample->StreamConfig, &pExample->StreamEvents, pExample, &pStream ); if ( bAccepted ) { xrtTlsStreamDestroy(pStream); } return bAccepted; } /* 记录 Listener 唯一关闭回调。 */ static void exampleTlsStreamListenerClose( xnetlistener* pListener, ptr pData ) { example_tls_stream* pExample = (example_tls_stream*)pData; (void)pListener; xrtAtomic32Store( &pExample->ListenerClosed, 1, XMEMORY_RELEASE ); } /* 运行一个可由 openssl s_client 访问的 TLS 1.3 Echo 服务。 */ int main(int argc, char** argv) { static const xstrview Protocols[] = { XRT_STR_INIT("http/1.1") }; example_tls_stream Example; uint8* pCertificateData = NULL; uint8* pPrivateData = NULL; size_t iCertificateSize = 0; size_t iPrivateSize = 0; xbytesview Certificate; xtlsidentity* pIdentity = NULL; xnetengineconfig EngineConfig; xnetlistenconfig ListenConfig; xnetlistenerevents ListenerEvents; xnetengine* pEngine = NULL; xnetlistener* pListener = NULL; xnetaddr Address; unsigned long iPort = 8443u; int iResult = 1; if ( argc < 4 ) { printf( "usage: stream <rsa|p256|p384|ed25519> " "<certificate.der> <private.der> [port]\n" ); return 0; } if ( argc >= 5 ) { char* pEnd = NULL; iPort = strtoul(argv[4], &pEnd, 10); if ( (pEnd == argv[4]) || (*pEnd != '\0') || (iPort == 0) || (iPort > 65535u) ) { fprintf(stderr, "invalid port\n"); goto Cleanup; } } if ( !exampleTlsReadFile( argv[2], &pCertificateData, &iCertificateSize ) || !exampleTlsReadFile( argv[3], &pPrivateData, &iPrivateSize ) ) { fprintf(stderr, "failed to read DER identity\n"); goto Cleanup; } Certificate = (xbytesview) { pCertificateData, iCertificateSize }; pIdentity = exampleTlsIdentity( argv[1], &Certificate, 1u, (xbytesview) { pPrivateData, iPrivateSize } ); if ( pIdentity == NULL ) { exampleTlsStreamError("failed to create TLS identity"); goto Cleanup; } memset(&Example, 0, sizeof(Example)); memset(&ListenerEvents, 0, sizeof(ListenerEvents)); xrtTlsServerConfigInit(&Example.ServerConfig); Example.ServerConfig.Identity = pIdentity; Example.ServerConfig.Protocols = Protocols; Example.ServerConfig.ProtocolCount = 1u; Example.ServerConfig.RequireProtocol = true; xrtTlsStreamConfigInit(&Example.StreamConfig); Example.StreamEvents.Open = exampleTlsStreamOpen; Example.StreamEvents.Read = exampleTlsStreamRead; Example.StreamEvents.Writable = exampleTlsStreamWritable; Example.StreamEvents.Close = exampleTlsStreamClose; ListenerEvents.Accept = exampleTlsStreamAccept; ListenerEvents.Close = exampleTlsStreamListenerClose; xrtNetEngineConfigInit(&EngineConfig); pEngine = xrtNetEngineCreate(&EngineConfig); if ( (pEngine == NULL) || !xrtNetEngineStart(pEngine) ) { exampleTlsStreamError("failed to start network engine"); goto Cleanup; } xrtNetListenConfigInit(&ListenConfig); if ( !xrtNetAddrAny( &ListenConfig.Address, XNET_FAMILY_IPV4, (uint16)iPort ) ) { exampleTlsStreamError("failed to build listen address"); goto Cleanup; } pListener = xrtNetListen( pEngine, &ListenConfig, &ListenerEvents, NULL, &Example ); if ( (pListener == NULL) || !xrtNetListenerLocal(pListener, &Address) ) { exampleTlsStreamError("failed to listen"); goto Cleanup; } printf( "TLS echo listening on 0.0.0.0:%u; press Enter to stop\n", (unsigned)Address.Port ); (void)getchar(); if ( !xrtNetListenerClose(pListener) ) { exampleTlsStreamError("failed to close listener"); goto Cleanup; } while ( (xrtAtomic32Load( &Example.ListenerClosed, XMEMORY_ACQUIRE ) == 0) || (xrtAtomic32Load( &Example.Connections, XMEMORY_ACQUIRE ) != 0) ) { xrtThreadYield(); } iResult = 0; Cleanup: xrtNetListenerDestroy(pListener); if ( (pEngine != NULL) && !xrtNetEngineDestroy(pEngine) ) { exampleTlsStreamError("failed to destroy network engine"); iResult = 1; } xrtTlsIdentityRelease(pIdentity); free(pPrivateData); free(pCertificateData); return iResult; }
Что только что произошло. (1) Главный цикл Echo — эталон жёсткого противодавления: Available → Buffer → Front 取 Span → Send(部分受理 iWritten) → Consume(iWritten) → AGAIN 即返回 (доступно → буфер → Front берёт Span → Send с частичным приёмом iWritten → Consume(iWritten) → при AGAIN возврат) — и Read, и Writable входят в этот единственный путь без дублей; медленный клиент никогда не разорвёт память. (2) Разводка Accept: в колбэке TCP Accept StreamAccept одним шагом строит комбинированный объект — здесь же Destroy (демонстрация владения: успех Accept даёт ссылку, таблица событий приняла дальнейшее; настоящий сервис держит соединение, естественно живущее до закрытия). (3) Транспортный запрос в Open: StreamTransport одалживает TCP-поток, Remote даёт адрес, печатается «TLS client open: адрес» — мост между слоем TLS и транспортным слоем ровно один. (4) Сходка жизненных циклов: Close-колбэк декрементирует Connections, ListenerClose ставит флаг — главный поток ждёт обоих нулей и лишь затем освобождает разделяемую конфигурацию; вот закрытая сторона оркестровки «N соединений делят одну сборку». (5) Прочтите рядом с TCP-эхом главы 67: событийная модель, потребление по Span, продолжение после короткой записи — построчно изоморфны; добавились только рукопожатие и двухуровневое закрытие.
Вторая полная программа: полный тур возможностей слоя Stream
Вторая программа из examples/tls/stream_tour/main.c — пять строк покрывают продвинутую грань слоя Stream:
*/ #include "../embedded_identity.h" #include <stdio.h> #include <string.h> #include <math.h> static inline double exampleTimerLimit(int64 Timeout) { return Timeout == XRT_WAIT_FOREVER ? INFINITY : xrtTimer() + (double)Timeout / 1000.0; } static inline bool exampleTimerExpired(double Limit) { return xrtTimer() >= Limit; } static inline int64 exampleTimerRemaining(double Limit) { double Ms; if (Limit == INFINITY) return XRT_WAIT_FOREVER; Ms = ceil((Limit - xrtTimer()) * 1000.0); return Ms <= 0 ? 0 : Ms >= 0x1p63 ? INT64_MAX : (int64)Ms; } #define EXAMPLE_DEADLINE_MS INT64_C(5000) /* 客户端上下文:Open/Read 回调与主线程交接。 */ typedef struct example_tls_upgrade { volatile bool bOpen; volatile bool bClosed; volatile size_t Received; size_t iPullup; size_t iRead; size_t iConsume; char ReadOut[8]; bool bSessionOk; bool bDataOk; bool bReadMore; bool bEventsSwapped; size_t iBound; } example_tls_upgrade; /* Worker 任务上下文:TCP 连接后升级为 TLS。 */ typedef struct example_upgrade_task { xnetstream* pTcp; xtlssession* pSession; /* 仅 Attach 形态非空 */ const xtlsclientconfig* pClient; const xtlsstreamconfig* pStream; const xtlsstreamevents* pEvents; ptr pData; xtlsstream* pTls; volatile bool bDone; bool bOk; } example_upgrade_task; /* 客户端事件集与热替换副本:文件级存储保证回调期间存活。 */ static xtlsstreamevents g_ClientEvents; static xtlsstreamevents g_SwappedEvents; static xtlsverifydecision exampleAcceptAll( const xtlspeer* pPeer, ptr pContext) { (void)pPeer; (void)pContext; return XTLS_VERIFY_ACCEPT; } static bool exampleSpinUntil(volatile bool* pFlag) { double iDeadline = exampleTimerLimit(EXAMPLE_DEADLINE_MS); while ( !*pFlag ) { if ( exampleTimerExpired(iDeadline) ) { return false; } xrtThreadYield(); } return true; } /* ---- HTTP over TLS 段:ParseTls 双向解析。 ---- */ static volatile bool g_bHttpReqParsed; static volatile bool g_bHttpRspParsed; static volatile bool g_bHttpMode; /* Worker 任务:热替换事件集 / 发送请求。 */ typedef struct example_http_task { xtlsstream* pStream; volatile bool bDone; bool bOk; } example_http_task; /* 请求与应答全文用 sizeof 定长——少一字节 Header 即不完整。 */ static const char arrRequest[] = "GET /x HTTP/1.1\r\nHost: h\r\n\r\n"; static const char arrResponse[] = "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nhi"; static void exampleHttpSendTask(xnetworker* pWorker, ptr pUserData) { example_http_task* pTask = (example_http_task*)pUserData; size_t iWritten = 0; (void)pWorker; pTask->bOk = (xrtTlsStreamSend(pTask->pStream, arrRequest, sizeof(arrRequest) - 1u, &iWritten) == XTLS_OK) && (iWritten == sizeof(arrRequest) - 1u); pTask->bDone = true; } /* Worker 任务:StreamClient(pSession == NULL)或 StreamAttach。 */ static void exampleUpgradeTask(xnetworker* pWorker, ptr pUserData) { example_upgrade_task* pTask = (example_upgrade_task*)pUserData; (void)pWorker; if ( pTask->pSession != NULL ) { pTask->bOk = xrtTlsStreamAttach(pTask->pTcp, pTask->pSession, pTask->pStream, pTask->pEvents, pTask->pData, &pTask->pTls); } else { pTask->bOk = xrtTlsStreamClient(pTask->pTcp, pTask->pClient, pTask->pStream, pTask->pEvents, pTask->pData, &pTask->pTls); } pTask->bDone = true; } /* 服务端 Read 回调(Worker):从 TLS 明文块链解析请求并应答。 */ static void exampleHttpServerRead(xtlsstream* pStream, const xnetbuf* pBuffer, ptr pData) { xhttp1head Head; xhttpfield Fields[8]; xhttp1limits Limits; xhttp1errorinfo Error; size_t iWritten = 0; xnetspan Span; (void)pBuffer; (void)pData; * 以 "GET" 起头的请求前缀。 */ while ( xrtTlsStreamAvailable(pStream) >= 3u ) { if ( !xrtTlsStreamPullup(pStream, 3u, &Span) || (Span.Size < 3u) ) { return; } if ( memcmp(Span.Data, "GET", 3u) == 0 ) { break; } if ( !xrtTlsStreamConsume(pStream, 1u) ) { return; } } xrtHttp1LimitsInit(&Limits); xrtHttp1HeadInit(&Head, Fields, 8); if ( (xrtHttp1RequestParseTls(pStream, &Head, &Limits, &Error) != XHTTP1_READY) || (Head.Method.Size != 3u) || (memcmp(Head.Method.Data, "GET", 3u) != 0) || (Head.Target.Size != 2u) || (memcmp(Head.Target.Data, "/x", 2u) != 0) || (xrtTlsStreamSend(pStream, arrResponse, sizeof(arrResponse) - 1u, &iWritten) != XTLS_OK) || (iWritten != sizeof(arrResponse) - 1u) ) { return; } g_bHttpReqParsed = true; } /* 客户端 Read 回调(Worker):从明文块链解析响应。 */ static void exampleHttpClientRead(xtlsstream* pStream, const xnetbuf* pBuffer, ptr pData) { xhttp1head Head; xhttpfield Fields[8]; xhttp1limits Limits; xhttp1errorinfo Error; (void)pBuffer; (void)pData; xrtHttp1LimitsInit(&Limits); xrtHttp1HeadInit(&Head, Fields, 8); if ( (xrtHttp1ResponseParseTls(pStream, &Head, &Limits, &Error) == XHTTP1_READY) && (Head.Status == 200u) && (Head.ContentLength == 2u) ) { g_bHttpRspParsed = true; } } /* 服务端回显:借用明文前缀逐块回送,短写等 Writable 再续。 */ static void exampleServerRead(xtlsstream* pStream, const xnetbuf* pBuffer, ptr pData) { (void)pBuffer; (void)pData; if ( g_bHttpMode ) { exampleHttpServerRead(pStream, NULL, NULL); return; } while ( xrtTlsStreamAvailable(pStream) != 0 ) { const xnetbuf* pPlain = xrtTlsStreamBuffer(pStream); xnetspan Span; size_t iWritten = 0; if ( (pPlain == NULL) || !xrtNetBufFront(pPlain, &Span) ) { (void)xrtTlsStreamAbort(pStream); return; } if ( xrtTlsStreamSend(pStream, Span.Data, Span.Size, &iWritten) != XTLS_OK ) { (void)xrtTlsStreamAbort(pStream); return; } if ( (iWritten != 0) && !xrtTlsStreamConsume(pStream, iWritten) ) { (void)xrtTlsStreamAbort(pStream); return; } } } static void exampleUpgradeOpen(xtlsstream* pStream, ptr pData) { example_tls_upgrade* pClient = (example_tls_upgrade*)pData; xnetspan Vec[2]; size_t iWritten = 0; /* Open 回调(Worker 上):聚集发送一条 9 字节明文。 */ Vec[0].Data = (cbytes)"vec-"; Vec[0].Size = 4; Vec[1].Data = (cbytes)"gather"; Vec[1].Size = 5; (void)xrtTlsStreamSendVec(pStream, Vec, 2, &iWritten); /* SendBound 是 Worker 专用:明文尺寸 → 密文上界。 */ (void)xrtTlsStreamSendBound(pStream, 64u, &pClient->iBound); * (先复制处理器再替换,语义等价但演示了热替换入口)。 */ g_SwappedEvents = g_ClientEvents; pClient->bEventsSwapped = xrtTlsStreamSetEvents(pStream, &g_SwappedEvents, pClient); pClient->bSessionOk = xrtTlsStreamSession(pStream) != NULL; pClient->bDataOk = xrtTlsStreamData(pStream) == pClient; pClient->bOpen = true; } static void exampleUpgradeRead(xtlsstream* pStream, const xnetbuf* pBuffer, ptr pData) { if ( g_bHttpMode ) { exampleHttpClientRead(pStream, NULL, NULL); return; } example_tls_upgrade* pClient = (example_tls_upgrade*)pData; size_t iAvail = xrtTlsStreamAvailable(pStream); size_t iWant = iAvail < 9u ? iAvail : 9u; xnetspan Span; (void)pBuffer; * Pullup 按当前可用前缀请求连续化,而不是按期望总长。 */ if ( (iWant != 0u) && xrtTlsStreamPullup(pStream, iWant, &Span) && (Span.Size == iWant) ) { pClient->iPullup = iWant; /* 当前已连续化的前缀长度 */ } /* 异步在途观测:Worker 内快照(主线程同样可读)。 */ (void)xrtTlsStreamAsyncBytes(pStream); (void)xrtTlsStreamAsyncCount(pStream); if ( iAvail < 9u ) { /* 消息未到齐:ReadMore 在保留已到前缀的前提下请求继续累积。 */ pClient->bReadMore = xrtTlsStreamReadMore(pStream); return; } * 剩余 5 字节用 Consume 精确丢弃——Read 本身就是消费式复制。 */ pClient->bReadMore = xrtTlsStreamReadMore(pStream); pClient->iRead = 0; (void)xrtTlsStreamRead(pStream, pClient->ReadOut, 4u, &pClient->iRead); if ( xrtTlsStreamConsume(pStream, 5u) ) { pClient->iConsume = 5u; } pClient->Received = pClient->Received + 9u; } static void exampleUpgradeClose(xtlsstream* pStream, xnetresult Result, const xerror* pError, ptr pData) { example_tls_upgrade* pClient = (example_tls_upgrade*)pData; (void)pStream; (void)Result; (void)pError; pClient->bClosed = true; } /* 等一条 TCP Stream 进入 OPEN。 */ static bool exampleWaitTcpOpen(xnetstream* pTcp) { double iDeadline = exampleTimerLimit(EXAMPLE_DEADLINE_MS); while ( xrtNetStreamState(pTcp) != XNET_STREAM_OPEN ) { if ( exampleTimerExpired(iDeadline) ) { return false; } xrtThreadYield(); } return true; } int main(void) { xnetengineconfig EngineConfig; xnetresolverconfig ResolverConfig; xtlslistenerconfig ListenerConfig; xtlsserverconfig ServerConfig; xtlsclientconfig ClientConfig; xtlsverifierconfig VerifierConfig; xtlsstreamconfig StreamConfig; xtlsstreamevents ServerEvents; xnetengine* pEngine = NULL; xnetresolver* pResolver = NULL; xtlsverifier* pVerifier = NULL; xtlsidentity* pIdentity = NULL; xtlslistener* pListener = NULL; xnetstream* pTcpA = NULL; xnetstream* pTcpB = NULL; xtlssession* pSessionB = NULL; xtlsstream* pClientA = NULL; xtlsstream* pClientB = NULL; xtlsstream* pServerA = NULL; xtlsstream* pServerB = NULL; example_tls_upgrade ClientA; example_tls_upgrade ClientB; example_upgrade_task TaskA; example_upgrade_task TaskB; xnetpost Post; xfuture* pSendFuture = NULL; xnetaddr Address; xnetspan AsyncVec[2]; size_t iBound = 0; double iDeadline; int iResult = 1; memset(&ClientA, 0, sizeof(ClientA)); memset(&ClientB, 0, sizeof(ClientB)); /* 服务端:回显 Listener(Read 事件原样回送)。 */ pIdentity = exampleEmbeddedIdentity(); if ( pIdentity == NULL ) { fprintf(stderr, "failed to create embedded identity\n"); goto Cleanup; } xrtTlsServerConfigInit(&ServerConfig); ServerConfig.Identity = pIdentity; xrtTlsListenerConfigInit(&ListenerConfig); ListenerConfig.Tls = ServerConfig; (void)xrtNetAddrLoopback(&ListenerConfig.Listen.Address, XNET_FAMILY_IPV4, 0); memset(&ServerEvents, 0, sizeof(ServerEvents)); ServerEvents.Read = exampleServerRead; /* 客户端:接受一切 + 显式 VerifyName(直连不走 Dial 名字推导)。 */ xrtTlsVerifierConfigInit(&VerifierConfig); VerifierConfig.Verify = exampleAcceptAll; pVerifier = xrtTlsVerifierCreate(&VerifierConfig); if ( pVerifier == NULL ) { fprintf(stderr, "failed to create verifier\n"); goto Cleanup; } xrtTlsClientConfigInit(&ClientConfig); ClientConfig.Verifier = pVerifier; ClientConfig.VerifyName = XRT_STR_LITERAL("localhost"); xrtTlsStreamConfigInit(&StreamConfig); memset(&g_ClientEvents, 0, sizeof(g_ClientEvents)); g_ClientEvents.Open = exampleUpgradeOpen; g_ClientEvents.Read = exampleUpgradeRead; g_ClientEvents.Close = exampleUpgradeClose; xrtNetEngineConfigInit(&EngineConfig); EngineConfig.Workers = 1; xrtNetResolverConfigInit(&ResolverConfig); pEngine = xrtNetEngineCreate(&EngineConfig); pResolver = xrtNetResolverCreate(&ResolverConfig); if ( (pEngine == NULL) || !xrtNetEngineStart(pEngine) || (pResolver == NULL) ) { fprintf(stderr, "failed to start engine/resolver\n"); goto Cleanup; } /* StreamEvents 让每条被接受的连接自动回显(队列中同样生效)。 */ pListener = xrtTlsListenerStart(pEngine, &ListenerConfig, NULL, &ServerEvents, NULL); if ( (pListener == NULL) || !xrtTlsListenerLocal(pListener, &Address) ) { iResult = 2; goto Cleanup; } /* 形态一(STARTTLS):裸 TCP → Worker 上 StreamClient 升级。 */ pTcpA = xrtNetStreamConnect(pEngine, &Address, 0, NULL, NULL, NULL); if ( (pTcpA == NULL) || !exampleWaitTcpOpen(pTcpA) ) { iResult = 3; goto Cleanup; } memset(&TaskA, 0, sizeof(TaskA)); TaskA.pTcp = pTcpA; TaskA.pClient = &ClientConfig; TaskA.pStream = &StreamConfig; TaskA.pEvents = &g_ClientEvents; TaskA.pData = &ClientA; if ( !xrtNetPostInit(&Post) || !xrtNetPost( xrtNetStreamWorker(pTcpA), &Post, exampleUpgradeTask, &TaskA) || !exampleSpinUntil(&TaskA.bDone) || !TaskA.bOk ) { iResult = 3; goto Cleanup; } pClientA = TaskA.pTls; /* 形态二(Attach):显式会话 + 裸 TCP → Worker 上 Attach 组合。 */ pSessionB = xrtTlsClientCreate(&ClientConfig, NULL); pTcpB = xrtNetStreamConnect(pEngine, &Address, 0, NULL, NULL, NULL); if ( (pSessionB == NULL) || (pTcpB == NULL) || !exampleWaitTcpOpen(pTcpB) ) { iResult = 4; goto Cleanup; } memset(&TaskB, 0, sizeof(TaskB)); TaskB.pTcp = pTcpB; TaskB.pSession = pSessionB; TaskB.pStream = &StreamConfig; TaskB.pEvents = &g_ClientEvents; TaskB.pData = &ClientB; if ( !xrtNetPostInit(&Post) || !xrtNetPost( xrtNetStreamWorker(pTcpB), &Post, exampleUpgradeTask, &TaskB) || !exampleSpinUntil(&TaskB.bDone) || !TaskB.bOk ) { iResult = 4; goto Cleanup; } pClientB = TaskB.pTls; /* Attach 成功即接管调用方的 Session 引用;这里不能再 Destroy。 */ pSessionB = NULL; /* 两个客户端都握手完成。 */ if ( !exampleSpinUntil(&ClientA.bOpen) || !exampleSpinUntil(&ClientB.bOpen) ) { iResult = 5; goto Cleanup; } printf("stream-tour: two clients upgraded, echo verified\n"); /* 取出两条服务端流:队列中的连接被 Accept 消费后才开始驱动回显。 */ { double iEnd = exampleTimerLimit(EXAMPLE_DEADLINE_MS); while ( (pServerA == NULL) || (pServerB == NULL) ) { xtlsstream* pOne = xrtTlsListenerAccept(pListener); if ( pOne != NULL ) { if ( pServerA == NULL ) { pServerA = pOne; } else { pServerB = pOne; } iEnd = exampleTimerLimit( EXAMPLE_DEADLINE_MS); continue; } if ( exampleTimerExpired(iEnd) ) { iResult = 6; goto Cleanup; } xrtThreadYield(); } } /* 回显到达:Read 回调里完成 Pullup/Read/Consume 核对。 */ iDeadline = exampleTimerLimit(EXAMPLE_DEADLINE_MS); while ( ClientA.Received < 9u ) { if ( exampleTimerExpired(iDeadline) ) { iResult = 6; goto Cleanup; } xrtThreadYield(); } if ( (ClientA.iPullup != 9u) || (ClientA.iRead != 4u) || (ClientA.iConsume != 5u) || (memcmp(ClientA.ReadOut, "vec-", 4) != 0) || !ClientA.bSessionOk || !ClientA.bDataOk || !ClientA.bReadMore || !ClientA.bEventsSwapped || (ClientA.iBound < 64u) ) { iResult = 6; goto Cleanup; } printf("client(starttls): vec=9 bytes read=4 consume=%zu", ClientA.iConsume); printf(" session+data=%s\n", (ClientA.bSessionOk && ClientA.bDataOk) ? "ok" : "fail"); /* Attach 客户端:SendVecAsync(Future 式聚集发送)。 */ AsyncVec[0].Data = (cbytes)"asy-"; AsyncVec[0].Size = 4; AsyncVec[1].Data = (cbytes)"ncvec"; AsyncVec[1].Size = 5; pSendFuture = xrtTlsStreamSendVecAsync(pClientB, AsyncVec, 2); if ( (pSendFuture == NULL) || (xrtFutureWaitFor(pSendFuture, EXAMPLE_DEADLINE_MS) != XWAIT_OK) || (xrtFutureState(pSendFuture) != XFUTURE_RESOLVED) ) { iResult = 7; goto Cleanup; } iDeadline = exampleTimerLimit(EXAMPLE_DEADLINE_MS); while ( ClientB.Received < 9u ) { if ( exampleTimerExpired(iDeadline) ) { iResult = 7; goto Cleanup; } xrtThreadYield(); } printf("client(attach): async-vec=ok future-written=9"); /* Pending:发送队列排空。 */ iDeadline = exampleTimerLimit(EXAMPLE_DEADLINE_MS); while ( xrtTlsStreamPending(pClientB) != 0u ) { if ( exampleTimerExpired(iDeadline) ) { iResult = 8; goto Cleanup; } xrtThreadYield(); } printf(" pending=0\n"); /* SendBound 的结果已在 Open 回调(Worker 内)算好带回。 */ iBound = ClientA.iBound; printf("bounds: 64 -> %zu ciphertext, error=%s\n", iBound, xrtTlsStreamError(pClientA) == NULL ? "(none)" : "err"); (void)xrtTlsStreamAsyncBytes(pClientA); (void)xrtTlsStreamAsyncCount(pClientA); * 双方 Read 回调内用 ParseTls 从明文块链解析。 ---- */ g_bHttpMode = true; { example_http_task Send; memset(&Send, 0, sizeof(Send)); Send.pStream = pClientB; if ( !xrtNetPost(xrtNetStreamWorker(pTcpB), &Post, exampleHttpSendTask, &Send) || !exampleSpinUntil(&Send.bDone) || !Send.bOk ) { iResult = 9; goto Cleanup; } } iDeadline = exampleTimerLimit(EXAMPLE_DEADLINE_MS); while ( !g_bHttpReqParsed || !g_bHttpRspParsed ) { if ( exampleTimerExpired(iDeadline) ) { iResult = 9; goto Cleanup; } xrtThreadYield(); } printf("http1-tls: request+response parsed on stream ok\n"); iResult = 0; Cleanup: xrtFutureDestroy(pSendFuture); * 共享截止时间等终态,超时者 Abort 兜底。 */ { xtlsstream* Streams[4]; double iEnd = exampleTimerLimit(EXAMPLE_DEADLINE_MS); bool bSettled; Streams[0] = pClientA; Streams[1] = pClientB; Streams[2] = pServerA; Streams[3] = pServerB; for ( size_t i = 0; i < 4; ++i ) { if ( Streams[i] != NULL ) { (void)xrtTlsStreamClose(Streams[i]); } } do { bSettled = true; for ( size_t i = 0; i < 4; ++i ) { xtlsstreamstate State = Streams[i] != NULL ? xrtTlsStreamState(Streams[i]) : XTLS_STREAM_CLOSED; if ( (State != XTLS_STREAM_CLOSED) && (State != XTLS_STREAM_FAILED) ) { bSettled = false; } } if ( bSettled || exampleTimerExpired(iEnd) ) { break; } xrtThreadYield(); } while ( 1 ); for ( size_t i = 0; i < 4; ++i ) { if ( Streams[i] != NULL ) { xtlsstreamstate State = xrtTlsStreamState(Streams[i]); if ( (State != XTLS_STREAM_CLOSED) && (State != XTLS_STREAM_FAILED) ) { (void)xrtTlsStreamAbort(Streams[i]); } } } (void)exampleSpinUntil(&ClientA.bClosed); (void)exampleSpinUntil(&ClientB.bClosed); } xrtTlsStreamDestroy(pClientA); xrtTlsStreamDestroy(pClientB); xrtTlsStreamDestroy(pServerA); xrtTlsStreamDestroy(pServerB); xrtTlsSessionDestroy(pSessionB); if ( pTcpA != NULL ) { (void)xrtNetStreamAbort(pTcpA); } if ( pTcpB != NULL ) { (void)xrtNetStreamAbort(pTcpB); } xrtNetStreamDestroy(pTcpA); xrtNetStreamDestroy(pTcpB); if ( pListener != NULL ) { double iEnd = exampleTimerLimit(EXAMPLE_DEADLINE_MS); (void)xrtTlsListenerClose(pListener); while ( xrtTlsListenerState(pListener) != XTLS_LISTENER_CLOSED ) { if ( exampleTimerExpired(iEnd) ) { break; } xrtThreadYield(); } } xrtTlsListenerDestroy(pListener); xrtNetResolverDestroy(pResolver); if ( (pEngine != NULL) && !xrtNetEngineDestroy(pEngine) && (iResult == 0) ) { iResult = 10; } xrtTlsVerifierRelease(pVerifier); xrtTlsIdentityRelease(pIdentity); return iResult; }
Что только что произошло. (1) Строка 1 проверяет разом два клиентских пути: starttls (переговорное обновление на открытом соединении — официальный ответ на упражнение главы 87) и attach (прямая навеска TCP-потока) — вместе с серверным Accept полное множество трёх способов построения слоя Stream. (2) Строка 2 демонстрирует пакетную отправку Vec (9 байтов) и потребление Span (прочитано 4, потреблено 5 — потребление может обгонять границу чтения, семантика уровня воды). (3) Строка 3 идёт асинхронной записью Vec: после завершения Future Pending в ноль — облик Vec/асинхронной ступени пяти ступеней отправки главы 68 на TLS-потоке. (4) Строка 4 меряет границу шифртекста: 64 байта открытого текста → 86 байтов шифртекста (16-байтовая метка AEAD + накладные расходы заголовка записи) — реальный якорь для планирования ёмкости. (5) Строка 5 гонит на потоке полный разбор запроса-ответа HTTP/1 — самопроверка, сцепляющая примитивы глав 84–87 в одну цепь, и анонс тома 9.
87.4契约
- Построение: сервер
StreamAccept(один шаг внутри колбэка Accept); клиент через встроенный attachTlsDial/DialAsync; обе стороны делят весь прикладной интерфейсxtlsstream. - События: Open/Read/Writable/Close изоморфны TCP-потоку; колбэки исполняются на Worker-е Engine; Close — единственное уведомление о конечном состоянии (результат + структурная ошибка).
- Жёсткое противодавление: остановка потребления = остановка приёма — возврат Span за Span + ожидание Writable при короткой записи есть стандартная эхо-форма; память отвязана от самого медленного клиента.
- Транспортный мост:
StreamTransportодалживает нижележащий TCP-поток (только-читающие запросы: адрес/состояние); кешировать между колбэками нельзя. - Двухуровневое закрытие:
Closeидёт со стеканием close_notify;Abortрвёт немедленно; одиночное соединение ждёт CLOSED/FAILED прежде Destroy; сервисный уровень ждёт «все соединения + слушатель» в ноль прежде выпуска разделяемой конфигурации. - Разделение конфигураций:
xtlsserverconfig(протокольная грань) иxtlsstreamconfig(грань потока) независимы; конфигурационные объекты разделяемы подсчётом ссылок. - Автоматизация рукопожатия: цикл Feed/Drive исполняет комбинированный слой — приложение никогда не видит шифртекста (граничное измерение
stream_tour— диагностическое употребление). - Обрезка:
XRT_MODULE_TLS_STREAMнезависимо (зависит от NET) — нужна лишь сессионная машина без транспортной комбинации, обрезайте.
Типичные ошибки
Симптом: пики памяти на высокой пропускной способности — Read-колбэк каждого соединения копирует всю зону приёма в собственную очередь, противодавление повисает в воздухе.
Причина: противодавление слоя Stream построено на «потреблении открытого текста (Consume)»; скопировать — не потребить, приёмная сторона продолжает продвигаться, и ваша очередь становится неограниченным буфером.
static void onRead(xtlsstream* pStream, const xnetbuf* pBuffer, ptr pData) { queue_push(g_MyQueue, pBuffer); /* скопировали, но не Consume: противодавление обнулено */ }
static void onRead(xtlsstream* pStream, const xnetbuf* pBuffer, ptr pData) { /* сколько можем обработать - столько потребляем; остальное оставляем следующему Read/Writable */ while ( xrtTlsStreamAvailable(pStream) != 0 ) { /* ... Front берёт Span, обработка + Consume ... */ if ( 下游满了 ) { return; } /* остановили потребление = остановили приём = противодавление работает */ } }
Симптом: в колбэке последнего соединения доступ к уже освобождённой идентичности/конфигурации — падение ровно на отключении последнего клиента.
Причина: подсчёт ссылок разделяемой конфигурации живёт на каждом соединении — закрытие Listener не равно закрытию соединений; выпуск обязан ждать обоих нулей.
listenerClose(...) { xrtTlsIdentityRelease(g_Identity); /* ещё есть активные соединения-пользователи */ }
/* атомарная сходка счётчиков: Connections==0 && ListenerClosed - только тогда единый выпуск */ if ( (xrtAtomic32Load(&Connections, XMEMORY_ACQUIRE) == 0) && (xrtAtomic32Load(&ListenerClosed, XMEMORY_ACQUIRE) != 0) ) { release_shared_assembly(); } /* эту проверку делают оба колбэка - Close и ListenerClose */
Симптом: изредка перепутанное поведение — закешированный указатель TCP-потока рассогласовывается после продвижения внутреннего состояния комбинированного объекта.
Причина: StreamTransport — диагностическое/запросное представление «транспортного моста на данный момент», не долгий дескриптор. Нужна транспортная способность (запросить адрес, состояние) — берите в момент надобности и пользуйтесь тут же.
g_SavedTcp = xrtTlsStreamTransport(pStream); /* сохранить */ /* несколько колбэков спустя */ send_on_tcp(g_SavedTcp, ...); /* прямая запись в транспорт мимо слоя TLS: рассогласование состояний */
/* запрашивать по надобности (скажем, журнал в Open); отправка - всегда через Send слоя TLS */ if ( xrtNetStreamRemote(xrtTlsStreamTransport(pStream), &Remote) ) { log_client(Remote); } xrtTlsStreamSend(pStream, Data, Size, &Written); /* открытый текст через парадную дверь */
Практические задания
Сгенерируйте самоподписанный сертификат, прогоните службу stream и подключитесь openssl s_client -connect localhost:8443 -alpn http/1.1; вводите текст и проверяйте эхо; пронаблюдайте адрес клиента, печатаемый сервером. Критерии приёмки: эхо побайтно верно; после разрыва (Ctrl-C или Q) срабатывает Close-колбэк сервера, счётчик соединений в ноль, процесс не выходит (продолжает обслуживать следующее соединение).
Вставьте перед Echo слой «аудита»: в Read-колбэке посчитайте байты открытого текста и число Span каждого соединения, на Close выдайте сводку (всего байтов / средний размер Span / длительность соединения). Критерии приёмки: 4 конкурентных соединения ведут статистику независимо; данные сходятся с отправленным openssl-ом; статистика не копирует открытый текст (по пути нулевого копирования).
Реализуйте шлюз «сперва команда открытого рукопожатия, потом обновление»: после TCP Accept не сразу TLS, а построчный разбор команд (строчное кадрирование главы 72); получив STARTTLS\r\n, зовите StreamAttach и обновляйтесь до TLS (остальные команды до обновления отрабатывают эхом в открытом режиме). Подсказка: один и тот же объект соединения до и после обновления — смотрите клиентский путь starttls в stream_tour. Критерии приёмки: на открытой стадии команды нормально отдаются эхом; после обновления открытые команды отвергаются; openssl через подсказку starttls завершает обновление и зашифрованную сессию; счётчик соединений и ресурсы обеих стадий вычищены верно.
Итоги
| Тема | Кратко |
|---|---|
| Комбинированный объект | сервер StreamAccept (один шаг в колбэке Accept) / клиент со встроенным attach в TlsDial |
| Событийная модель | Open/Read/Writable/Close изоморфны TCP-потоку; исполнение на потоках Worker |
| Жёсткое противодавление | остановили потребление = остановили приём; возврат Span + ожидание Writable; память отвязана от самого медленного |
| Прикладной интерфейс | Send (частичный приём) / Available/Buffer/Front/Consume / Close / Abort / State |
| Транспортный мост | StreamTransport одалживает TCP-поток — по надобности, без кеша; отправка через парадную дверь слоя TLS |
| Разделение конфигураций | xtlsserverconfig (протокольная) / xtlsstreamconfig (потоковая); разделяемый подсчёт ссылок |
| Сервисный жизненный цикл | единый выпуск разделяемой сборки после «все соединения + слушатель» в ноль (атомарная сходка) |
| Одиночный жизненный цикл | Close/Abort → ждать CLOSED/FAILED → Destroy (правило главы 82) |
| Автоматизация рукопожатия | Feed/Drive исполняет комбинированный слой, приложение не видит шифртекста |
| Обрезка | XRT_MODULE_TLS_STREAM независимо от сессионной машины; stream_tour — самопроверка всех возможностей |
📖 Справочники API в этой главе:tls (201 записей)tls_stream (67 записей)net (247 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки