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

Модуль UDP вписывает датаграммы в модель движка: двойственная форма — безсвязный конец (recvfrom несёт адрес источника — любой источник, фильтруете вы) и соединённая (после connect принимается только тот пир — клиентская форма); событийный приём/передача (колбэки движка — та же грань колбэков, что у TCP); семейство батчинга (пример udp_batch — амортизация нескольких датаграмм за один приём/передачу); мультикаст (вступление/выход из группы — пример multicast); PMTU и асинхронные ошибки (примеры udp_introspect/udp_errors — ICMP unreachable и прочие возможности платформы). У UDP нет управления потоком и машины состояний — но колбэки движка, подсчёт ссылок и протокол остановки — все те же.

69.1引入

Существенное различие UDP и TCP не «надёжный против ненадёжного», а граница данных: TCP — байтовый поток (границы сообщений — ваша забота; цепочка буферов главы 65 и кадрирование главы 72 существуют ради этого); UDP — датаграммы (один send — одно сообщение, один recv — одно полное сообщение — границу хранит ядро). Это различие делает форму API UDP совершенно иной: без бюджета отправки (датаграмма либо входит в очередь, либо нет), без цепочки приёма (одно сообщение — один буфер), без управления потоком (ненадёжность — свойство: аудио/видео реального времени скорее потеряет, чем накопит).

Выбор из двух форм: безсвязный конец (серверная форма) — после bind принимает от любого источника (recvfrom прикладывает адрес источника; ответ sendto указывает адресата); соединённая форма (клиентская) — после connect «запирает» пира и принимает только от него (фильтр делает ядро), send шлёт напрямую (без указания каждый раз). UDP connect не делает рукопожатия (это просто фильтр + адрес по умолчанию) — семантически совершенно не то, что TCP connect (реальное установление соединения).

69.2概念

Двойственная форма и жизненный цикл

1Созданиеобъект xnetudp -> bind (безсвязный) или connect (соединённая)
2Состояниямашина состояний UDP (BINDING/BOUND/CONNECTING/CONNECTED — переходные состояния асинхронного bind/connect)
3Приём/передачабезсвязный recvfrom (с источником) / sendto (с адресатом); соединённый recv/send (фиксированный пир)
4ЗакрытиеClose -> CLOSED -> Destroy — подсчёт ссылок как у Stream (контракт главы 67 продолжается)

Асинхронные bind/connect: привязка и соединение UDP — тоже операции движка (переходы машины состояний — опрос xrtNetUdpState или ожидание событий; помощник WaitState примера udp демонстрирует опрос с дедлайном).

Грань событийных колбэков

Таблица событий UDP (та же форма, что у Stream): Read (датаграмма пришла — recvfrom/recv в колбэке), Write (можно отправлять), Close (уведомление о финале). Приём/передача в колбэке: прибытие датаграммы запускает колбэк, колбэк подтягивает (вытягивающий recv — датаграммы в очереди, колбэк — уведомление); пакетное подтягивание (несколько за один колбэк — эффективная форма разгона завалов). Ограничения Worker те же, что у Stream (колбэки на Worker, межпоточно Post — контракт главы 68 в UDP-версии).

Семейство батчинга и структура датаграммы

xnetudppacket (носитель датаграммы + адреса источника); пакетные варианты оперируют несколькими датаграммами сразу (подача/приём массивом) — амортизирующий вход сценариев высокого PPS (DNS-серверы, мониторинговые репорты). Семейство Msg (SendMsg/RecvMsg — родня портового слоя главы 64): векторная отправка с адресатом — многобуферный вариант UDP.

Мультикаст и PMTU

Мультикаст (пример multicast): вступление/выход из групп (адрес группы + локальный интерфейс), управление TTL (прыжки между сегментами); приём/передача тем же API, что юникаст (адрес группы как адресат). Зонд PMTU и асинхронные ошибки: oversized send отбрасывается по пути (при запрещённой фрагментации) — ICMP «frag needed» возвращается асинхронно (RecvError по возможностям платформы — применение гейта возможностей главы 64 к UDP); пример udp_errors демонстрирует приём ошибок, udp_introspect — запросы PMTU/статистики.

69.3示例

Полная программа: двойственный петлевой контур

Из примера репозитория examples/network/udp/main.c — безсвязный конец + соединённая форма в одной сцене:

examples/network/udp/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;
}



/* 在截止时间内等待 UDP 状态转换。 */
static bool exampleUdpWaitState(xnetudp* pUdp, xnetudpstate State)
{
	double iDeadline = exampleTimerLimit(3000);

	while ( xrtNetUdpState(pUdp) != State ) {
		if ( exampleTimerExpired(iDeadline) ) {
			return false;
		}
		xrtThreadYield();
	}
	return true;
}



/* 在截止时间内拉取一个 UDP 包。 */
static xnetudppacket* exampleUdpReceive(xnetudp* pUdp)
{
	double iDeadline = exampleTimerLimit(3000);
	xnetudppacket* pPacket;

	for ( ;; ) {
		pPacket = xrtNetUdpReceive(pUdp);
		if ( pPacket != NULL ) {
			return pPacket;
		}
		if ( exampleTimerExpired(iDeadline) ) {
			return NULL;
		}
		xrtThreadYield();
	}
}



/* 建立绑定端和连接端,完成一次 UDP 回环收发。 */
int main(void)
{
	xnetengineconfig EngineConfig;
	xnetudpconfig UdpConfig;
	xnetengine* pEngine = NULL;
	xnetudp* pServer = NULL;
	xnetudp* pClient = NULL;
	xnetudppacket* pPacket = NULL;
	xnetaddr Address;
	xnetdgramcontrol Control;
	str sEndpoint = NULL;
	int iResult = 1;

	xrtNetEngineConfigInit(&EngineConfig);
	EngineConfig.Workers = 2;
	xrtNetUdpConfigInit(&UdpConfig);
	UdpConfig.ReceiveMeta = XNET_DGRAM_META_DESTINATION;
	pEngine = xrtNetEngineCreate(&EngineConfig);
	if ( (pEngine == NULL) || !xrtNetEngineStart(pEngine) ) {
		goto Cleanup;
	}
	(void)xrtNetAddrLoopback(&Address, XNET_FAMILY_IPV4, 0);
	pServer = xrtNetUdpBind(pEngine, &Address, 0,
		&UdpConfig, NULL, NULL);
	if ( (pServer == NULL) || !xrtNetUdpLocal(pServer, &Address) ) {
		iResult = 2;
		goto Cleanup;
	}
	sEndpoint = xrtNetAddrEndpointString(&Address);
	if ( sEndpoint == NULL ) {
		iResult = 3;
		goto Cleanup;
	}
	printf("UDP server: %s\n", sEndpoint);
	xrtFree(sEndpoint);
	sEndpoint = NULL;
	pClient = xrtNetUdpConnect(pEngine, &Address, 1,
		NULL, NULL, NULL);
	if ( (pClient == NULL) ||
		 !exampleUdpWaitState(pServer, XNET_UDP_OPEN) ||
		 !exampleUdpWaitState(pClient, XNET_UDP_OPEN) ) {
		iResult = 4;
		goto Cleanup;
	}
	if ( xrtNetUdpSend(pClient, "hello UDP", 9) !=
		 XNET_RESULT_OK ) {
		iResult = 5;
		goto Cleanup;
	}
	pPacket = exampleUdpReceive(pServer);
	if ( pPacket == NULL ) {
		iResult = 6;
		goto Cleanup;
	}
	printf("received: %.*s\n",
		(int)xrtNetUdpPacketSize(pPacket),
		(cstr)xrtNetUdpPacketData(pPacket));
	if ( ((xrtNetUdpPacketMeta(pPacket)->Flags &
		  XNET_DGRAM_META_DESTINATION) == 0) ||
		 ((xrtNetUdpSendControlAvailable(pServer) &
		  XNET_DGRAM_CONTROL_SOURCE) == 0) ) {
		iResult = 7;
		goto Cleanup;
	}
	memset(&Control, 0, sizeof(Control));
	Control.Flags = XNET_DGRAM_CONTROL_SOURCE;
	Control.Source = xrtNetUdpPacketMeta(pPacket)->Destination;
	Control.Source.Port = 0;
	if ( xrtNetUdpSendMsg(
		pServer,
		xrtNetUdpPacketRemote(pPacket),
		&Control,
		"reply",
		5
	) != XNET_RESULT_OK ) {
		iResult = 8;
		goto Cleanup;
	}
	xrtNetUdpPacketDestroy(pPacket);
	pPacket = exampleUdpReceive(pClient);
	if ( (pPacket == NULL) || (xrtNetUdpPacketSize(pPacket) != 5) ||
		 (memcmp(xrtNetUdpPacketData(pPacket), "reply", 5) != 0) ) {
		iResult = 9;
		goto Cleanup;
	}
	printf("response: %.*s\n",
		(int)xrtNetUdpPacketSize(pPacket),
		(cstr)xrtNetUdpPacketData(pPacket));
	iResult = 0;

Cleanup:
	xrtNetUdpPacketDestroy(pPacket);
	xrtFree(sEndpoint);
	if ( pClient != NULL ) {
		(void)xrtNetUdpClose(pClient);
		(void)exampleUdpWaitState(pClient, XNET_UDP_CLOSED);
	}
	if ( pServer != NULL ) {
		(void)xrtNetUdpClose(pServer);
		(void)exampleUdpWaitState(pServer, XNET_UDP_CLOSED);
	}
	xrtNetUdpDestroy(pClient);
	xrtNetUdpDestroy(pServer);
	if ( (pEngine != NULL) && !xrtNetEngineDestroy(pEngine) &&
		 (iResult == 0) ) {
		iResult = 10;
	}
	return iResult;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/network/udp/main.c -lws2_32 -liphlpapiUDP server: 127.0.0.1:NNNNNreceived: hello UDP

Что только что произошло. (1) Безсвязный конец сервера делает bind (порт 0 — динамическое назначение; Local извлекает и печатает 127.0.0.1:NNNNN); ожидание состояния (помощник WaitState опрашивает BOUND в пределах дедлайна — управление переходным состоянием асинхронного bind). (2) Клиент в соединённой форме connect к серверу — принимает только от него (ответ сервера). (3) Клиент send hello UDP — без параметра адресата (connect уже запер); (4) событие Read сервера срабатывает, recvfrom подтягивает (источник — клиент) → печать received: hello UDP — каждая форма на своём месте. (5) Ответ sendto на адрес клиента — ответ безсвязного конца обязан указывать адресата.

Полная программа: батчинг и мультикаст

Из examples/network/udp_batch/main.c и examples/network/udp_multicast/main.c (читать вместе):

examples/network/udp_batch/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;
}



/* 等 Future 成功解析。 */
static bool exampleFutureWait(xfuture* pFuture)
{
	return (pFuture != NULL) &&
		(xrtFutureWaitFor(pFuture, INT64_C(3000)) == XWAIT_OK) &&
		(xrtFutureState(pFuture) == XFUTURE_RESOLVED);
}



/* 在截止时间内等 UDP 进入指定状态。 */
static bool exampleWaitState(xnetudp* pUdp, xnetudpstate State)
{
	double iDeadline = exampleTimerLimit(3000);

	while ( xrtNetUdpState(pUdp) != State ) {
		if ( exampleTimerExpired(iDeadline) ) {
			return false;
		}
		xrtThreadYield();
	}
	return true;
}



/* 收满 count 条(普通单包拉取,等待全部到齐)。 */
static size_t exampleDrainCount(xnetudp* pUdp, size_t iWant)
{
	size_t iGot = 0;
	double iDeadline = exampleTimerLimit(3000);

	while ( iGot < iWant ) {
		xnetudppacket* pPacket = xrtNetUdpReceive(pUdp);

		if ( pPacket != NULL ) {
			xrtNetUdpPacketDestroy(pPacket);
			++iGot;
			iDeadline = exampleTimerLimit(3000);
			continue;
		}
		if ( exampleTimerExpired(iDeadline) ) {
			break;
		}
		xrtThreadYield();
	}
	return iGot;
}



/* 服务端绑小接收槽演示截断;客户端连发演示三种批量接收。 */
int main(void)
{
	xnetengineconfig EngineConfig;
	xnetudpconfig UdpConfig;
	xnetengine* pEngine = NULL;
	xnetudp* pServer = NULL;
	xnetudp* pClient = NULL;
	xnetaddr Address;
	xnetudppacket* pPackets[8];
	xnetudppacket* pKept = NULL;
	xnetudpbatch* pBatch = NULL;
	xnetudpbatch* pBatchRef = NULL;
	xfuture* pFuture = NULL;
	xfuture* pWritable = NULL;
	xfuture* pErrorFuture = NULL;
	char Big[64];
	size_t iCount;
	size_t i;
	int iResult = 1;

	xrtNetEngineConfigInit(&EngineConfig);
	EngineConfig.Workers = 1;
	xrtNetUdpConfigInit(&UdpConfig);
	UdpConfig.ReceiveSize = 16u;  /* 对端发 64 字节 → 截断前缀 */
	UdpConfig.Truncation = XNET_UDP_TRUNCATE_DELIVER;
	pEngine = xrtNetEngineCreate(&EngineConfig);
	if ( (pEngine == NULL) || !xrtNetEngineStart(pEngine) ) {
		goto Cleanup;
	}
	(void)xrtNetAddrLoopback(&Address, XNET_FAMILY_IPV4, 0);
	pServer = xrtNetUdpBind(pEngine, &Address, 0, &UdpConfig,
		NULL, NULL);
	if ( (pServer == NULL) || !xrtNetUdpLocal(pServer, &Address) ) {
		iResult = 2;
		goto Cleanup;
	}
	pClient = xrtNetUdpConnect(pEngine, &Address, 0, NULL,
		NULL, NULL);
	if ( (pClient == NULL) ||
		 !exampleWaitState(pServer, XNET_UDP_OPEN) ||
		 !exampleWaitState(pClient, XNET_UDP_OPEN) ) {
		iResult = 3;
		goto Cleanup;
	}

	/* 截断演示:64 字节载荷到达 16 字节接收槽。 */
	memset(Big, 'A', sizeof(Big));
	if ( xrtNetUdpSend(pClient, Big, sizeof(Big)) != XNET_RESULT_OK ) {
		goto Cleanup;
	}
	iCount = exampleDrainCount(pServer, 1);
	if ( iCount != 1u ) {
		iResult = 4;
		goto Cleanup;
	}
	{
		/* 重发一条并直接取包核对截断标志与前缀长度。 */
		xnetudppacket* pPacket = NULL;
		double iDeadline = exampleTimerLimit(3000);

		if ( xrtNetUdpSend(pClient, Big, sizeof(Big)) !=
			 XNET_RESULT_OK ) {
			goto Cleanup;
		}
		while ( (pPacket = xrtNetUdpReceive(pServer)) == NULL ) {
			if ( exampleTimerExpired(iDeadline) ) {
				break;
			}
			xrtThreadYield();
		}
		if ( (pPacket == NULL) || !xrtNetUdpPacketTruncated(pPacket) ||
			 (xrtNetUdpPacketSize(pPacket) != 16u) ) {
			xrtNetUdpPacketDestroy(pPacket);
			iResult = 5;
			goto Cleanup;
		}
		printf("batch: truncated prefix=%zu of %zu\n",
			xrtNetUdpPacketSize(pPacket), sizeof(Big));
		xrtNetUdpPacketDestroy(pPacket);
	}

	/* 拉取批量:三条齐发,等队列到齐后一次 ReceiveBatch 全取。 */
	for ( i = 0; i < 3u; ++i ) {
		if ( xrtNetUdpSend(pClient, "bb", 2) != XNET_RESULT_OK ) {
			goto Cleanup;
		}
	}
	memset(pPackets, 0, sizeof(pPackets));
	{
		double iDeadline = exampleTimerLimit(3000);

		while ( xrtNetUdpQueued(pServer) < 3u ) {
			if ( exampleTimerExpired(iDeadline) ) {
				iResult = 6;
				goto Cleanup;
			}
			xrtThreadYield();
		}
		iCount = xrtNetUdpReceiveBatch(pServer, pPackets, 8);
		if ( iCount != 3u ) {
			iResult = 6;
			goto Cleanup;
		}
	}
	printf("pull batches: %zu packets in 1 batch\n", iCount);
	for ( i = 0; i < iCount; ++i ) {
		xrtNetUdpPacketDestroy(pPackets[i]);
	}

	/* 阻塞批量:两条齐发,BatchPacket 借用 + BatchTake 转移。 */
	for ( i = 0; i < 2u; ++i ) {
		if ( xrtNetUdpSend(pClient, "ww", 2) != XNET_RESULT_OK ) {
			goto Cleanup;
		}
	}
	{
		double iDeadline = exampleTimerLimit(3000);

		while ( xrtNetUdpQueued(pServer) < 2u ) {
			if ( exampleTimerExpired(iDeadline) ) {
				iResult = 7;
				goto Cleanup;
			}
			xrtThreadYield();
		}
	}
	pBatch = xrtNetUdpReceiveBatchWait(pServer, 4,3000, NULL);
	if ( (pBatch == NULL) || (xrtNetUdpBatchCount(pBatch) < 1u) ) {
		iResult = 7;
		goto Cleanup;
	}
	{
		xnetudppacket* pBorrowed = xrtNetUdpBatchPacket(pBatch, 0);
		xnetudppacket* pTaken = xrtNetUdpBatchTake(pBatch,
			xrtNetUdpBatchCount(pBatch) - 1u);

		if ( (pBorrowed == NULL) || (pTaken == NULL) ||
			 (xrtNetUdpPacketSize(pBorrowed) != 2u) ||
			 (xrtNetUdpBatchPacket(pBatch,
				xrtNetUdpBatchCount(pBatch) - 1u) != NULL) ) {
			xrtNetUdpPacketDestroy(pTaken);
			iResult = 8;
			goto Cleanup;
		}
		printf("wait batch: %zu packets (borrow=take=ok)\n",
			xrtNetUdpBatchCount(pBatch));
		xrtNetUdpPacketDestroy(pTaken);
	}
	xrtNetUdpBatchDestroy(pBatch);
	pBatch = NULL;

	/* Future 批量:两条齐发 + 双引用把包与批量带出生命周期。 */
	for ( i = 0; i < 2u; ++i ) {
		if ( xrtNetUdpSend(pClient, "ff", 2) != XNET_RESULT_OK ) {
			goto Cleanup;
		}
	}
	{
		double iDeadline = exampleTimerLimit(3000);

		while ( xrtNetUdpQueued(pServer) < 2u ) {
			if ( exampleTimerExpired(iDeadline) ) {
				iResult = 9;
				goto Cleanup;
			}
			xrtThreadYield();
		}
	}
	pFuture = xrtNetUdpReceiveBatchAsync(pServer, 4);
	if ( !exampleFutureWait(pFuture) ) {
		iResult = 9;
		goto Cleanup;
	}
	pBatch = (xnetudpbatch*)xrtFutureValue(pFuture);
	if ( (pBatch == NULL) || (xrtNetUdpBatchCount(pBatch) < 1u) ) {
		iResult = 10;
		goto Cleanup;
	}
	
	 * 必须自己 Ref(包与整批各一份),不能替 Future 释放。 */
	pKept = xrtNetUdpPacketRef(xrtNetUdpBatchPacket(pBatch, 0));
	pBatchRef = xrtNetUdpBatchRef(pBatch);
	if ( (pKept == NULL) || (pBatchRef == NULL) ) {
		iResult = 11;
		goto Cleanup;
	}
	printf("future batch: %zu packets, ref kept size=%zu\n",
		xrtNetUdpBatchCount(pBatch),
		xrtNetUdpPacketSize(pKept));
	/* 先销毁 Future(释放其引用):两份 Ref 让包与批量继续存活。 */
	xrtFutureDestroy(pFuture);
	pFuture = NULL;
	if ( (xrtNetUdpBatchCount(pBatchRef) < 1u) ||
		 (xrtNetUdpPacketSize(pKept) != 2u) ) {
		iResult = 12;
		goto Cleanup;
	}
	xrtNetUdpPacketDestroy(pKept);
	pKept = NULL;
	xrtNetUdpBatchDestroy(pBatchRef);
	pBatchRef = NULL;
	pBatch = NULL;

	/* 错误队列空路径:拉取 NULL/0 + Future 由关闭终结。 */
	{
		xnetudperrorpacket* pErrors[4];

		printf("error queue: empty pull=%zu/%zu",
			(size_t)(xrtNetUdpReceiveError(pServer) != NULL ?
				1u : 0u),
			xrtNetUdpReceiveErrorBatch(pServer, pErrors, 4));
	}
	pErrorFuture = xrtNetUdpReceiveErrorAsync(pServer);
	(void)xrtNetUdpAbort(pServer);
	if ( !exampleWaitState(pServer, XNET_UDP_CLOSED) ) {
		iResult = 13;
		goto Cleanup;
	}
	/* 关闭使未决错误 Future 进入终态(RESOLVED=1..CLOSED=4)。 */
	(void)xrtFutureWaitFor(pErrorFuture, INT64_C(3000));
	printf(", async terminal=%d\n",
		(int)xrtFutureState(pErrorFuture));

	/* 发送条件:空闲队列立即可写(同步 + Future 双形态)。 */
	printf("writable: sync=%d",
		xrtNetUdpWritable(pClient, 64,3000,
			NULL) ? 1 : 0);
	pWritable = xrtNetUdpWritableAsync(pClient, 64);
	printf(" async=%d\n", exampleFutureWait(pWritable) ? 1 : 0);
	iResult = 0;

Cleanup:
	xrtFutureDestroy(pWritable);
	xrtFutureDestroy(pErrorFuture);
	xrtFutureDestroy(pFuture);
	xrtNetUdpBatchDestroy(pBatch);
	xrtNetUdpBatchDestroy(pBatchRef);
	xrtNetUdpPacketDestroy(pKept);
	if ( pClient != NULL ) {
		(void)xrtNetUdpAbort(pClient);
		(void)exampleWaitState(pClient, XNET_UDP_CLOSED);
	}
	if ( pServer != NULL ) {
		(void)xrtNetUdpAbort(pServer);
		(void)exampleWaitState(pServer, XNET_UDP_CLOSED);
	}
	xrtNetUdpDestroy(pClient);
	xrtNetUdpDestroy(pServer);
	if ( (pEngine != NULL) && !xrtNetEngineDestroy(pEngine) &&
		 (iResult == 0) ) {
		iResult = 14;
	}
	return iResult;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/network/udp_batch/main.c -lws2_32 -liphlpapibatch: truncated prefix=16 of 64pull batches: 3 packets in 1 batchwait batch: 2 packets (borrow=take=ok)future batch: 2 packets, ref kept size=2error queue: empty pull=0/0, async terminal=4writable: sync=1 async=1

Что только что произошло. (1) truncated prefix=16 of 64 — семантика усечённого префикса (малый буфер принимает большую датаграмму: усечение до вместимого, размер датаграммы запрашиваем — форма усечения внутри границы UDP). (2) pull batches: 3 packets in 1 batch — пакетное подтягивание: три датаграммы одним вызовом (доказательство амортизации разгрома завала за один колбэк). (3) Строки wait/future: блокирующее ожидание и форма Future делают по одному пакетному приёму (borrow=take=ok — оба вида владения при извлечении буферов датаграмм проверены). (4) Строка error queue: пустое вытягивание и асинхронные финалы очереди ошибок — семейство RecvError, протестированное на UDP. (5) Строка writable: синхронный и асинхронный пробы отправляемости. Пример multicast добавляет мультикаст-позицию (join → send на адрес группы → приём → leave); примеры sync/future — сестринские блокирующая/Future формы — четыре формы выровнены с семейством TCP.

69.4契约

  • Границы данных: одна датаграмма — один буфер, без цепочки и бюджета — фундаментальное отличие от TCP; границы не теряются, и порядок тоже (порядок и не обещался).
  • Двойственная форма: безсвязный конец (любой источник + sendto ответ) / соединённая (connect-фильтр + фиксированный адресат) — по роли.
  • Асинхронный жизненный цикл: управление переходными состояниями bind/connect (опрос State или ожидание событий + deadline).
  • Та же грань колбэков: события Read/Write/Close; вытягивающий приём/передача; ограничения Worker и Post как у Stream.
  • Мультикаст: join/leave + TTL; приём/передача как юникаст. Семейство батчинга амортизирует высокий PPS.
  • Асинхронные ошибки: ошибки класса ICMP/PMTU гейтятся по возможностям платформы (RecvError) — обратная связь «недостижимо» ненадёжных датаграмм.

От примеров к инженерии: три хозяина UDP

Класс обнаружения и регистрации (дом мультикаста): обнаружение сервисов (испытательная версия примера multicast), раздача конфигурации, широковещательные сердцебиения — малые датаграммы + мультикаст + безсвязная дешевизна; TTL мультикаста управляет охватом (TTL=1 ограничивает локальным сегментом).Класс потоков реального времени (ненадёжность как свойство): аудио/видео, игры, телеметрия — отправка фиксированным ритмом, без ретрансляции потерь (перепосылка устаревших данных бессмысленна), seq-номера для обнаружения переупорядочивания. Класс запрос-ответ (надёжность строится прикладным слоем): DNS-запросы, простой RPC — тайм-аут + повтор + спаривание ID запроса (стандартный слой продвинутого упражнения). Общий водораздел трёх хозяев — цена потери пакета: обнаружению всё равно, реальному времени ретрансляция запрещена, запрос-ответ обязан повторять — стройте прикладной слой по цене, а не по умолчанию «всё через TCP» или голый UDP наугад.

Место UDP в эпоху QUIC

Место UDP в современном сетевом стеке заслуживает главо-уровневого обновления картины: QUIC/HTTP3 полностью работают на UDP — надёжный транспорт, управление потоком и шифрование перестроены на прикладном слое (над UDP). Почему: окостенение TCP в ядре (переделки посредниками-миддлбоксами, медленные апгрейды, блокировка головы строки) вытолкнула инновации вниз, на носитель UDP. Модуль UDP XRT поставляет для этого engine-форму фундамента (грань колбэков/батчинг/мультикаст/PMTU) — сырьё для протоколов класса QUIC. Он дополняет собственный стек TCP XRT (engine-обёртку над TCP ядра): стандартный веб — TCP (глава-золотой стандарт), собственные протоколы и малые задержки стартуют с сырья UDP. Современный ответ на «TCP или UDP»: потребность в стандартных протоколах — TCP; управляемая эволюция протокола — сырьё UDP; уже не старая дихотомия «надёжный против быстрого».

Наблюдение и безопасность: два слабых места UDP

Слабые места наблюдения UDP-сервиса надо укреплять проактивно (конвейер главы 50). Потери невидимы: у UDP нет ретрансляции — сколько потеряно, знает только прикладной слой (анализ пробелов seq — статистика непрерывности seq в журнал); плоскость безопасности: UDP легко подделывает адрес источника (нет рукопожатия) — точки обороны против усилительных отражающих атак (классика — усиление DNS): ограничение размера ответа (на запрос 64 байта не возвращать 4000), проверка адресата (фильтр соединённой формы), ограничение скорости. Пройдите оба слабых места до выхода сервиса в свет — «простота» UDP — простота API; эксплуатационная и безопасностная работа только прибавляется.

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

Ловушка 1: читать UDP connect как TCP connect:Ловушка 1: читать UDP connect как TCP connect

Симптомы: вера «connect успешен = пир достижим» — на деле connect успешен, даже когда пира нет (UDP connect ставит только фильтр); или вера в надёжность после connect — потери как были, так и есть.

Причина: UDP connect — локальная настройка фильтра — без рукопожатия, без подтверждений, без изменения ненадёжной природы.

错误写法
if ( xrtNetUdpConnect(Udp, &Peer) ) {
	AssumeReachable(Peer);   /* неверно — UDP connect не проверяет существование пира */
}
正确写法
xrtNetUdpConnect(Udp, &Peer);   /* ставит фильтр + адресата по умолчанию — локальная операция */
SendRequest(Udp);
if ( !WaitResponse(Udp, Deadline) ) {   /* достижимость проверяется прикладным запрос-ответом */
	RetryOrFail();
}
Ловушка 2: размер датаграммы сверх MTU пути:Ловушка 2: размер датаграммы сверх MTU пути

Симптомы: на локальной петле нормально, между сегментами теряется — большие датаграммы отбрасываются (фрагментация запрещена или у промежуточного маршрута MTU меньше); редкие частичные приходы (частичная потеря фрагментов).

Причина: датаграмма UDP превышает MTU пути — фрагментация ядром (частичная потеря = потеря всей) или PMTU-дыра (бит DF + ICMP отфильтрован).

错误写法
char Big[65000];
Fill(Big);
xrtNetUdpSend(Udp, Big, sizeof(Big));   /* сверх MTU — между сегментами почти наверняка потеря целиком */
正确写法
/* прикладное ограничение в безопасном размере (обычно ниже 1200-1400 байт); большие сообщения — TCP или прикладная нарезка */
char Packet[1200];
xrtNetUdpSend(Udp, Packet, sizeof(Packet));

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

Базовое: воспроизведение двойственного контура

Воспроизведите главный пример; затем добавьте «второго клиента», шлющего на сервер — убедитесь: безсвязный конец принимает от любого источника, соединённая форма — только от запертого пира (пакеты второго клиента фильтруются).

Продвинутое: тайм-аут-повтор запрос-ответ

Стандартный слой надёжности UDP-клиента: отправить запрос → ждать с дедлайном → по тайм-ауту повторить (экспоненциальный откат) → после N неудач доложить; сервер имитирует потери (случайно отбрасывая 50%) для проверки сходимости повторов. Подсказка: семейство deadline + отмена главы 54.

Вызов: служба обнаружения через мультикаст

Обнаружение сервисов в локальной сети: серверы вступают в группу и периодически вещают о себе; клиенты вступают в группу и принимают сообщения обнаружения; подтверждение — юникастное соединение. Критерий приёмки: при конкурентном вещании многих серверов клиент перечисляет всех; join/leave чисто (без утечек); контроль вещательного шторма (интервал + TTL); вся цепочка от обнаружения до соединения в журнале и аудируема.

Итоги

ТемаКраткая справка
Границы данныходна датаграмма — один буфер; границу хранит ядро; без цепочки и бюджета
Двойственная формабезсвязный конец (любой источник + sendto) / соединённая (connect-фильтр + фиксированный адресат)
Жизненный циклмашина состояний асинхронного bind/connect + опрос с deadline / ожидание событий
Грань колбэковRead/Write/Close как у Stream; вытягивающий приём/передача; те же ограничения Worker
Семейство батчинганесколько датаграмм за раз — амортизация высокого PPS; семейство Msg с векторами адресов
Мультикастjoin/leave + TTL; приём/передача как юникаст
MTUдатаграммы в безопасном размере (~1200); ошибки PMTU гейтятся по возможностям

📖 Справочники API в этой главе:udp (87 записей)net (247 записей)

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