本章导读

DNS 把域名变成地址列表(第 63 章已见同步版 xrtNetResolve)。本章展开引擎亲和的异步版:xnetresolveop 操作对象提交到引擎——回调在 Worker 上执行(与后续连接操作同线程——天然免锁,这是"引擎亲和"的全部含义);结果仍是地址列表(happy-eyeballs 的输入不变);超时与取消经操作对象控制。缓存与解析策略(hosts 优先、TTL)在模块契约层约定。

66.1引入

同步解析的坑在启动路径之外:每请求解析(直连场景)会阻塞请求线程数毫秒到数秒(DNS 慢是网络服务最常见的尾延迟源);解析完还要连接——两个异步步骤的衔接(解析回调里发起连接)在普通线程池模型下要跨线程交接数据(解析在池线程 A、连接要在引擎线程 B——加锁或队列)。

引擎亲和的答案:解析操作提交给引擎——完成回调跑在 Worker 上,与连接、收发同一线程——"解析完直接连"零交接零锁。第 61 章骨架里"DNS 外挂位"的正确填法:不是外挂到独立线程池,是提交给引擎(第 59 章任务池外挂计算 vs 本章引擎内解析——选择判据是"后续操作在哪")。

66.2概念

同步与异步的分工

形态入口适用
同步xrtNetResolve启动配置解析、工具类程序
异步(引擎)resolveop 提交引擎每请求解析、与服务连接衔接

异步的操作对象 xnetresolveop:提交(引擎+域名+端口+回调+数据)→ 完成(回调在 Worker、xrtNetResolveOpResult 取列表)。回调纪律照全库(轻活——解析结果通常立即消费/传递)。

引擎亲和的含义

1提交resolveop → 引擎排队(DNS 在系统解析路径异步执行)
2完成回调在引擎 Worker 线程执行
3衔接回调里直接 Connect(同线程——零交接零锁)
4对照独立线程池解析 → 结果跨线程给引擎 → 加锁或队列(有交接成本)

"亲和"的本质:后续操作的执行位置决定解析的执行位置——要连引擎管理的连接,解析就交给同一个引擎。这与第 56 章"调度器线程绑定"的逻辑同源:数据与操作归同一线程,同步成本消失。

超时、取消与缓存

异步操作带超时(deadline 参数族——第 42 章口径)与取消(操作对象可取消——第 54 章取消树的 DNS 方言)。缓存语义在模块契约:解析结果由系统解析器路径决定(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

刚才发生了什么。 ① 解析操作提交给引擎(域名 localhost + 端口 + 完成回调 + 状态数据)——主线程不阻塞。② 完成回调在 Worker 上执行:xrtNetResolveOpResult 取出地址列表、逐项打印——::1 与 127.0.0.1(顺序可能互换——双栈列表语义与同步版一致)。③ 回调里的输出直接用 printf——它跑在普通线程上下文(非信号处理函数那种受限环境——第 49 章对比)。④ 主线程等待原子计数(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

刚才发生了什么。 ① 解析提交返回 Future——解析结果(地址列表)经 Future 交付。② Wait/组合子/延续(第 58 章全套)直接可用:多域名并发解析 + First(哪个域名先解析成功用哪个)、超时竞争(解析 vs 定时器)——Future 形态解锁组合。③ resolver 范例的回调形态适合"解析完直接连"的直线流程;Future 形态适合"解析是更大编排的一环"——两种形态按编排需求选。

66.4契约

  • 引擎亲和:解析回调在 Worker 上——与后续连接同线程、零交接零锁;后续在引擎就提交引擎。
  • 列表语义:异步结果同样是地址列表——happy-eyeballs 按序尝试(第 63 章契约延续)。
  • 回调轻活:解析回调立即消费/传递结果——重处理移交下一环节。
  • 超时取消:操作对象带 deadline 族与取消——第 54 章方言。
  • 缓存边界:XRT 不做应用层 DNS 缓存;确需自建(值树+TTL——第 31 章缓存宿主)。
  • 形态选择:工具同步 / 服务引擎回调 / 编排 Future——四范例四张脸。

从示例到工程:解析的三个宿主

启动配置:服务启动读配置里的上游域名——同步解析一次、存地址表(第 31 章值树);启动路径的同步是可接受的(一次性、有超时保护)。连接池预热:启动/定期对上游域名异步解析+试探连接——预热消除首请求的 DNS 尾延迟;挑战练习就是它。每请求直连场景(代理转发、爬虫类):每请求域名不同——解析必须异步且引擎亲和(坑 1 的执法场景);解析回调里直连(happy-eyeballs 循环在回调里展开)。三宿主按"解析频率与请求路径的关系"选择——一次配置启动同步、周期预热引擎异步、每请求引擎亲和异步。

happy-eyeballs 的完整实现位

第 63 章立了"按序尝试"的语义,本章补上它作为异步编排的完整实现位:解析完成(列表到手)→ 并发发起首选项连接(通常 v6)+ 定时器(约 250ms)→ 定时器先到则并发第二项(v4)→ 任一成功取消其余。这是 Future 组合子(第 58 章 First+超时竞争)的直接应用——resolver_future 形态为此存在。XRT 的 Connect 入口接受地址列表吗——不,接受单地址;循环由调用方展开(每个引擎的 Connect 一次)——这个展开正是"组网策略"层( xhttp 扩展库内置了完整实现——卷十)。

观测位:解析的延迟与失败率

解析是网络服务的第一跳,观测(第 50 章管线)在卷七的第一个哨位:解析耗时(提交到回调的间隔——尾延迟主源);失败率与错误分类(NXDOMAIN vs 超时 vs 系统错误——处置不同:域名错是配置问题、超时是网络问题);列表大小分布(多地址域名的回退余量)。三个指标进结构化日志——DNS 问题(最常见又最隐蔽的服务故障源)从"感觉慢"变成曲线可见。resolve_tour 范例的统计口径是这三个指标的取数示范。

一个历史包袱的化解:getaddrinfo 的线程安全之谜

传统 C 网络编程的 DNS 有个著名暗坑:getaddrinfo 的跨平台行为不一致(glibc 早期版本非线程安全、部分平台需要特殊初始化)——多线程服务的 DNS 偶发崩溃曾是经典 bug。XRT 的解析层内部处理了这些平台差异(系统解析路径的封装——使用者零感知)。理解这一点的价值与第 49 章信号 API 同构:受限环境的纪律由模块承担,业务代码保持正常形状。同时它的局限也要知道:XRT 不自带 DNS 协议栈(走系统解析器)——需要 DoH/自定义解析的场景在 xhttp/宿主层实现(系统解析器的策略对企业网络透明可控)。

与第 63 章的关系厘清

本章与第 63 章的分工容易模糊——两章都讲"域名到地址"。厘清:第 63 章是地址模型章——xnetaddr 类型与操作是全卷货币,其中 xrtNetResolve(同步列表)作为地址获取的便捷入口顺带出现;本章是解析策略章——异步形态、引擎亲和、超时取消、缓存边界、happy-eyeballs 编排。一章讲"地址是什么"、一章讲"地址怎么来"——dns 范例(第 63 章用)与 resolver 范例(本章)的对照就是两章视角的对照。后续章节(TCP 连接、代理拨号)只依赖第 63 章的地址类型——本章的解析策略在服务的连接编排层使用。

常见错误

坑 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:解析结果跨线程交接加锁

症状:解析回调与连接代码之间一把锁——吞吐卡在这把锁上;偶尔死锁(锁序与引擎其他锁冲突)。

原因:解析跑在独立线程池(非引擎亲和)——结果要跨线程给引擎线程,交接处引入锁或队列。

错误写法
/* 独立池解析 */
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 个条目)

点击查看完整签名、参数约束、返回值与错误说明