不能直接用 getaddrinfo 做异步 DNS 查询,因为它是阻塞的,会导致线程卡住;即使丢进线程池也属伪异步,无法取消、难集成事件循环、线程开销大。

为什么不能直接用 getaddrinfo 做异步 DNS 查询
因为标准 C++ 没有内置异步 DNS API,而 getaddrinfo 是阻塞的 —— 它会卡住当前线程直到系统 resolver 返回结果(可能几百毫秒甚至超时)。哪怕你把它丢进线程池,也属于“伪异步”:无法取消、无法与 epoll/kqueue 集成、线程开销大,且在高并发场景下容易耗尽线程资源。
用 libuv + uv_getaddrinfo 实现真正异步解析
libuv 是 Node.js 底层跨平台异步 I/O 库,它封装了各系统原生异步 DNS 机制(Linux 上用 getaddrinfo_a 或线程池模拟,macOS/iOS 用 dns_service_create_query,Windows 用 GetAddrInfoEx),对外提供统一回调接口。关键优势是:可取消、无额外线程、能和 TCP/UDP 句柄共用同一个事件循环。
实操要点:
-
uv_getaddrinfo的hints.ai_family设为AF_UNSPEC可同时请求 A 和 AAAA;设为AF_INET或AF_INET6则只查一种 - 回调函数中必须调用
uv_freeaddrinfo释放addrinfo链表,否则内存泄漏 - 错误码通过
status参数返回:UV_EAI_NONAME表示域名不存在,UV_EAI_FAIL表示 DNS 服务器拒绝或格式错误,不是网络超时 - 超时需自行用
uv_timer_t控制,uv_getaddrinfo本身不支持 timeout 参数
简短示例(忽略初始化和 loop run):
立即学习“C++免费学习笔记(深入)”;
void on_dns_resolve(uv_getaddrinfo_t* req, int status, struct addrinfo* res) {
if (status == 0) {
for (struct addrinfo* ai = res; ai; ai = ai->ai_next) {
if (ai->ai_family == AF_INET) {
char ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &((struct sockaddr_in*)ai->ai_addr)->sin_addr, ip, sizeof(ip));
printf("A: %s\n", ip);
} else if (ai->ai_family == AF_INET6) {
char ip[INET6_ADDRSTRLEN];
inet_ntop(AF_INET6, &((struct sockaddr_in6*)ai->ai_addr)->sin6_addr, ip, sizeof(ip));
printf("AAAA: %s\n", ip);
}
}
} else {
fprintf(stderr, "DNS resolve failed: %s\n", uv_strerror(status));
}
uv_freeaddrinfo(res); // 必须调用
delete req; // req 是 new 出来的,此处释放
}发起请求:
uv_getaddrinfo_t* req = new uv_getaddrinfo_t;
struct addrinfo hints = {};
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
int r = uv_getaddrinfo(loop, req, on_dns_resolve, "example.com", nullptr, &hints);
if (r != 0) {
fprintf(stderr, "uv_getaddrinfo error: %s\n", uv_strerror(r));
delete req;
}不用第三方库?std::thread + getaddrinfo 的安全封装要点
若项目禁止引入 libuv 或 Boost.Asio,只能用标准库,那必须自己管理线程+超时+取消。但注意:getaddrinfo 本身不可中断,所以“取消”实际是等待线程自然退出或超时后丢弃结果。
关键约束:
- 每个查询必须独占一个
std::thread,不能复用线程(getaddrinfo不是 reentrant 的) - 必须用
std::promise/std::future传递结果,避免裸指针生命周期问题 -
std::thread::joinable()要检查,防止重复join或忘记join/detach - Linux 下建议设置
resolv.conf中的options timeout:1 attempts:2,否则默认可能等 5 秒
常见失败场景和调试线索
DNS 异步请求失败,90% 不是代码逻辑问题,而是环境或配置导致:
- 查不到 AAAA 记录但 A 正常 → 检查本地 DNS 服务器是否支持 IPv6 解析(如
dig AAAA example.com @8.8.8.8) -
UV_EAI_AGAIN频繁出现 → 系统 resolver 负载高或 /etc/resolv.conf 配置了 unreachable nameserver - 同一域名反复解析变慢 → 检查是否未复用
uv_loop_t,每次新建 loop 会重载 resolver 配置 - macOS 上返回空结果但无错误 →
hints.ai_flags忘加AI_ADDRCONFIG,导致 IPv6 接口未启用时 AAAA 查询被静默跳过
真正难处理的是 resolver 层的中间状态:超时、截断、重试策略、EDNS0 支持与否 —— 这些都藏在系统库背后,异步封装层只能暴露结果和粗粒度错误码,细节日志得靠 tcpdump -i any port 53 抓包确认。


















