Appearance
DNS 域名解析
概念
DNS(Domain Name System,域名系统)是互联网的"电话簿":把人类好记的域名(如 www.example.com)翻译成机器要用的 IP 地址。 它同时也是整个互联网上规模最大的分布式数据库。
★ 三个关键词,一个都不能少:
| 关键词 | 含义 | 为什么必须有 |
|---|---|---|
| 层次 | 域名是一棵树,从根往下逐级分权 | 否则一个机构管不了全世界的名字 |
| 分布式 | 没有一台机器存全部映射,各级域名服务器各管一段 | 集中存的话,容量、性能、单点故障全都不可接受 |
| 缓存 | 任何一级解析结果都会被本地缓存一段时间(TTL) | 否则根服务器会被全世界的查询压垮 |
★ 一句话说清它与上一章的关系:本章是应用层的第一个协议,也是"为什么 DNS 要用 UDP"的答案——上一章(net/44-congestion.md)讲过"建连要 1 个 RTT",而域名解析是"发出请求前的第一步",必须尽可能快,所以 DNS 查询默认走 UDP,省掉三次握手。
与主线其他部分的呼应:DNS 查询本身是一段二进制报文(12 B 首部 + 变长域名),本章示例就是把这段报文按字节拆开——这与 net/30-ip.md 拆 IP 首部、net/41-tcp.md 拆 TCP 首部是同一套"逐字段读位"的手艺。
原理
一、域名的树形结构与长度限制
★ 域名的层次(从右往左读):
text
★ 域名从右往左, 范围从大到小:
. <- 根 (root), 写作一个点
│
┌────────────┼─────────────┬───────────────┐
com org cn (gTLD / ccTLD)
│ │
example tsinghua <- 二级域 (机构自己申请)
│ │
www www <- 主机名 (机构自己分配)
★ 完整的写法: www.example.com. <- 最后那个点是"根", 常可省略
★ 不省略末尾的点 = 完全限定域名 (FQDN)★ 长度限制(必背数字):
| 限制项 | 数值 | 说明 |
|---|---|---|
| 单个标签 | ≤ 63 字符 | 长度字节本身占 1 B,所以标签编码最长 63 + 1 = 64 B |
| 整个域名 | ≤ 253 字符 | 编码后不超过 255 B(因为总长度字段只有 1 B) |
| 根服务器 | 13 组(a ~ m) | 靠任播部署了成百上千个物理实例,对外仍是 13 个"地址" |
★ 顶级域的两类:gTLD(generic TLD,通用顶级域,如 .com/.org/.net);ccTLD(country-code TLD,国家与地区顶级域,如 .cn/.uk/.jp,以及中国香港的 .hk、中国台湾的 .tw、中国澳门的 .mo)。
⚠️ 别把"13 组根服务器"理解成"只有 13 台机器":13 是"根区文件里的 13 个地址",每个地址背后用任播(anycast)部署了多个物理服务器——实际实例数早已上千。 考试只考"根服务器有 13 个"这句话,不会考实例数。
二、四类域名服务器
★ 分工表(必背):
| 类型 | 管什么 | 典型回答 |
|---|---|---|
| 根域名服务器 | 只知道"各顶级域由谁管" | ".com 的事去问 .com 顶级域名服务器,它的地址是 …" |
| 顶级域名服务器 | 管一个顶级域下的所有二级域 | "example.com 由谁管,去问它的权限服务器" |
| 权限域名服务器(权威) | 管一个区(zone)内的全部主机记录 | "www.example.com = 93.184.216.34"(最终答案) |
| 本地域名服务器 | 管的不是"某个域",而是"一群客户" | 替主机去逐级问,并把结果缓存起来 |
★ 本地域名服务器是最特殊的一个:它不位于域名树的任何一层——它属于"你的网络"(ISP 或企业内网),通常是"主机在 DNS 配置里填的那一个地址"。 主机之所以只需要知道它就够了,是因为它会替主机去跑完剩下的路(递归查询)。
三、递归查询与迭代查询
★ 两种查询方式的分工(必背,最容易考混):
| 查询方式 | 谁对谁 | 回答的内容 | 本场景中用在 |
|---|---|---|---|
| 递归查询 | 主机 → 本地域名服务器 | "要么给我最终 IP,要么给我失败" | 主机对本地服务器 |
| 迭代查询 | 本地域名服务器 → 根 / 顶级 / 权限 | "我不知道,但你去问某某服务器" | 本地服务器逐级向上问 |
★ 完整解析过程(锚点:解析 www.example.com,缓存全空):
text
主机 本地域名服务器 根 .com 顶级 权限服务器
│ │ │ │ │
│ ①递归查询: www.example.com │ │ │
├───────────────────────►│ │ │ │
│ │ ②迭代: 问根 │ │ │
│ ├─────────────────────►│ │ │
│ │ ③答: 去问 .com 顶级 │ │ │
│ │◄─────────────────────┤ │ │
│ │ ④迭代: 问 .com 顶级 │ │
│ ├─────────────────────────────────►│ │
│ │ ⑤答: 去问 example.com 权限服务器 │ │
│ │◄─────────────────────────────────┤ │
│ │ ⑥迭代: 问 example.com 权限 │
│ ├───────────────────────────────────────────────►│
│ │ ⑦答: www.example.com = 93.184.216.34 │
│ │◄───────────────────────────────────────────────┤
│ ⑧递归应答: IP = 93.184.216.34 │ │ │
│◄───────────────────────┤ │ │ │
★ 共 8 个报文 (①②③④⑤⑥⑦⑧)★ 报文数账(必背):
| 情形 | 报文数 | 说明 |
|---|---|---|
| 缓存全空 | 8 | 主机↔本地 2 个 + 本地与三级服务器往返 6 个 |
| 本地服务器有缓存 | 2 | 主机问、本地答——不再向任何上级服务器发报文 |
| 主机自己有缓存 | 0 | 连本地服务器都不用问 |
⚠️ "谁能缓存"是常考的隐藏分:主机、本地域名服务器都会缓存——所以"第二次访问同一个网站会快很多"。 缓存有生存期 TTL,到期必须重新查询;查不到(域名不存在)时也会"负缓存"一小段时间,避免反复无效查询。
四、DNS 报文格式与资源记录
★ 报文首部固定 12 B(六个字段):
text
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
┌───────────────────────────────┬───────────────────────────────┐
│ 标识 ID │QR│ Opcode │AA│TC│RD│RA│ Z │RCODE│
│ 16 位 │1 │ 4 位 │1 │1 │1 │1 │3 位│ 4位 │
├───────────────────────────────┼───────────────────────────────┤
│ QDCOUNT │ ANCOUNT │
├───────────────────────────────┼───────────────────────────────┤
│ NSCOUNT │ ARCOUNT │
└───────────────────────────────┴───────────────────────────────┘
★ 之后依次是: 查询段 (QNAME + QTYPE + QCLASS)
回答段 / 授权段 / 附加段 (资源记录 RR)
★ QNAME 编码 (锚点 www.example.com):
03 'w''w''w' 07 'e''x''a''m''p''l''e' 03 'c''o''m' 00
└ 每个标签前用 1 个字节写长度, 最后用 0 字节收尾 (= 根)★ 常见资源记录(RR)类型(必背):
| 类型 | 名字 | 存什么 | 例子 |
|---|---|---|---|
| A | 地址 | IPv4 地址 | www.example.com → 93.184.216.34 |
| AAAA | 四 A | IPv6 地址 | www.example.com → 2606:2800::1 |
| CNAME | 规范名 | 别名指向另一个域名 | blog.example.com → example.wordpress.com |
| MX | 邮件交换 | 该域的邮件服务器 | example.com → mail.example.com |
| NS | 名字服务器 | 该域由哪台名称服务器管 | example.com → ns1.example.com |
| PTR | 指针 | IP 反查域名(反向解析) | 34.216.184.93.in-addr.arpa → www.example.com |
| SOA | 起始授权 | 区的管理信息(主服务器、序列号、TTL) | 每个区必须有一条 |
五、DNS 为什么用 UDP
★ 一次对比(这就是"省一个 RTT"的账):
| 传输层 | 一次查询的额外开销 | 结果 |
|---|---|---|
| UDP 53 | 无连接,直接问、直接答 | 总时延 ≈ 1 个 RTT(往返) |
| TCP 53 | 先三次握手 1 RTT,再问再答 1 RTT | 总时延 ≈ 2 个 RTT |
★ 所以 DNS 查询默认走 UDP——但有两种情况必须改成 TCP:
text
① 响应的长度超过 512 B (UDP 报文放不下) -> 改 TCP 重来
★ 有了 EDNS0 之后这个上限可协商到 4096 B, 但"超限改 TCP"这个机制不变
② 区域传送 (主服务器把整个区的数据同步给辅服务器, AXFR/IXFR) -> 必须 TCP
★ 因为它要传的数据量很大, 而且要可靠
★ 顺带: 请求方如果收到"带 TC=1(截断)标志"的响应, 就知道要用 TCP 重问⚠️ 别答成"DNS 只能 UDP":准确的说法是"查询默认 UDP、超长与区域传送走 TCP"。 这道题最常以"DNS 都用 UDP 吗"或"什么时候用 TCP"的形式出现。
示例
例 1:C 实现——DNS 查询报文逐字段解析
参数:手工构造一个查询
www.example.com的 DNS 报文,共 33 B。
#include <stdio.h>
#include <string.h>
int main(void) {
/* 手工构造查询 www.example.com 的 DNS 报文, 共 33 B */
unsigned char q[33] = {
0x1A, 0x2B, /* 标识 ID */
0x01, 0x00, /* 标志: QR=0 查询, RD=1 期望递归 */
0x00, 0x01, /* QDCOUNT = 1 */
0x00, 0x00, /* ANCOUNT = 0 */
0x00, 0x00, /* NSCOUNT = 0 */
0x00, 0x00, /* ARCOUNT = 0 */
0x03, 'w', 'w', 'w', /* 标签 www */
0x07, 'e', 'x', 'a', 'm', 'p', 'l', 'e', /* 标签 example */
0x03, 'c', 'o', 'm', /* 标签 com */
0x00, /* 根标签: QNAME 结束 */
0x00, 0x01, /* QTYPE = A */
0x00, 0x01 /* QCLASS = IN */
};
int ID = (q[0] << 8) | q[1];
int fl = (q[2] << 8) | q[3];
int QR = (fl >> 15) & 1, OP = (fl >> 11) & 0xF, AA = (fl >> 10) & 1;
int TC = (fl >> 9) & 1, RD = (fl >> 8) & 1, RA = (fl >> 7) & 1, RC = fl & 0xF;
int QD = (q[4] << 8) | q[5], AN = (q[6] << 8) | q[7];
int NS = (q[8] << 8) | q[9], AR = (q[10] << 8) | q[11];
char name[128];
int i = 12, k, n, nlab = 0, chars = 0, span, qtype, qclass, nl = 0;
printf("(1) DNS 首部 (固定 12 B)\n");
printf(" ID = 0x%04X QR=%d (0=查询) Opcode=%d AA=%d TC=%d RD=%d RA=%d RCODE=%d\n",
ID, QR, OP, AA, TC, RD, RA, RC);
printf(" QDCOUNT=%d ANCOUNT=%d NSCOUNT=%d ARCOUNT=%d\n", QD, AN, NS, AR);
printf(" ★ 整报文长度 = %d B\n", (int)sizeof(q));
printf("\n");
printf("(2) QNAME 逐标签解码\n");
while (q[i] != 0) {
n = q[i];
printf(" 偏移 %2d: len=%d label=", i, n);
for (k = 1; k <= n; k++) {
putchar(q[i + k]);
name[nl++] = q[i + k];
}
printf("\n");
name[nl++] = '.';
chars += n; nlab++;
i += n + 1;
}
printf(" 偏移 %2d: len=0 (根标签, QNAME 结束)\n", i);
i += 1; /* 吃掉根标签的 0 字节 */
name[nl - 1] = '\0'; /* 去掉末尾多余的点 */
span = i - 12;
printf(" ★ QNAME 占 %d B = %d 个标签字符 + %d 个长度字节 + 1 个根字节\n",
span, chars, nlab);
printf(" 域名 = %s, 共 %d 个字符\n", name, (int)strlen(name));
printf("\n");
qtype = (q[i] << 8) | q[i + 1];
qclass = (q[i + 2] << 8) | q[i + 3];
printf("(3) 查询段\n");
printf(" QTYPE = %d (A) QCLASS = %d (IN) -> 4 B\n", qtype, qclass);
printf(" ★ 12 + %d + 4 = %d B (整个查询报文)\n", span, 12 + span + 4);
printf("\n");
printf("(4) 长度限制\n");
printf(" 单个标签 <= 63 B; 整个域名 <= 253 字符 (编码后 <= 255 B)\n");
printf(" UDP 载荷上限 512 B -> EDNS0 可放宽到 4096 B\n");
printf(" 区域传送与超过 512 B 的应答改走 TCP 53\n");
return 0;
}
c 本站为静态站,不提供在线运行;可复制到本地用 gcc / python 执行
预期输出:
text
(1) DNS 首部 (固定 12 B)
ID = 0x1A2B QR=0 (0=查询) Opcode=0 AA=0 TC=0 RD=1 RA=0 RCODE=0
QDCOUNT=1 ANCOUNT=0 NSCOUNT=0 ARCOUNT=0
★ 整报文长度 = 33 B
(2) QNAME 逐标签解码
偏移 12: len=3 label=www
偏移 16: len=7 label=example
偏移 24: len=3 label=com
偏移 28: len=0 (根标签, QNAME 结束)
★ QNAME 占 17 B = 13 个标签字符 + 3 个长度字节 + 1 个根字节
域名 = www.example.com, 共 15 个字符
(3) 查询段
QTYPE = 1 (A) QCLASS = 1 (IN) -> 4 B
★ 12 + 17 + 4 = 33 B (整个查询报文)
(4) 长度限制
单个标签 <= 63 B; 整个域名 <= 253 字符 (编码后 <= 255 B)
UDP 载荷上限 512 B -> EDNS0 可放宽到 4096 B
区域传送与超过 512 B 的应答改走 TCP 53⚠️ 三点说明:
- 本机无 C 编译器:此段代码逐行人工审查,并用等价的 Python 实现实跑核对,输出逐字一致。
0x01 0x00这个标志字要会拆:最高位 QR = 0 表示这是查询;第 8 位 RD = 1 表示"期望对方递归查询"——主机对本地域名服务器正是靠这一位表达"请帮我查到最后"。 若把 RD 置 0,本地服务器只回它自己知道的部分,不代查。- QNAME 的编码规律是"长度 + 内容"交替,末尾 0 收尾:所以"能认出域名边界"不需要额外分隔符——这也是"标签长度上限 63"的由来:长度字段只有 1 个字节,且高两位另有用途(压缩指针),可用值 0 ~ 63。
例 2:Python——报文数账、长度校验与记录类型
def pad(s, w):
"""按"显示宽度"补空格: 汉字算 2 列, 否则终端里对不齐"""
return s + ' ' * max(0, w - sum(2 if ord(c) > 0x2000 else 1 for c in s))
def encode(name):
"""把域名编码成 DNS 线上格式: 每标签前置长度字节, 末尾补 0"""
out = b''
for lab in name.split('.'):
out += bytes([len(lab)]) + lab.encode('ascii')
return out + b'\x00'
print('=== ① 域名编码与长度账 ===')
for nm in ('www.example.com', 'example.com', 'a.b.c.d.e', 'mail.tsinghua.edu.cn'):
enc = encode(nm)
labs = nm.split('.')
print(' ' + pad(nm, 24) + pad('%d 字符' % len(nm), 12) + pad('%d 个标签' % len(labs), 12)
+ '编码 %d B' % len(enc))
print(' ★ 编码长度 = 标签字符数 + 标签数 + 1 (根字节); 点号不占字节')
print(' 校验 www.example.com: 13 + 3 + 1 = 17 B')
print(' ★ 整个域名 <= 253 字符, 编码后 <= 255 B (总长度字段只有 1 B)')
print()
print('=== ② 解析一次的报文数 ===')
print(' ' + pad('情形', 26) + pad('报文数', 10) + '明细')
cases = [('主机无缓存, 本地无缓存', 8, '主机↔本地 2 个 + 本地与根/顶级/权限往返 6 个'),
('本地域名服务器有缓存', 2, '只有主机问、本地答'),
('主机自己有缓存', 0, '连本地服务器都不用问'),
('响应超 512 B 改走 TCP', 8 + 2, '多出三次握手(1 个)与重问(1 个)的额外报文')]
for a, n, d in cases:
print(' ' + pad(a, 26) + pad(str(n), 10) + d)
print(' ★ 缓存是"DNS 不会被压垮"的唯一原因: 命中就只剩 2 个报文')
print()
print('=== ③ 递归查询与迭代查询 ===')
print(' ' + pad('环节', 30) + pad('方式', 12) + '对方必须回答什么')
q = [('主机 -> 本地域名服务器', '递归', '最终 IP 或失败'),
('本地 -> 根域名服务器', '迭代', '下一步该问谁'),
('本地 -> 顶级域名服务器', '迭代', '下一步该问谁'),
('本地 -> 权限域名服务器', '迭代', '最终 IP (或"没有这条记录")')]
for a, b, c in q:
print(' ' + pad(a, 30) + pad(b, 12) + c)
print(' ★ 主机只发 1 个查询, 剩下的路全由本地服务器代走')
print()
print('=== ④ 四类域名服务器 ===')
srv = [('根域名服务器', '13 组 (a~m), 任播多实例', '只知道各顶级域由谁管'),
('顶级域名服务器', '管一个顶级域', '知道该顶级域下各二级域由谁管'),
('权限域名服务器', '管一个区 (zone)', '给出最终答案'),
('本地域名服务器', '不属域名树的任何一层', '替客户逐级问并缓存')]
print(' ' + pad('类型', 18) + pad('管理范围', 26) + '典型回答')
for a, b, c in srv:
print(' ' + pad(a, 18) + pad(b, 26) + c)
print()
print('=== ⑤ 常见资源记录类型 ===')
rr = [('A', 'IPv4 地址', 'www.example.com -> 93.184.216.34'),
('AAAA', 'IPv6 地址', 'www.example.com -> 2606:2800::1'),
('CNAME', '别名', 'blog.example.com -> example.wordpress.com'),
('MX', '邮件服务器', 'example.com -> mail.example.com'),
('NS', '该域的名字服务器', 'example.com -> ns1.example.com'),
('PTR', '反向解析', '34.216.184.93.in-addr.arpa -> www.example.com'),
('SOA', '区的管理信息', '每区必须一条: 主服务器 + 序列号 + TTL')]
print(' ' + pad('类型', 10) + pad('存什么', 20) + '例子')
for a, b, c in rr:
print(' ' + pad(a, 10) + pad(b, 20) + c)
print()
print('=== ⑥ 缓存 TTL 与"第二次访问" ===')
ttl, first, second = 86400, 60, 3
print(' 首次解析: 主机↔本地 %d ms + 3 级迭代各 20 ms = %d ms' % (first, first + 3 * 20))
print(' 本地已缓存 (TTL = %d s 未到期): %d ms' % (ttl, second))
print(' 提速 = %d / %d = %.1f 倍' % (first + 3 * 20, second, (first + 3 * 20) / second))
print(' ★ 域名不存在时也会"负缓存", 避免反复无效查询')
print(' ★ TTL 到期 -> 重新逐级查询; 正向与反向解析是两套独立记录')
print()
print('=== ⑦ UDP 还是 TCP ===')
print(' ' + pad('场合', 34) + pad('传输层', 10) + '原因')
uu = [('普通查询与应答 (<= 512 B)', 'UDP 53', '省掉三次握手的 1 个 RTT'),
('应答超过 512 B', 'TCP 53', 'UDP 放不下, 靠 TC=1 标志触发重问'),
('区域传送 AXFR / IXFR', 'TCP 53', '数据量大且必须可靠'),
('启用 EDNS0 后的大应答', 'UDP 53 (可到 4096 B)', '协商放大 UDP 载荷上限')]
for a, b, c in uu:
print(' ' + pad(a, 34) + pad(b, 10) + c)
print(' ★ 准确说法: 查询默认 UDP, 超长与区域传送走 TCP')
python 本站为静态站,不提供在线运行;可复制到本地用 gcc / python 执行
输出对照(真实运行结果):
=== ① 域名编码与长度账 ===
www.example.com 15 字符 3 个标签 编码 17 B
example.com 11 字符 2 个标签 编码 13 B
a.b.c.d.e 9 字符 5 个标签 编码 11 B
mail.tsinghua.edu.cn 20 字符 4 个标签 编码 22 B
★ 编码长度 = 标签字符数 + 标签数 + 1 (根字节); 点号不占字节
校验 www.example.com: 13 + 3 + 1 = 17 B
★ 整个域名 <= 253 字符, 编码后 <= 255 B (总长度字段只有 1 B)
=== ② 解析一次的报文数 ===
情形 报文数 明细
主机无缓存, 本地无缓存 8 主机↔本地 2 个 + 本地与根/顶级/权限往返 6 个
本地域名服务器有缓存 2 只有主机问、本地答
主机自己有缓存 0 连本地服务器都不用问
响应超 512 B 改走 TCP 10 多出三次握手(1 个)与重问(1 个)的额外报文
★ 缓存是"DNS 不会被压垮"的唯一原因: 命中就只剩 2 个报文
=== ③ 递归查询与迭代查询 ===
环节 方式 对方必须回答什么
主机 -> 本地域名服务器 递归 最终 IP 或失败
本地 -> 根域名服务器 迭代 下一步该问谁
本地 -> 顶级域名服务器 迭代 下一步该问谁
本地 -> 权限域名服务器 迭代 最终 IP (或"没有这条记录")
★ 主机只发 1 个查询, 剩下的路全由本地服务器代走
=== ④ 四类域名服务器 ===
类型 管理范围 典型回答
根域名服务器 13 组 (a~m), 任播多实例 只知道各顶级域由谁管
顶级域名服务器 管一个顶级域 知道该顶级域下各二级域由谁管
权限域名服务器 管一个区 (zone) 给出最终答案
本地域名服务器 不属域名树的任何一层 替客户逐级问并缓存
=== ⑤ 常见资源记录类型 ===
类型 存什么 例子
A IPv4 地址 www.example.com -> 93.184.216.34
AAAA IPv6 地址 www.example.com -> 2606:2800::1
CNAME 别名 blog.example.com -> example.wordpress.com
MX 邮件服务器 example.com -> mail.example.com
NS 该域的名字服务器 example.com -> ns1.example.com
PTR 反向解析 34.216.184.93.in-addr.arpa -> www.example.com
SOA 区的管理信息 每区必须一条: 主服务器 + 序列号 + TTL
=== ⑥ 缓存 TTL 与"第二次访问" ===
首次解析: 主机↔本地 60 ms + 3 级迭代各 20 ms = 120 ms
本地已缓存 (TTL = 86400 s 未到期): 3 ms
提速 = 120 / 3 = 40.0 倍
★ 域名不存在时也会"负缓存", 避免反复无效查询
★ TTL 到期 -> 重新逐级查询; 正向与反向解析是两套独立记录
=== ⑦ UDP 还是 TCP ===
场合 传输层 原因
普通查询与应答 (<= 512 B) UDP 53 省掉三次握手的 1 个 RTT
应答超过 512 B TCP 53 UDP 放不下, 靠 TC=1 标志触发重问
区域传送 AXFR / IXFR TCP 53 数据量大且必须可靠
启用 EDNS0 后的大应答 UDP 53 (可到 4096 B)协商放大 UDP 载荷上限
★ 准确说法: 查询默认 UDP, 超长与区域传送走 TCP五条结论:
- ★ 域名的编码规律是"长度 + 内容"交替、末尾 0 收尾:
www.example.com有 3 个标签、标签字符共 13 个,编码后是 B——点号不占字节,它由长度字节隐含表达。 本文示例报文里的 QNAME 是 17 B,正好对上。 - ★ 长度限制来自字段宽度:单个标签 ≤ 63(长度字段 1 B,且高 2 位留给压缩指针);整个域名 ≤ 253 字符(编码后 ≤ 255 B,因为总长度字段只有 1 B)。
- ★ 一次无缓存解析共 8 个报文:主机↔本地 2 个 + 本地与三级服务器往返 6 个;本地服务器有缓存时只剩 2 个、主机自己有缓存时为 0 个。
- ★ 递归查询用在"主机 → 本地"这一跳:本地服务器对上级一律用迭代查询——"谁代查"与"谁给线索"是这两种方式的分界。
- ★ DNS 查询默认走 UDP 53:省掉 1 个 RTT 的握手;响应超过 512 B(或带 TC = 1 标志)以及区域传送(AXFR/IXFR)必须改用 TCP 53。
考点
考点
1. 必背结论
- ★ DNS 的三个特征:层次、分布式、带缓存。
- ★ 域名从右往左读:根(最后的点)→ 顶级域 → 二级域 → 主机名;不省略末尾的点叫完全限定域名 FQDN。
- ★★ 长度限制:单个标签 ≤ 63 字符;整个域名 ≤ 253 字符(编码后 ≤ 255 B)。
- ★ 根服务器 13 组(a ~ m):靠任播部署大量物理实例,对外仍是 13 个地址。
- ★★ 四类域名服务器:根(只知道顶级域由谁管)、顶级(知道二级域由谁管)、权限/权威(给出最终答案)、本地(替客户逐级问并缓存)。
- ★★ 递归查询只用在"主机 → 本地域名服务器":回答"最终 IP 或失败";本地对根/顶级/权限全部用迭代查询:回答"下一步去问谁"。
- ★★ 一次无缓存解析共 8 个报文;本地有缓存时 2 个;主机自己有缓存时 0 个。
- ★ DNS 首部固定 12 B:标识 ID、标志(QR/Opcode/AA/TC/RD/RA/RCODE)、QDCOUNT、ANCOUNT、NSCOUNT、ARCOUNT。
- ★ QNAME 编码:每个标签前置 1 个长度字节,末尾用 0 字节(根)收尾;点号不单独编码。
- ★ RD 位 = "期望递归":主机对本地域名服务器置 1。
- ★★ 资源记录类型:A(IPv4)、AAAA(IPv6)、CNAME(别名)、MX(邮件服务器)、NS(名字服务器)、PTR(反向解析)、SOA(区的管理信息)。
- ★★ DNS 查询默认 UDP 53;响应超过 512 B、带 TC = 1 标志、区域传送(AXFR/IXFR)三种情况改用 TCP 53;EDNS0 可把 UDP 载荷上限协商到 4096 B。
- ★ 缓存有 TTL;域名不存在时会负缓存一小段时间。
2. 高频陷阱
- 把 DNS 服务器说成"一台大服务器":错。它是分布式的层次数据库——没有任何一台机器存全部映射。
- 把根服务器记成"一台"或"几百台":错。是 13 组(a ~ m);物理实例靠任播上千,考试只看 13 这个数。
- 认为"根域名服务器直接给出 IP":错。根只告诉你"去问哪个顶级域名服务器"——最终 IP 由权限域名服务器给出。
- 把递归与迭代搞反:错。主机对本地是递归(要最终答案);本地对上级是迭代(只要线索)。
- 认为"本地域名服务器属于域名树的某一层":错。它不属于任何一层,它属于"你的网络"(ISP/企业)。
- 把"域名长度上限"记成 63:错。63 是"单个标签"的上限;整个域名是 253 字符(255 B 编码)。
- 认为"解析一次就是问一次":错。无缓存时是 8 个报文——因为本地服务器要逐级迭代三轮。
- 漏掉"主机自己也会缓存":不完整。主机、本地服务器都会缓存;主机命中时连本地服务器都不问。
- 认为"DNS 只能用 UDP":错。超 512 B 与区域传送必须用 TCP;TC = 1 标志就是"请改用 TCP 重问"的信号。
- 把 CNAME 与 A 记录搞混:要分清。A 给的是 IP;CNAME 给的是另一个域名——拿到 CNAME 后还要再解析一次。
- 把 PTR 与 A 当成"同一条记录正反两看":不准确。反向解析走的是独立的
in-addr.arpa树,需要单独配置,常常与正解不一致。 - 忘记 MX 记录:要记。邮件系统发信前必须先查 MX 记录——这也是 DNS 与邮件协议(
net/52-others.md)的接口。 - 认为"QNAME 里每个点都要占一个字节":错。点由长度字节隐含表达——
www.example.com编码后是 17 B 而不是 19 B。
3. 解题模板("DNS 题")
① 报文数题:
无缓存 -> 8 个报文 (主机↔本地 2 + 本地与三级往返 6)
本地缓存命中 -> 2 个
主机缓存命中 -> 0 个
★ 加一次 TCP 重问则再 +2 (握手 1 + 重问 1)
② 编码长度题:
编码长度 = 标签字符数 + 标签数 + 1 (根字节) <- 点号不占字节!
www.example.com -> 13 + 3 + 1 = 17 B
example.com -> 10 + 2 + 1 = 13 B
★ 别把"域名字符总数(含点)"直接拿来加, 那会把点算两遍
③ 长度上限题:
单个标签 <= 63 字符
整个域名 <= 253 字符 / 255 B
④ 服务器归属题:
"只知道顶级域" -> 根
"知道该顶级域下的二级域" -> 顶级
"给出 www.xxx.com 的 IP" -> 权限 (权威)
"替客户跑完全程并缓存" -> 本地
⑤ 传输层题:
<= 512 B -> UDP 53
超 512 B / TC=1 / 区域传送 -> TCP 53
问了 EDNS0 -> UDP 上限可到 4096 B4. 与相邻章节的接口
net/44-congestion.md(拥塞控制):"建连要 1 个 RTT"是 DNS 选 UDP 的直接理由——解析是"发请求前的第一步",必须最快。net/40-udp.md(UDP):DNS 查询是 UDP 面向报文的典型用法——一问一答各占一个报文,TCP 的字节流语义在这里完全用不上。net/52-others.md(应用层协议概览):SMTP 发信前要查 MX 记录、DHCP 要把 DNS 服务器地址一起下发——应用层协议彼此嵌套,DNS 是底座。net/30-ip.md与net/31-subnet.md:解析出来的 IP 要接着参与路由与子网判断——"域名 → IP → 下一跳"是应用层到网络层的直通车。soft/30-distributed.md(分布式系统):DNS 是"分布式一致性"的经典案例——它选择"最终一致"(靠 TTL 收敛)而不是强一致,正是为了可用性与性能。os/30-filesystem.md与os/32-io.md:本地的 hosts 文件与 DNS 缓存都在操作系统里查——"先查 hosts、再查 DNS"这个顺序是内核/解析库的固定行为。
小结
- ★ DNS 是分布式层次数据库,三个关键词:层次、分布式、缓存。
- ★ 域名从右往左读:根 → 顶级域 → 二级域 → 主机名(FQDN 末尾不省点)。
- ★★ 长度限制:标签 ≤ 63 字符;整名 ≤ 253 字符(编码 ≤ 255 B);根服务器 13 组(a ~ m)。
- ★★ 四类服务器:根 / 顶级 / 权限(权威)/ 本地;本地不属于域名树的任何一层。
- ★★ 递归用在"主机 → 本地";本地对上级一律迭代("下一步问谁")。
- ★★ 报文数:无缓存 8 个、本地缓存命中 2 个、主机缓存命中 0 个。
- ★ 报文格式:首部 12 B + QNAME(长度 + 内容交替,末尾 0) + QTYPE/QCLASS 各 2 B;锚点查询共 33 B。
- ★ 记录类型:A / AAAA / CNAME / MX / NS / PTR / SOA。
- ★★ 传输层:查询默认 UDP 53;超 512 B、TC = 1、区域传送(AXFR/IXFR)改走 TCP 53;EDNS0 可放宽到 4096 B。
上一篇:TCP 拥塞控制:慢开始、拥塞避免、快重传、快恢复 | 下一篇:HTTP 与 HTTPS
评论(0)
当前浏览器不允许本地存储,评论无法保存。
还没有评论,来说两句。