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

Главы 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 движутся только вперёд:

CONNECTINGсоединение установлено →OPEN
OPENClose / пир закрылся →CLOSING
CLOSINGуборка завершена →CLOSED
OPENзапрошен Close →CLOSING
CLOSINGоперации в полёте завершены →CLOSED

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, сервер принимает и печатает. Это канонический образец «минимум кода проходит полный жизненный цикл»:

examples/network/tcp_sync/main.c源码 ↗
#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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/network/tcp_sync/main.c -lws2_32 -liphlpapireceived: hello

Что только что произошло. Шесть узлов заслуживают внимания. (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-сервиса: чтение и запись через таблицу событий, сервер эхирует прямым переносом приёмного буфера (нулевое копирование), после отключения пира — чистый дренаж и закрытие:

examples/network/tcp/main.c源码 ↗
















 */


#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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/network/tcp/main.c -lws2_32 -liphlpapilistening on 127.0.0.1:52173reply: hello TCP

Три ключевых пункта событийного лица. (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.

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

Ловушка 1: разрушение до CLOSED:Ловушка 1: разрушение до CLOSED

Симптомы: изредка падения или ассерты, особенно при выходе нагруженного сервера; иногда зависает xrtNetEngineDestroy.

Причина: до публикации CLOSED объект ещё используется операциями в полёте и Worker; ранний Destroy освобождает память, на которую ещё есть ссылки.

错误写法
xrtNetStreamClose(pStream);
xrtNetStreamDestroy(pStream);   /* состояние может быть ещё CLOSING */
正确写法
xrtNetStreamClose(pStream);
while ( xrtNetStreamState(pStream) != XNET_STREAM_CLOSED ) {
	xrtThreadYield();
}
xrtNetStreamDestroy(pStream);
Ловушка 2: забытый освобождение результата Recv:Ловушка 2: забытый освобождение результата Recv

Симптомы: память в долгоживущих программах растёт; в статистике распределений 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);
}
Ловушка 3: жёстко вшитый порт прослушивания:Ловушка 3: жёстко вшитый порт прослушивания

Симптомы: ошибки 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 (соединение)
Состояния StreamCONNECTING → 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 записей)

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