C++如何实现高性能的异步DNS解析器?(网络连接预处理)

4次阅读

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

C++如何实现高性能的异步DNS解析器?(网络连接预处理)

为什么不能直接用 getaddrinfo 做异步DNS?

getaddrinfo 是阻塞的,哪怕你把它丢进线程池,也绕不开系统调用本身的同步等待。linux 下它会读 /etc/resolv.conf、发 udp 包、等超时重传、再解析响应——整个过程无法被 epollio_uring 监听。强行非阻塞包装(比如设 socket 为 non-blocking)也没用,因为 getaddrinfo 根本不暴露底层 socket。

真实场景中,你希望在建立 TCP 连接前就并发查多个域名,且不阻塞主线程事件循环。这时候必须绕过 libc 的 DNS 封装,自己构造 DNS 查询包。

  • 别依赖 AI_ADDRCONFIGAI_V4MAPPED 等 flag——它们只影响 getaddrinfo 行为,对自研解析器无效
  • Linux 上 /etc/resolv.confnameserver 行才是唯一可信的上游地址,别硬编码 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_uringIORING_OP_SENDTO 不会自动重试,超时逻辑必须由用户态 timer(如 timerfd_create)驱动重发。

如何避免 std::String 和 DNS 名字压缩导致的解析错误?

DNS 响应里的域名不是纯 ASCII 字符串,而是标签长度+内容的二进制格式(如 x03wwwx07examplex03comx00),还可能含压缩指针(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-query body 是 base64url 编码的 DNS 报文,不是文本——别用 curl -d 手动测试,容易漏掉 padding 或换行
  • HTTP/2 流复用需要维护连接池,而 DNS 预处理通常是短时爆发请求,连接池收益极低
  • 证书校验若关掉(CURLOPT_SSL_VERIFYPEER=0),就失去 DoH 安全意义;若开着,就得嵌入 CA bundle 或调系统 store,增加部署复杂度

真正难的不是协议实现,而是把 transaction ID、超时、重试、响应解析、CNAME 展开、IPv4/IPv6 优先级这整条链路串成无锁、无内存拷贝、可取消的状态机——多数人卡在这一步,不是卡在“怎么发包”。

text=ZqhQzanResources