Ориентация главы
Модуль 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概念
Двойственная форма и жизненный цикл
Асинхронные 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 — безсвязный конец + соединённая форма в одной сцене:
*/ #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; }
Что только что произошло. (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 (читать вместе):
*/ #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; }
Что только что произошло. (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; эксплуатационная и безопасностная работа только прибавляется.
Типичные ошибки
Симптомы: вера «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(); }
Симптомы: на локальной петле нормально, между сегментами теряется — большие датаграммы отбрасываются (фрагментация запрещена или у промежуточного маршрута 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 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки