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

DNS превращает доменные имена в списки адресов (глава 63 уже встретила синхронный xrtNetResolve). Эта глава разворачивает сродную движку асинхронную форму: объект операции xnetresolveop подаётся в движок — колбэк исполняется на Worker (тот же поток, что и последующие операции соединения — естественно без блокировок; в этом весь смысл «сродства с движком»); результат — всё тот же список адресов (вход happy-eyeballs не меняется); тайм-аут и отмена управляются через объект операции. Кэш и стратегия резолва (hosts-приоритет, TTL) согласованы на уровне контрактов модуля.

66.1引入

Ловушка синхронного резолва — вне стартового пути: резолв на каждый запрос (сценарии прямого соединения) блокирует поток запроса на миллисекунды или секунды (медленный DNS — самый частый источник хвостовых задержек сетевого сервиса); после резолва идёт соединение — и сшивка двух асинхронных шагов (инициирование соединения в колбэке резолва) в обычной модели пула потоков требует межпоточной передачи данных (резолв на потоке пула A, соединение нужно на потоке движка B — блокировки или очереди).

Ответ сродного движка: операция резолва подаётся в движок — колбэк завершения работает на Worker, в том же потоке, что соединения и приём/передача — «разрешил — сразу connect», ноль передач, ноль блокировок. Правильное заполнение «слота DNS-выноса» скелета главы 61: не вынос в отдельный пул потоков, а подача в движок (вынос вычислений в пул задач главы 59 против резолва внутри движка этой главы — критерий выбора: «где живут последующие операции»).

66.2概念

Разделение синхронного и асинхронного

ФормаВходПодходит
СинхроннаяxrtNetResolveРазбор стартовой конфигурации, инструменты
Асинхронная (движок)resolveop подаётся в движокРезолв на запрос, сшивка с сервисными соединениями

Объект асинхронной операции xnetresolveop: подача (движок + домен + порт + колбэк + данные) → завершение (колбэк на Worker, xrtNetResolveOpResult берёт список). Дисциплина колбэка по всей библиотеке (лёгкая работа — результаты резолва обычно сразу потребляются/передаются).

Смысл сродства с движком

1Подачаresolveop -> очередь движка (DNS исполняется асинхронно на системном пути резолва)
2Завершениеколбэк исполняется в потоке Worker движка
3Сшивкав колбэке сразу Connect (тот же поток — ноль передач, ноль блокировок)
4Контрастрезолв в отдельном пуле -> результат межпоточно в движок -> блокировки или очередь (цена передачи)

Суть «сродства»: место исполнения последующих операций решает, где исполняется резолв — чтобы соединять соединения, которыми управляет движок, отдайте резолв тому же движку. Логика та же, что у «привязки потока планировщика» главы 56: данные и операции в одном потоке — и цена синхронизации исчезает.

Тайм-аут, отмена и кэш

Асинхронные операции несут тайм-ауты (семейство параметров deadline — соглашения главы 42) и отмену (объект операции отменяем — DNS-диалект дерева отмен главы 54). Семантика кэша — в контракте модуля: результаты резолва решает системный путь (hosts-приоритет, системный кэш) — XRT не делает прикладного DNS-кэша (наблюдение главы 59: вверх по течению кэш уже есть; повторный — сложность, а не выгода; если прикладной кэш действительно нужен — стройте дерево значений + TTL — хозяин кэша главы 31). Пример resolver_future — сестра в форме Future (модель доставки главы 58, надетая на резолв).

Разделение семейства примеров resolver

dns (синхронный список, глава 63), resolver (колбэк движка, главный пример этой главы), resolver_future (форма Future), resolve_tour (тур по всем интерфейсам) — четыре примера, четыре лица одной возможности. Выбор: инструменты — синхронно, сервисы — колбэк движка, композиция (многодоменная конкурентность + First) — Future.

66.3示例

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

Из примера репозитория examples/network/resolver/main.c — колбэк получает список адресов на Worker:

examples/network/resolver/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 exampleresolver {
	xatomic32 Done;
} exampleresolver;



/* 输出异步查询得到的全部地址。 */
static void exampleResolverDone(xnetresolveop* pOperation, ptr pData)
{
	exampleresolver* pState = (exampleresolver*)pData;
	xnetaddrlist* pAddresses = xrtNetResolveOpResult(pOperation);

	if ( pAddresses != NULL ) {
		for ( size_t i = 0; i < xrtNetAddrListCount(pAddresses); i++ ) {
			str sAddress = xrtNetAddrString(
				xrtNetAddrListGet(pAddresses, i)
			);

			if ( sAddress != NULL ) {
				printf("%s\n", sAddress);
				xrtFree(sAddress);
			}
		}
		xrtNetAddrListDestroy(pAddresses);
	} else {
		const xerror* pError = xrtNetResolveOpError(pOperation);

		fprintf(stderr, "%s\n", pError != NULL ?
			xrtErrorMessage(pError) : "resolve failed");
	}
	(void)xrtAtomic32FetchAdd(&pState->Done, 1, XMEMORY_RELEASE);
}



/* 使用独立 Resolver 异步查询并等待一次完成回调。 */
int main(void)
{
	exampleresolver State;
	xnetresolver* pResolver;
	xnetresolveop* pOperation;
	double iDeadline;

	memset(&State, 0, sizeof(State));
	pResolver = xrtNetResolverCreate(NULL);
	if ( pResolver == NULL ) {
		return 1;
	}
	pOperation = xrtNetResolverResolve(
		pResolver,
		"localhost",
		XNET_FAMILY_UNSPEC,
		exampleResolverDone,
		&State
	);
	if ( pOperation == NULL ) {
		(void)xrtNetResolverDestroy(pResolver);
		return 2;
	}
	iDeadline = exampleTimerLimit(5000);
	while ( xrtAtomic32Load(&State.Done, XMEMORY_ACQUIRE) == 0 ) {
		if ( exampleTimerExpired(iDeadline) ) {
			(void)xrtNetResolveOpCancel(pOperation);
			break;
		}
		xrtThreadYield();
	}
	xrtNetResolveOpDestroy(pOperation);
	return xrtNetResolverDestroy(pResolver) ? 0 : 3;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/network/resolver/main.c -lws2_32 -liphlpapi::1127.0.0.1

Что только что произошло. (1) Операция резолва подана в движок (домен localhost + порт + колбэк завершения + данные состояния) — главный поток не блокируется. (2) Колбэк завершения исполняется на Worker: xrtNetResolveOpResult берёт список адресов и печатает построчно — ::1 и 127.0.0.1 (порядок может смениться — двухстековая списочная семантика идентична синхронной версии). (3) Вывод в колбэке прямо через printf — он работает в обычном контексте потока (не ограниченная среда вроде обработчика сигнала — контраст главы 49). (4) Главный поток ждёт атомарный счётчик (флаг Done) для подтверждения завершения — простейшее рандеву; в сервисной форме колбэк сразу Connect (ноль передач).

Полная программа: форма Future

Из examples/network/resolver_future/main.c — модель доставки главы 58, надетая на резолв:

examples/network/resolver_future/main.c源码 ↗
#include <stdio.h>
#include <xrt.h>


















 */


/* 使用 Future 等待异步 DNS,并读取 Future 持有的完整地址列表。 */
int main(void)
{
	xnetresolver* pResolver = xrtNetResolverCreate(NULL);
	xfuture* pFuture;
	xnetaddrlist* pAddresses;

	if ( pResolver == NULL ) {
		return 1;
	}
	pFuture = xrtNetResolveAsync(
		pResolver,
		"localhost",
		XNET_FAMILY_UNSPEC
	);
	if ( (pFuture == NULL) ||
		 (xrtFutureWaitFor(pFuture, 5000) != XWAIT_OK) ) {
		xrtFutureDestroy(pFuture);
		(void)xrtNetResolverDestroy(pResolver);
		return 2;
	}
	pAddresses = (xnetaddrlist*)xrtFutureValue(pFuture);
	if ( pAddresses == NULL ) {
		const xerror* pError = xrtFutureError(pFuture);

		fprintf(stderr, "%s\n", pError != NULL ?
			xrtErrorMessage(pError) : "resolve failed");
	} else {
		for ( size_t i = 0; i < xrtNetAddrListCount(pAddresses); i++ ) {
			str sAddress = xrtNetAddrString(
				xrtNetAddrListGet(pAddresses, i)
			);

			if ( sAddress != NULL ) {
				printf("%s\n", sAddress);
				xrtFree(sAddress);
			}
		}
	}
	xrtFutureDestroy(pFuture);
	return xrtNetResolverDestroy(pResolver) ? 0 : 3;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/network/resolver_future/main.c -lws2_32 -liphlpapi::1127.0.0.1

Что только что произошло. (1) Подача резолва возвращает Future — результат (список адресов) доставляется через Future. (2) Wait/комбинаторы/продолжения (полный набор главы 58) применимы напрямую: многодоменный конкурентный резолв + First (какой домен разрешился первым — тот и работает), гонка с тайм-аутом (резолв против таймера) — форма Future разблокирует композицию. (3) Колбэковая форма примера resolver подходит прямолинейному потоку «разрешил — сразу connect»; форма Future подходит случаю «резолв — звено большей оркестрации» — выбирайте по потребностям оркестрации.

66.4契约

  • Сродство с движком: колбэки резолва на Worker — тот же поток, что последующие соединения, ноль передач и ноль блокировок; если продолжение на движке — подайте резолв в движок.
  • Списочная семантика: асинхронные результаты тоже списки адресов — happy-eyeballs пробует по порядку (контракт главы 63 продолжается).
  • Лёгкие колбэки: колбэки резолва потребляют/передают результаты сразу — тяжёлая обработка переезжает на следующее звено.
  • Тайм-аут и отмена: объекты операций несут семейство deadline и отмену — диалект главы 54.
  • Граница кэша: XRT не делает прикладного DNS-кэша; при действительной нужде стройте сами (дерево значений + TTL — хозяин кэша главы 31).
  • Выбор формы: инструменты синхронно / сервисы колбэк движка / оркестрация Future — четыре примера, четыре лица.

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

Стартовая конфигурация: сервис при старте читает из конфига домены верхнего уровня — один синхронный резолв, таблица адресов в хранилище (дерево значений главы 31); синхронность стартового пути приемлема (однократно, с защитой тайм-аутом).Прогрев пула соединений: на старте или периодически асинхронно резолвить домены верхнего уровня + пробные соединения — прогрев снимает DNS-хвост первой просьбы; испытательное упражнение — ровно это. Сценарии прямого соединения на каждый запрос (прокси-пересылка, краулеры): у каждого запроса свой домен — резолв обязан быть асинхронным и сродным движку (сцена принуждения ловушки 1); в колбэке резолва прямое соединение (цикл happy-eyeballs разворачивается в колбэке). Три хозяина выбираются по «отношению частоты резолва к пути запроса» — однократная конфигурация при старте синхронно, периодический прогрев асинхронно в движке, на каждый запрос — сродный движку асинхронный.

Полная позиция реализации happy-eyeballs

Глава 63 установила семантику «пробовать по порядку»; эта глава добавляет её полную позицию реализации как асинхронной оркестрации: резолв завершён (список в руках) → конкурентно инициируется предпочтительное соединение (обычно v6) + таймер (~250 мс) → если таймер сработал первым, конкурентно запускается второй элемент (v4) → любое успешное отменяет остальные. Это прямое применение комбинаторов Future (First + гонка тайм-аута главы 58) — форма resolver_future существует ради этого. Принимает ли вход Connect в XRT список адресов? Нет — один адрес; цикл разворачивает вызывающий (по одному Connect на движок) — и это разворачивание и есть слой «стратегии группировки сети» (библиотека расширений xhttp несёт полную реализацию — том 10).

Пост наблюдения: задержка и доля неудач резолва

Резолв — первый прыжок сетевого сервиса, и наблюдение (конвейер главы 50) занимает первый пост тома 7: длительность резолва (интервал подачи-до-колбэка — главный источник хвостовых задержек); доля неудач и классификация ошибок (NXDOMAIN против тайм-аута против системной ошибки — обработка разная: ошибка домена — проблема конфигурации, тайм-аут — проблема сети); распределение размера списка (запас отката многодоменных адресов). Три метрики — в структурированный журнал: проблемы DNS (самый частый и самый скрытый источник сбоев сервиса) из «кажется медленным» превращаются в видимые кривые. Статистическая схема примера resolve_tour — образец снятия этих трёх метрик.

Развязка одного исторического узла: загадка потокобезопасности getaddrinfo

У DNS традиционного C-программирования есть знаменитая тёмная яма: поведение getaddrinfo непоследовательно между платформами (ранние glibc не потокобезопасны, некоторым платформам нужна особая инициализация) — редкие падения многопоточных сервисов на DNS были классикой. Слой резолва XRT обрабатывает эти платформенные различия внутри (обёртка системного пути резолва — пользователю нуль заметности). Ценность понимания изоморфна API сигналов главы 49: дисциплину ограниченной среды несёт модуль, а бизнес-код сохраняет нормальную форму. О пределах тоже надо знать: XRT не несёт собственного стека протокола DNS (идёт через системный резолвер) — сценарии, требующие DoH/кастомного резолва, реализуются на уровне xhttp/хоста (политика системного резолвера прозрачна и управляема для корпоративных сетей).

Уточнение отношений с главой 62

Разделение этой главы и главы 63 легко размывается — обе про «домен в адрес». Уточнено: глава 63 — глава модели адресов — тип xnetaddr и операции есть валюта тома, где xrtNetResolve (синхронный список) появляется попутно как удобный вход получения адресов; эта глава — глава стратегии резолва — асинхронная форма, сродство движку, тайм-аут/отмена, граница кэша, оркестрация happy-eyeballs. Одна глава говорит «что такое адрес», другая — «откуда адреса берутся» — противопоставление примера dns (глава 63) и примера resolver (эта глава) и есть противопоставление точек зрения двух глав. Последующие главы (соединения TCP, дозвон прокси) зависят только от типа адресов главы 63 — стратегия резолва этой главы используется на слое оркестрации соединений сервиса.

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

Ловушка 1: синхронный резолв на каждый запрос:Ловушка 1: синхронный резолв на каждый запрос

Симптомы: высокие хвостовые задержки запросов — P99 растягивается на сотни миллисекунд; редкий медленный DNS подвешивает весь запрос; пропускная способность дрожит вместе с DNS.

Причина: xrtNetResolve (синхронный) вошёл в путь запроса — каждый запрос блокируется на системном резолве (даже с системным кэшем есть переключения контекста и ожидание).

错误写法
void handle_request(void)
{
	xnetaddrlist* List = xrtNetResolve(Host, Port, XNET_FAMILY_UNSPEC);  /* синхронно — запрос блокируется */
	ConnectVia(List);
}
正确写法
/* Резолв на старте / прогреве пула соединений, или асинхронная форма в пути запроса */
SubmitResolveOp(Engine, Host, Port, onResolved, Data);   /* сродство движку — в колбэке сразу соединение */
Ловушка 2: блокировка при межпоточной передаче результатов резолва:Ловушка 2: блокировка при межпоточной передаче результатов резолва

Симптомы: замок между колбэком резолва и кодом соединения — пропускная способность затыкается на этом замке; редкие тупики (порядок замка конфликтует с другими замками движка).

Причина: резолв работает в отдельном пуле потоков (не сродный движку) — результат надо межпоточно передать в поток движка, и передача вносит замок или очередь.

错误写法
/* резолв в отдельном пуле */
xrtTaskSubmit(Pool, resolveAndHandoff, Host, NULL);
/* внутри resolveAndHandoff: Lock(gAddrMutex); сохранить список; Unlock; Post разбудить движок — цена передачи + замок */
正确写法
/* сродство движку — колбэк потребляет прямо на Worker, передача исчезает */
SubmitResolveOp(Engine, Host, Port, onResolved, Data);
/* onResolved работает на Worker: взять список, сразу Connect — тот же поток, ноль замков */

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

Базовое: сравнение двух форм

Один и тот же домен резолвится синхронно и колбэком движка — списки результатов совпадают; напечатайте исполняющий поток обеих версий (CurrentId), доказав, что колбэк на Worker.

Продвинутое: многодоменная конкурентность

Три домена конкурентно резолвятся движком (три объекта операций) — по завершении всех сводятся размеры списков; сравнение самый быстрый/самый медленный (таймер главы 42).

Вызов: линия прогрева пула соединений

Реализуйте линию прогрева при старте сервиса: N сконфигурированных доменов верхнего уровня резолвятся конкурентно → по каждому берётся первый адрес и инициируется пробное соединение → отчёт по таблице доступных верхних. Критерий приёмки: прогрев нигде не блокирует главный поток; частичные неудачи доменов не влияют на остальные (список неудач с цепочками причин в журнал); общий тайм-аут ограничивает всё (deadline); таблица доступных — в дерево значений (глава 31) для дальнейшего использования.

Итоги

ТемаКраткая справка
Принцип сродствапродолжение на движке → резолв подаётся в движок (колбэк в том же потоке, ноль замков)
Выбор формыинструменты синхронно / сервисы колбэк движка / оркестрация Future (четыре примера, четыре лица)
Списочная семантикаасинхронные результаты — тоже списки; контракт happy-eyeballs продолжается
Тайм-аут/отменаобъекты операций несут deadline и отмену — диалект главы 54
Граница кэшаXRT не добавляет прикладного кэша; при нужде — своё дерево значений + TTL
Хвостовые задержкирезолв на каждый запрос — только асинхронно; синхронный — источник хвостов номер один

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

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