Ориентация главы
Главы 63–66 закрыли вопрос «откуда берутся адреса» — разбор, порты, буферы и DNS. Эта глава начинает передавать настоящие данные: создать Engine, слушать порт, подключаться, слать и принимать байты и, наконец, чисто всё закрыть. Модуль TCP — фундамент всего дальнейшего: противодавление и серверные формы главы 68, TLS тома 8 и HTTP тома 9 — всё строится на одном объекте xnetstream. В этой главе осваиваются два лица: блокирующее синхронное (кратчайший путь кода для утилит) и событийное с обратными вызовами (каркас продакшн-сервисов), а два самых ошибкоопасных дела — «машина состояний» и «порядок закрытия» — разбираются досконально.
67.1引入
Представьте, что вы пишете небольшую утилиту миграции данных: прочитать пакет данных из одной локальной службы и переслать в другую. Сетевые потребности такого инструмента скромны, но полны — подключиться, отправить, принять целиком, закрыть чисто. Каждый, кто писал это на голых сокетах, знает: настоящее усилие — никогда не те пять системных вызовов, а завершение: что делать, если соединение застряло в полузакрытом состоянии? Можно ли закрываться, пока в буфере есть неотправленные данные? Когда можно безопасно освободить объект?
TCP-уровень XRT встроил эти ловушки в контракты объектов: у потоков явная машина состояний, CLOSED — единственное конечное состояние, и никакая ссылка не освобождается до его наступления; закрытие имеет две семантики — «дренировать» и «прервать немедленно»; один Engine ведёт циклы событий всех соединений. Глава проходит модель на двух полных программах.
67.2概念
Три объекта, один двигатель
xnetengine: сетевой движок. Управляет платформенным событийным бэкендом (IOCP / epoll / kqueue / io_uring) и набором потоков Worker; каждый Listener и Stream работают, будучи подвешены на некоторый Engine. В конфигурацииWorkersзадаёт число потоков. Практические значения для Worker: утилитам и модульным тестам хватает 1–2 — потоки Engine только диспетчеризуют события (планирование мелких задач прямо на цикл Worker — через сетевые группы задач task_net главы 61) и обратные вызовы, никакой деловой логики; планирование мощности сервера (число Worker, глубина очередей, уровни противодавления) — тема главы 68. Несколько Engine могут сосуществовать, но соглашение этой и последующих глав — один Engine на процесс.xnetlistener: слушатель. Привязывает адрес (порт 0 предоставляет системе) и принимает входящие соединения.xnetstream: поток. Читающе-пишущее лицо одного TCP-соединения; клиент и сервер держат по одному.
Состояния и Stream, и Listener движутся только вперёд:
CLOSED — единственное конечное состояние, и это гарантировано до публикации: сокет, буферы, операции в полёте и активное использование Engine уже завершены. Иначе говоря, только увидев CLOSED, можно освобождать ссылку вызывающего.
Одно лицо, два способа вызова
xnetstream одновременно предоставляет два соглашения о вызовах:
- Синхронное лицо:
xrtNetListenerAcceptWaitблокируется в ожидании соединения,xrtNetStreamRecvблокируется на приёме данных и возвращает владеющий байтовый результатxnetbytes. Утилитные программы (проверки живости, разовые миграции, простые клиенты) используют синхронное лицо — код кратчайший. - Событийное лицо: регистрируется таблица обратных вызовов (указатели
Accept/Read/Closeи др.), и потоки Worker движка вызывают их по мере поступления событий. Продакшн-сервисы используют событийное лицо ради высокой конкурентности и жёсткого противодавления.
Оба лица разделяют одни объекты и машину состояний; в этой главе показана по одной полной программе на каждое на каждое.
Две семантики закрытия и подсчёт ссылок
xrtNetStreamClose: запрашивает дренирующее закрытие — отправить данные в полёте, пройти протокольное завершение и войти вCLOSED.xrtNetStreamAbort: немедленный срыв — отбросить данные в полёте и как можно быстрее войти вCLOSED. Пути ошибок и секцииCleanupиспользуют его против зависаний.
Объекты, возвращаемые функциями создания, несут одну ссылку вызывающего; среда исполнения держит ещё одну внутреннюю ссылку до конца единственного обратного вызова Close. Правильный порядок выхода: запросить закрытие (Close или Abort) → дождаться состояния CLOSED → xrtNetStreamDestroy освобождает ссылку вызывающего. Ref/Destroy можно использовать из разных потоков, но Ref не может воскресить уже завершённый объект с нуля.
67.3示例
Первая полная программа: эхо на синхронном лице
Следующая программа взята из примера репозитория examples/network/tcp_sync/main.c: поднимает петлевой слушатель, подключается к нему и шлёт hello, сервер принимает и печатает. Это канонический образец «минимум кода проходит полный жизненный цикл»:
#include <stdio.h> #include <string.h> #include <xrt.h> */ /* 使用现有 Engine 完成一次本地阻塞 Accept 和接收。 */ int main(void) { xnetengineconfig EngineConfig; xnetlistenconfig ListenConfig; xnetengine* pEngine; xnetlistener* pListener; xnetstream* pClient; xnetstream* pServer; xnetbytes* pBytes; xbytesview View; xnetaddr Address; bool bReceived; xrtNetEngineConfigInit(&EngineConfig); EngineConfig.Workers = 2; pEngine = xrtNetEngineCreate(&EngineConfig); if ( (pEngine == NULL) || !xrtNetEngineStart(pEngine) ) { return 1; } xrtNetListenConfigInit(&ListenConfig); (void)xrtNetAddrLoopback( &ListenConfig.Address, XNET_FAMILY_IPV4, 0 ); pListener = xrtNetListen(pEngine, &ListenConfig, NULL, NULL, NULL); if ( (pListener == NULL) || !xrtNetListenerLocal(pListener, &Address) ) { return 2; } pClient = xrtNetStreamConnect( pEngine, &Address, 1, NULL, NULL, NULL ); pServer = xrtNetListenerAcceptWait( pListener,3000, NULL ); if ( (pClient == NULL) || (pServer == NULL) || (xrtNetStreamSend(pClient, "hello", 5) != XNET_RESULT_OK) ) { return 3; } pBytes = xrtNetStreamRecv( pServer, 0,3000, NULL ); View = xrtNetBytesView(pBytes); bReceived = pBytes != NULL; if ( pBytes != NULL ) { printf("received: %.*s\n", (int)View.Size, (cstr)View.Data); } xrtNetBytesDestroy(pBytes); (void)xrtNetStreamAbort(pClient); (void)xrtNetStreamAbort(pServer); (void)xrtNetListenerClose(pListener); while ( (xrtNetStreamState(pClient) != XNET_STREAM_CLOSED) || (xrtNetStreamState(pServer) != XNET_STREAM_CLOSED) || (xrtNetListenerState(pListener) != XNET_LISTENER_CLOSED) ) { xrtThreadYield(); } xrtNetStreamDestroy(pClient); xrtNetStreamDestroy(pServer); xrtNetListenerDestroy(pListener); return xrtNetEngineDestroy(pEngine) && bReceived ? 0 : 4; }
Что только что произошло. Шесть узлов заслуживают внимания. (1) Workers = 2 — для утилиты достаточно двух Worker; сам Engine деловой работой не занимается. (2) Адрес прослушивания строится через xrtNetAddrLoopback для петли, с портом 0; затем xrtNetListenerLocal извлекает настоящий порт, назначенный системой, — стандартная поза для конкурентных тестов, которым нельзя конфликтовать. (3) Третий аргумент 1 в xrtNetStreamConnect клиента — количество адресов; на синхронном лице соединение завершается в фоне на движке. (4) xrtNetListenerAcceptWait принимает дедлайн relative milliseconds; если соединения нет в срок, возвращается NULL, и в слоте ошибки потока остаётся категория тайм-аута (модель главы 4 здесь вступает в силу). (5) xrtNetStreamRecv возвращает владеющий xnetbytes; заимствуйте представление через xrtNetBytesView для чтения и вызывайте xrtNetBytesDestroy по завершении. (6) Уборка сначала делает Abort обоим потокам и слушателю, крутится, пока все три не достигнут CLOSED, затем по одному Destroy, и в конце xrtNetEngineDestroy.
Полная форма событийного лица: сервис эха на обратных вызовах
Вторая программа взята из examples/network/tcp/main.c — стандартный каркас продакшн TCP-сервиса: чтение и запись через таблицу событий, сервер эхирует прямым переносом приёмного буфера (нулевое копирование), после отключения пира — чистый дренаж и закрытие:
*/ #include <stdio.h> #include <string.h> #include <xrt.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; } typedef struct exampletcp { xnetstream* Client; xatomicptr Server; xatomic32 Accepted; xatomic32 Reply; xatomic32 Closed; } exampletcp; /* 接管服务端连接并设置回调数据。 */ static bool exampleAccept( xnetlistener* pListener, xnetstream* pStream, ptr pData ) { exampletcp* pExample = (exampletcp*)pData; (void)pListener; (void)xrtNetStreamSetData(pStream, pExample); xrtAtomicPtrStore( &pExample->Server, pStream, XMEMORY_RELEASE ); (void)xrtAtomic32FetchAdd( &pExample->Accepted, 1, XMEMORY_RELEASE ); return true; } /* 服务端直接回送字节,客户端输出回复。 */ static void exampleRead( xnetstream* pStream, xnetbuf* pBuffer, ptr pData ) { exampletcp* pExample = (exampletcp*)pData; xnetstream* pServer = (xnetstream*)xrtAtomicPtrLoad( &pExample->Server, XMEMORY_ACQUIRE ); char Data[64]; size_t iSize; if ( pStream == pServer ) { (void)xrtNetStreamSendBuffer(pStream, pBuffer); return; } iSize = xrtNetBufRead(pBuffer, Data, sizeof(Data)); printf("reply: %.*s\n", (int)iSize, Data); (void)xrtAtomic32FetchAdd( &pExample->Reply, 1, XMEMORY_RELEASE ); } /* 记录正常关闭。 */ static void exampleClose( xnetstream* pStream, xnetresult Result, const xerror* pError, ptr pData ) { exampletcp* pExample = (exampletcp*)pData; (void)pStream; if ( (Result == XNET_RESULT_OK) && (pError == NULL) ) { (void)xrtAtomic32FetchAdd( &pExample->Closed, 1, XMEMORY_RELEASE ); } } /* 建立回环服务器和客户端,发送一次请求后完整回收。 */ int main(void) { exampletcp Example; xnetengineconfig EngineConfig; xnetlistenconfig ListenConfig; xnetlistenerevents ListenerEvents; xnetstreamevents StreamEvents; xnetengine* pEngine; xnetlistener* pListener; xnetstream* pServer; xnetaddr Address; double iDeadline; str sEndpoint; int iResult = 1; memset(&Example, 0, sizeof(Example)); xrtAtomicPtrInit(&Example.Server, NULL); memset(&ListenerEvents, 0, sizeof(ListenerEvents)); memset(&StreamEvents, 0, sizeof(StreamEvents)); ListenerEvents.Accept = exampleAccept; StreamEvents.Read = exampleRead; StreamEvents.Close = exampleClose; pEngine = NULL; pListener = NULL; pServer = NULL; sEndpoint = NULL; xrtNetEngineConfigInit(&EngineConfig); EngineConfig.Workers = 2; pEngine = xrtNetEngineCreate(&EngineConfig); if ( (pEngine == NULL) || !xrtNetEngineStart(pEngine) ) { goto Cleanup; } xrtNetListenConfigInit(&ListenConfig); (void)xrtNetAddrLoopback( &ListenConfig.Address, XNET_FAMILY_IPV4, 0 ); pListener = xrtNetListen( pEngine, &ListenConfig, &ListenerEvents, &StreamEvents, &Example ); if ( (pListener == NULL) || !xrtNetListenerLocal(pListener, &Address) ) { iResult = 2; goto Cleanup; } sEndpoint = xrtNetAddrEndpointString(&Address); if ( sEndpoint == NULL ) { iResult = 3; goto Cleanup; } printf("listening on %s\n", sEndpoint); xrtFree(sEndpoint); sEndpoint = NULL; Example.Client = xrtNetStreamConnect( pEngine, &Address, 1, NULL, &StreamEvents, &Example ); if ( Example.Client == NULL ) { iResult = 4; goto Cleanup; } iDeadline = exampleTimerLimit(3000); while ( xrtAtomic32Load(&Example.Accepted, XMEMORY_ACQUIRE) == 0 ) { if ( exampleTimerExpired(iDeadline) ) { iResult = 5; goto Cleanup; } xrtThreadYield(); } pServer = (xnetstream*)xrtAtomicPtrLoad( &Example.Server, XMEMORY_ACQUIRE ); if ( pServer == NULL ) { iResult = 5; goto Cleanup; } if ( xrtNetStreamSend( Example.Client, "hello TCP", 9 ) != XNET_RESULT_OK ) { iResult = 6; goto Cleanup; } while ( xrtAtomic32Load(&Example.Reply, XMEMORY_ACQUIRE) == 0 ) { if ( exampleTimerExpired(iDeadline) ) { iResult = 7; goto Cleanup; } xrtThreadYield(); } (void)xrtNetStreamClose(Example.Client); (void)xrtNetStreamClose(pServer); iDeadline = exampleTimerLimit(3000); while ( xrtAtomic32Load(&Example.Closed, XMEMORY_ACQUIRE) != 2 ) { if ( exampleTimerExpired(iDeadline) ) { iResult = 8; goto Cleanup; } xrtThreadYield(); } (void)xrtNetListenerClose(pListener); iDeadline = exampleTimerLimit(3000); while ( xrtNetListenerState(pListener) != XNET_LISTENER_CLOSED ) { if ( exampleTimerExpired(iDeadline) ) { iResult = 8; goto Cleanup; } xrtThreadYield(); } iResult = 0; Cleanup: xrtFree(sEndpoint); pServer = (xnetstream*)xrtAtomicPtrLoad( &Example.Server, XMEMORY_ACQUIRE ); if ( (Example.Client != NULL) && (xrtNetStreamState(Example.Client) != XNET_STREAM_CLOSED) ) { (void)xrtNetStreamAbort(Example.Client); } if ( (pServer != NULL) && (xrtNetStreamState(pServer) != XNET_STREAM_CLOSED) ) { (void)xrtNetStreamAbort(pServer); } if ( (pListener != NULL) && (xrtNetListenerState(pListener) != XNET_LISTENER_CLOSED) ) { (void)xrtNetListenerClose(pListener); } iDeadline = exampleTimerLimit(3000); while ( ((Example.Client != NULL) && (xrtNetStreamState(Example.Client) != XNET_STREAM_CLOSED)) || ((pServer != NULL) && (xrtNetStreamState(pServer) != XNET_STREAM_CLOSED)) || ((pListener != NULL) && (xrtNetListenerState(pListener) != XNET_LISTENER_CLOSED)) ) { if ( exampleTimerExpired(iDeadline) ) { if ( iResult == 0 ) { iResult = 8; } break; } xrtThreadYield(); } xrtNetStreamDestroy(Example.Client); xrtNetStreamDestroy(pServer); xrtNetListenerDestroy(pListener); if ( (pEngine != NULL) && !xrtNetEngineDestroy(pEngine) && (iResult == 0) ) { iResult = 8; } return iResult; }
Три ключевых пункта событийного лица. (1) Обратный вызов Accept принимает каждое новое соединение: вызывает xrtNetStreamSetData, чтобы прикрепить деловой контекст к потоку, и все последующие обратные вызовы могут его извлечь — самодельное отображение «поток → контекст» не нужно. (2) В обратном вызове Read сервера xrtNetStreamSendBuffer передаёт приёмный буфер обратно по ссылке — данные идут из приёмного буфера в очередь отправки без копий; так правильно писать эхо-сервисы. Read клиента демонстрирует обычное чтение (xrtNetBufRead копирует в стековый буфер). (3) Обратный вызов Close — единственное уведомление о конечном состоянии: параметр xnetresult и объект ошибки различают «чистое закрытие» и «закрытие по ошибке»; образец считает только чистые. Обратные вызовы выполняются в потоках Worker движка, а главный поток наблюдает прогресс атомарным счётчиком и дедлайном — глава 68 поднимет это ожидание до настоящих примитивов синхронизации и управления противодавлением.
67.4契约
- Состояния: состояния Stream и Listener движутся только вперёд;
CLOSED— единственное конечное; до входа вCLOSEDсокет, буферы и операции в полёте уже завершены. - Ссылки: создание возвращает ссылку вызывающего; среда держит внутреннюю до конца обратного вызова
Close; сначала запрос закрытия, ожиданиеCLOSED, затемDestroy;Refпотокобезопасен, воскрешать не может. - Закрытие:
Closeдренирует,Abortнемедленно; пути ошибок всегдаAbort, счастливый путь даёт данным дойти. - Потоки: событийные обратные вызовы выполняются на Worker движка; обратный вызов
Closeещё может убираться на Worker, аxrtNetEngineDestroyждёт нормального выхода Worker. - Ошибки: отказы объявляются возвращаемым значением (
NULL/ неXNET_RESULT_OK), подробности в слоте ошибки потока; категории и домены — по модели главы 4.
Типичные ошибки
Симптомы: изредка падения или ассерты, особенно при выходе нагруженного сервера; иногда зависает xrtNetEngineDestroy.
Причина: до публикации CLOSED объект ещё используется операциями в полёте и Worker; ранний Destroy освобождает память, на которую ещё есть ссылки.
xrtNetStreamClose(pStream); xrtNetStreamDestroy(pStream); /* состояние может быть ещё CLOSING */
xrtNetStreamClose(pStream); while ( xrtNetStreamState(pStream) != XNET_STREAM_CLOSED ) { xrtThreadYield(); } xrtNetStreamDestroy(pStream);
Симптомы: память в долгоживущих программах растёт; в статистике распределений xnetbytes только увеличивается.
Причина: xrtNetStreamRecv возвращает владеющий байтовый результат; в отличие от буферов главы 15, его время жизни целиком принадлежит вызывающему.
xnetbytes* pBytes = xrtNetStreamRecv(pStream, 0, deadline, NULL); printf("%.*s\n", (int)xrtNetBytesView(pBytes).Size, (cstr)xrtNetBytesView(pBytes).Data); /* нет Destroy: утечка одного объекта результата на каждый приём */
xnetbytes* pBytes = xrtNetStreamRecv(pStream, 0, deadline, NULL); if ( pBytes != NULL ) { printf("%.*s\n", (int)xrtNetBytesView(pBytes).Size, (cstr)xrtNetBytesView(pBytes).Data); xrtNetBytesDestroy(pBytes); }
Симптомы: ошибки bind при параллельных тестах или когда коллега одновременно поднимает сервис — или хуже, подключение к чужому процессу на этом порту.
Причина: фиксированный порт превращает «какой процесс владеет каким портом» в проблему глобальной координации.
Правильный подход: укажите порт прослушивания 0 и дайте системе назначить, затем извлеките настоящую точку через xrtNetListenerLocal и передайте клиенту — оба образца используют именно эту позу.
Практические задания
Превратите программу синхронного лица в «ответить тем, что получено, в верхнем регистре»: сервер переводит полученный текст в верхний регистр и отправляет обратно; клиент печатает результат. Меняйте только логику приёма/отправки в main.c; код жизненного цикла не трогайте.
Вызовите xrtNetListenerAcceptWait с более коротким дедлайном и в ветке тайм-аута используйте xrtErrorIs(xrtGetError(), XERR_TIMEOUT) для классификации и напечатайте accept timeout. Подсказка: сначала убедитесь, что функция объявила отказ (вернула NULL), и только затем читайте слот ошибки потока — это «ловушка 3» главы 4 на практике.
xrtNetStreamConnect по диапазону портов на 127.0.0.1 (скажем, 51000–51050) по одному и выведите список открытых портов; различите две неудачи «отказано в соединении» и «тайм-аут» (по категории ошибки). Критерии приёмки: при запущенном эхо-сервисе этой главы сканер точно перечисляет его порт; явно отказанные и проглоченные файрволом порты получают разные пометки; после сканирования всего Engine разрушается нормально, без утечек.
Итоги
| Тема | Краткая справка |
|---|---|
| Тройка объектов | Engine (пул потоков + событийный бэкенд) / Listener (прослушивание) / Stream (соединение) |
| Состояния Stream | CONNECTING → OPEN → CLOSING → CLOSED, только вперёд |
| Порядок выхода | запрос закрытия (Close/Abort) → ждать CLOSED → Destroy → Engine последним |
| Синхронное лицо | AcceptWait / Send / Recv (владеющий xnetbytes, Destroy по завершении) |
| Событийное лицо | таблица обратных вызовов + SetData для контекста; SendBuffer без копий; Close — уведомление о конце |
| Назначение портов | порт 0 + ListenerLocal для настоящей точки; без конфликтов при конкурентности |
| Ошибки | возвращаемое значение объявляет отказ; всё кроме XNET_RESULT_OK — проверить слот ошибки потока |
📖 Справочники API в этой главе:tcp (81 записей)tcp_server (22 записей)net (247 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки