
dns 查询的本质:理解 dns 协议的二进制消息结构
DNS 协议工作在应用层,但与 HTTP 等文本协议截然不同——它不使用 ASCII 命令行语法,而是基于紧凑、定长+变长混合的二进制消息格式(定义于 RFC 1035)。这意味着你无法像发送 GET / HTTP/1.1 那样直接写出明文查询;相反,一个标准 DNS 查询是一个至少 12 字节的二进制数据包,由以下逻辑部分构成:
- Header(12 字节):含事务 ID(2B)、标志位(2B,含 QR、Opcode、AA、TC、RD、RA 等)、问题数(2B)、回答数/授权数/附加数(各 2B);
- Question Section(变长):包含查询域名(以“标签长度 + 标签”序列编码,如 google.com. 编码为 06google03com00)、查询类型(2B,如 A=1, AAAA=28, MX=15)、查询类(2B,通常为 IN=1)。
例如,查询 google.com 的 A 记录,其二进制 Question Section 并非 "google.com A",而是:
06 67 6f 6f 67 6c 65 03 63 6f 6d 00 00 01 00 01 ↑───────── domain ─────────↑ ↑type=A↑ ↑class=IN↑
✅ 实践建议:在 Go 中开发 DNS 服务器,强烈推荐使用成熟的库(如 github.com/miekg/dns),而非手动拼接字节。它封装了完整的消息构建与解析逻辑,例如:
m := new(dns.Msg)
m.SetQuestion(dns.Fqdn("google.com."), dns.TypeA)
m.RecursionDesired = true
// 序列化为二进制 []byte,可直接写入 UDP 连接
buf, err := m.Pack()
if err != nil { /* handle */ }⚠️ 注意事项:
- 域名必须以 FQDN 形式传递(末尾带点,如 "google.com."),否则库可能补全为相对域名;
- DNS 默认使用 UDP 端口 53,单包大小限制为 512 字节(EDNS0 可扩展);
- TCP 回退机制在响应超长或区域传输时启用;
- 手动解析 Header 标志位需按位运算(如 msg.Header.Bits&0x8000 != 0 判断是否为响应)。
总结:DNS 查询是面向机器优化的二进制协议,其设计目标是高效、低开销和确定性解析。理解其结构是实现可靠 DNS 服务的基础,而借助专业库可避免底层字节错误,聚焦业务逻辑。










