因为getaddrinfo是阻塞调用,内部同步发udp查询、重传并等待响应,全程占用线程栈,即使丢入线程池也属伪异步;真正异步dns必须绕过libc,自行构造dns查询包、用非阻塞socket发送、借助epoll等待响应。

为什么不能直接用 getaddrinfo 做异步?
因为 getaddrinfo 是阻塞调用,哪怕你把它丢进线程池,也属于“伪异步”——它内部会同步发 UDP 查询、重传、等待响应,全程占用线程栈。真正异步 DNS 必须绕过 libc 的封装,自己构造 DNS 查询包、用非阻塞 socket 发送、靠 epoll 或 kqueue 等待响应。Linux 上最轻量可行的路径是:用 socket(AF_INET, SOCK_DGRAM | SOCK_NONBLOCK) + epoll_wait + 手动解析 DNS 响应包。
如何构造一个合法的 DNS 查询包(IPv4/IPv6 通用)?
DNS 查询包结构固定:12 字节头部 + 问题部分。关键字段必须设对,否则权威服务器直接丢弃:
-
QR位清零(表示 query),RD位置 1(递归查询) -
QDCOUNT设为 1,ANCOUNT/NSCOUNT/ARCOUNT全为 0 - 域名要按 DNS 格式编码:每段长度字节 + 内容,结尾加
\0,例如"www.example.com"编码为\x03www\x07example\x03com\x00 -
QTYPE:查 IPv4 用0x0001(A 记录),IPv6 用0x001c(AAAA 记录) -
QCLASS固定为0x0001(IN 类)
别手写整个包——用 uint8_t buf[512] 开头填头部,再用循环拼域名,最后 memcpy 进去。超过 512 字节不保证被处理(EDNS0 可扩展,但公共 DNS 如 8.8.8.8 默认只认 512)。
怎么用 epoll 等待 DNS 响应并避免超时/乱序?
每个查询必须绑定唯一 transaction ID(DNS 头部前两字节),同时记录发送时间戳。收到 UDP 包后:
- 检查
recvfrom返回长度 ≥ 12,且前两字节匹配你发出的 ID - 检查
QR位为 1,RCode为 0(无错误) - 用
epoll_ctl(EPOLL_CTL_ADD)注册 socket 读事件,超时用epoll_wait的timeout参数控制(建议 3000ms) - 收到响应后立刻
epoll_ctl(EPOLL_CTL_DEL)移除监听,防止重复触发
注意:UDP 不保证顺序和到达,不能假定先发先回;也不能复用同一个 socket 给多个并发查询——要么每个查询独占 socket,要么用 ID + 地址四元组(src ip/port + dst ip/port)做响应匹配。
解析 DNS 响应时最容易漏掉的三个细节
响应包里资源记录(RR)是变长结构,跳过问题部分后,必须逐个解析:
- 域名压缩指针(0xc0 开头的两字节)必须解压,不能当普通字符串读——要回溯到前面已解析的位置
-
RDLENGTH指向 RDATA 起始,A 记录是 4 字节in_addr,AAAA 是 16 字节in6_addr,别直接 memcpy 到sockaddr_in成员里 -
ANCOUNT是答案数,但实际有效记录可能少于它(比如 CNAME 链中只有最后一个有 A/AAAA)——得遍历所有 answer,过滤出TYPE == 1 || TYPE == 28的记录
没有现成的跨平台 DNS 解析库就别硬啃 RFC 1035;真要快速落地,用 libunbound 或 c-ares 更稳——它们内部已经处理了重试、TCP fallback、EDNS0、DNSSEC 验证等边界情况。自己实现只适合学习或嵌入式极简场景。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











