本章导读
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 取列表)。回调纪律照全库(轻活——解析结果通常立即消费/传递)。
引擎亲和的含义
"亲和"的本质:后续操作的执行位置决定解析的执行位置——要连引擎管理的连接,解析就交给同一个引擎。这与第 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 上拿地址列表:
*/ #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; }
刚才发生了什么。 ① 解析操作提交给引擎(域名 localhost + 端口 + 完成回调 + 状态数据)——主线程不阻塞。② 完成回调在 Worker 上执行:xrtNetResolveOpResult 取出地址列表、逐项打印——::1 与 127.0.0.1(顺序可能互换——双栈列表语义与同步版一致)。③ 回调里的输出直接用 printf——它跑在普通线程上下文(非信号处理函数那种受限环境——第 49 章对比)。④ 主线程等待原子计数(Done 标志)确认完成——最简汇合;服务形态下回调直接 Connect 衔接(零交接)。
完整程序:Future 形态
来自 examples/network/resolver_future/main.c——第 58 章交付模型套在解析上:
#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; }
刚才发生了什么。 ① 解析提交返回 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 章的地址类型——本章的解析策略在服务的连接编排层使用。
常见错误
症状:请求尾延迟高——P99 拉到数百毫秒;DNS 偶发慢时整个请求挂起;吞吐随 DNS 抖动。
原因:xrtNetResolve(同步)进了请求路径——每次请求阻塞等系统解析(即使有系统缓存也有上下文切换与等待)。
void handle_request(void) { xnetaddrlist* List = xrtNetResolve(Host, Port, XNET_FAMILY_UNSPEC); /* 同步——请求阻塞 */ ConnectVia(List); }
/* 启动/连接池预热时解析,或异步形态接入请求路径 */ SubmitResolveOp(Engine, Host, Port, onResolved, Data); /* 引擎亲和——回调里直接连 */
症状:解析回调与连接代码之间一把锁——吞吐卡在这把锁上;偶尔死锁(锁序与引擎其他锁冲突)。
原因:解析跑在独立线程池(非引擎亲和)——结果要跨线程给引擎线程,交接处引入锁或队列。
/* 独立池解析 */ 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 个条目)
点击查看完整签名、参数约束、返回值与错误说明