不能直接用 getaddrinfo 做异步 dns,因其底层系统调用阻塞且不可被 epoll/io_uring 监听;必须绕过 libc 自行构造 dns 报文、管理事务 id 与超时,并正确解析域名压缩格式。

为什么不能直接用 getaddrinfo 做异步DNS?
getaddrinfo 是阻塞的,哪怕你把它丢进线程池,也绕不开系统调用本身的同步等待。Linux 下它会读 /etc/resolv.conf、发 UDP 包、等超时重传、再解析响应——整个过程无法被 epoll 或 io_uring 监听。强行非阻塞包装(比如设 socket 为 non-blocking)也没用,因为 getaddrinfo 根本不暴露底层 socket。
真实场景中,你希望在建立 TCP 连接前就并发查多个域名,且不阻塞主线程或事件循环。这时候必须绕过 libc 的 DNS 封装,自己构造 DNS 查询包。
- 别依赖
AI_ADDRCONFIG或AI_V4MAPPED等 flag——它们只影响getaddrinfo行为,对自研解析器无效 - Linux 上
/etc/resolv.conf的nameserver行才是唯一可信的上游地址,别硬编码8.8.8.8 - UDP 查询没有连接状态,但需自己管理 transaction ID 和超时;TCP 查询虽可靠,但开销大,一般只在 UDP 超时后降级使用
怎么用 io_uring 发起非阻塞 DNS 查询?
io_uring 本身不支持 DNS 协议,但它能高效提交 UDP send/recv 请求,并批量等待完成。关键是你得自己拼 DNS 查询报文(12 字节 header + QNAME + QTYPE/QCLASS),并维护一个 per-query 的上下文(ID、超时时间、回调函数指针)。
示例要点:
立即学习“C++免费学习笔记(深入)”;
- 用
socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, IPPROTO_UDP)创建非阻塞 socket - 用
io_uring_prep_sendto提交查询,目标地址取自/etc/resolv.conf解析出的nameserver - 用
io_uring_prep_recvfrom等待响应,buffer 至少 512 字节(RFC 1035 规定最小 UDP 响应上限) - 收到响应后,用
ntohs(*(uint16_t*)buf)检查 transaction ID 是否匹配,否则丢弃(防 spoofing 或乱序)
注意:io_uring 的 IORING_OP_SENDTO 不会自动重试,超时逻辑必须由用户态 timer(如 timerfd_create)驱动重发。
如何避免 std::string 和 DNS 名字压缩导致的解析错误?
DNS 响应里的域名不是纯 ASCII 字符串,而是标签长度+内容的二进制格式(如 \x03www\x07example\x03com\x00),还可能含压缩指针(0xc0 0x0c)。用 std::string 直接接收或比较会出错——它把 \x00 当结束符,又忽略压缩逻辑。
正确做法是写一个轻量解析器,逐字节读取:
- 遇到
0xc0开头的两字节,说明是压缩指针,高位 2 bit 是标志位,剩下 14 bit 是偏移量 - 普通标签以长度字节开头,后面跟着对应长度的字符,结尾
\x00才算完整域名 - 不要用
std::string::c_str()去传给inet_pton——先提取出标准点分格式(如"192.0.2.1"),再转
常见错误现象:getaddrinfo 返回 EAI_NONAME,但你的解析器返回空结果——大概率是没处理压缩指针,跳过了 CNAME 链中的中间节点。
要不要支持 DoH(DNS over HTTPS)?
除非你明确要穿透企业防火墙或规避本地 DNS 劫持,否则别在高性能预处理场景里加 DoH。它引入 TLS 握手、HTTP/2 帧解析、证书验证三重开销,单次查询延迟从几毫秒涨到几十毫秒,还依赖 OpenSSL 或 BoringSSL 链接。
更现实的选择是:保留 UDP/TCP fallback 路径,只在检测到 /etc/resolv.conf 中配置了 127.0.0.1(比如 systemd-resolved 或 dnsmasq)时,才走本地代理;其他情况一律直连上游 nameserver。
- DoH 的
POST /dns-querybody 是 base64url 编码的 DNS 报文,不是文本——别用curl -d手动测试,容易漏掉 padding 或换行 - HTTP/2 流复用需要维护连接池,而 DNS 预处理通常是短时爆发请求,连接池收益极低
- 证书校验若关掉(
CURLOPT_SSL_VERIFYPEER=0),就失去 DoH 安全意义;若开着,就得嵌入 CA bundle 或调系统 store,增加部署复杂度
真正难的不是协议实现,而是把 transaction ID、超时、重试、响应解析、CNAME 展开、IPv4/IPv6 优先级这整条链路串成无锁、无内存拷贝、可取消的状态机——多数人卡在这一步,不是卡在“怎么发包”。











