不能直接用getaddrinfo做异步解析,因为它是阻塞式系统调用,内核层面不可中断,且glibc 2.36+已废弃getaddrinfo_a;其默认ai_addrconfig在ipv6不可用时跳过aaaa记录,不支持自定义dns服务器与ttl缓存控制。

为什么不能直接用 getaddrinfo 做异步解析
getaddrinfo 是阻塞的,调用时线程会卡住,直到 DNS 响应返回或超时。在高并发服务(比如 HTTP 代理、连接池预热)里,这会导致线程数暴涨或响应延迟激增。POSIX 没有标准异步接口,Linux 的 getaddrinfo_a 已被标记为废弃,glibc 2.36+ 默认不编译它,且不支持自定义 DNS 服务器和 TTL 缓存控制。
用 libuv + uv_getaddrinfo 实现非阻塞解析
libuv 提供跨平台异步 DNS 解析,底层在 Linux/macOS 用线程池模拟异步,在 Windows 用 IOCP。它不暴露原始 DNS 报文,但足够满足大多数场景——比如解析 example.com 获取 IPv4/IPv6 地址列表。
关键点:
-
uv_getaddrinfo回调中必须检查status:负值表示失败(如UV_EAI_NONAME),不是所有错误都对应网络问题,也可能是本地/etc/hosts无匹配项 - 不要在回调里直接
freeaddrinfo;libuv 内部已管理内存,只需在回调末尾调用uv_freeaddrinfo - 超时需自行封装:libuv 不提供 DNS 超时参数,得用
uv_timer_t配合uv_cancel主动终止未完成的请求
uv_getaddrinfo_t req; uv_getaddrinfo(uv_default_loop(), &req, on_resolved, "example.com", nullptr, &hints);
手动实现 LRU 缓存需关注 TTL 和线程安全
DNS 记录带 TTL(单位秒),缓存不能简单“永久保存”。直接用 std::unordered_map 存 std::string → std::vector<sockaddr_storage></sockaddr_storage> 会出错:TTL 过期后仍返回旧地址,导致连接失败或绕过 CDN。
实操建议:
- 缓存条目结构至少包含:
std::vector<sockaddr_storage></sockaddr_storage>、uint64_t expire_at_ms(用uv_hrtime()计算)、int ai_family(区分 AF_INET/AF_INET6) - 读缓存前先比对
uv_hrtime()和expire_at_ms,过期则忽略并触发新解析 - 写缓存时用
std::shared_mutex(C++17):读多写少,避免std::mutex导致解析密集时锁争用 - 不缓存 NXDOMAIN(
UV_EAI_NONAME)结果:某些域名短暂不可达,缓存它会放大故障
绕过系统 DNS 配置:用 ldns 或 dns-cxx 发送原始 UDP 查询
当需要指定 DNS 服务器(如 8.8.8.8)、验证 DNSSEC、或解析非标准记录(TXT、SRV)时,必须跳过系统 resolver。推荐 ldns(C 库,C++ 可封装)而非自己拼 DNS 报文。
注意点:
-
ldns_resolver_new_frm_file("/etc/resolv.conf")会读系统配置,但可改用ldns_resolver_new手动设nameserver地址 - UDP 查询无重传逻辑,需自己实现:超时后重发(最多 2 次),并用随机化 ID + 端口避免冲突
- 收到响应后必须校验 ID、QR flag、RCODE,并检查是否截断(TC bit),截断时要降级走 TCP(
ldns_pkt_query不自动处理)
缓存这类原始响应时,TTL 从 DNS 报文的 RR 段读取,不是系统 getaddrinfo 返回的模糊值。
getaddrinfo 返回的地址可能来自 /etc/hosts(无 TTL)或 NSS 模块(TTL 不透明),而原始 DNS 查询能拿到真实 TTL 字段。如果业务对 DNS 更新敏感(比如灰度切流),这点差异就是故障根因。C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











