跳到主要内容

5. 解析器

5.1. 引言​

解析器 (Resolvers) 是将用户程序与域名服务器连接起来的程序. 在最简单的情况下, 解析器以子程序调用、系统调用等形式接收来自用户程序 (如邮件程序、TELNET、FTP) 的请求, 并以与本地主机数据格式兼容的形式返回所需信息.

解析器与请求解析器服务的程序位于同一台机器上, 但可能需要查询其他主机上的名称服务器. 由于解析器可能需要查询多个名称服务器, 或者可能在本地缓存中拥有所请求的信息, 解析器完成所需的时间可能变化很大, 从毫秒到几秒不等.

解析器的一个非常重要的目标是通过从先前结果的缓存中回答大多数请求来消除网络延迟和名称服务器负载. 由此可见, 由多个进程、用户、机器等共享的缓存比非共享缓存更高效.

5.2. 客户端-解析器接口​

5.2.1. 典型功能​

解析器的客户端接口受本地主机约定的影响, 但典型的解析器-客户端接口有三个功能:

1. 主机名到主机地址转换​

此功能通常被定义为模拟以前基于 HOSTS.TXT 的功能. 给定一个字符串, 调用者想要一个或多个 32 位 IP 地址. 在 DNS 下, 它转换为对类型 A RR 的请求. 由于 DNS 不保留 RR 的顺序, 此功能可以选择对返回的地址进行排序, 或者如果服务只向客户端返回一个选择, 则选择 "最佳" 地址. 请注意, 推荐返回多个地址, 但单个地址可能是模拟以前 HOSTS.TXT 服务的唯一方法.

2. 主机地址到主机名转换​

此功能通常遵循以前功能的形式. 给定一个 32 位 IP 地址, 调用者想要一个字符串. IP 地址的八位字节被反转, 用作名称组件, 并以 "IN-ADDR.ARPA" 为后缀. 使用类型 PTR 查询来获取具有主机主名称的 RR. 例如, 对应于 IP 地址 1.2.3.4 的主机名请求查找域名 "4.3.2.1.IN-ADDR.ARPA" 的 PTR RR.

3. 通用查找功能​

此功能从 DNS 检索任意信息, 在以前的系统中没有对应功能. 调用者提供 QNAME, QTYPE 和 QCLASS, 并想要所有匹配的 RR. 此功能通常对所有 RR 数据使用 DNS 格式而不是本地主机的格式, 并返回所有 RR 内容 (如 TTL) 而不是带有本地引用约定的处理形式.

结果类型​

当解析器执行指定功能时, 它通常有以下结果之一传递回客户端:

  • 一个或多个给出所请求数据的 RR: 在这种情况下, 解析器以适当的格式返回答案.

  • 名称错误 (NE): 当引用的名称不存在时发生. 例如, 用户可能输错了主机名.

  • 数据未找到错误: 当引用的名称存在, 但适当类型的数据不存在时发生. 例如, 应用于邮箱名称的主机地址功能将返回此错误, 因为名称存在, 但没有地址 RR.

重要的是要注意, 在主机名和地址之间转换的功能可以将 "名称错误" 和 "数据未找到" 错误条件合并为单一类型的错误返回, 但通用功能不应这样做. 原因之一是应用程序可能首先请求有关名称的一种类型的信息, 然后对同一名称发出第二个请求以获取其他类型的信息; 如果两个错误合并, 则无用的查询可能会减慢应用程序.

5.2.2. 别名​

在尝试解析特定请求时, 解析器可能会发现所讨论的名称是别名. 例如, 当解析器找到 CNAME RR 时, 可能会发现给定用于主机名到地址转换的名称是别名. 如果可能, 应将别名条件从解析器发回给客户端.

在大多数情况下, 解析器在遇到 CNAME 时只是在新名称处重新启动查询. 但是, 在执行通用功能时, 当 CNAME RR 与查询类型匹配时, 解析器不应追踪别名. 这允许查询询问别名是否存在. 例如, 如果查询类型是 CNAME, 用户对 CNAME RR 本身感兴趣, 而不是它指向的名称处的 RR.

别名可能出现几种特殊情况. 应避免多级别名, 因为它们缺乏效率, 但不应将其作为错误发出信号. 别名循环和指向不存在名称的别名应被捕获, 并将错误条件传递回客户端.

5.2.3. 临时故障​

在不完美的世界中, 所有解析器偶尔都无法解析特定请求. 这种情况可能由于链路故障或网关问题导致解析器与网络其余部分断开连接, 或者较少见地由于特定域的所有服务器同时故障或不可用而引起.

至关重要的是, 这种情况不应作为名称或数据不存在错误发送给应用程序. 这种行为对人类来说很烦人, 当邮件系统使用 DNS 时可能造成严重破坏.

虽然在某些情况下可以通过无限期阻塞请求来处理此类临时问题, 但这通常不是一个好选择, 特别是当客户端是可以继续处理其他任务的服务器进程时. 推荐的解决方案是始终将临时故障作为解析器功能的可能结果之一, 即使这可能使模拟现有 HOSTS.TXT 功能更加困难.

5.3. 解析器内部机制​

每个解析器实现使用略有不同的算法, 通常花费比典型情况更多的逻辑来处理各种错误. 本节概述了解析器操作的推荐基本策略, 但将细节留给 [RFC-1035].

5.3.1. 存根解析器​

实现解析器的一个选项是将解析功能从本地机器移出, 移入支持递归查询的名称服务器. 这可以为缺乏执行解析器功能资源的 PC 提供简单的域服务方法, 或者可以集中整个本地网络或组织的缓存.

剩余的存根 (stub) 只需要一个将执行递归请求的名称服务器地址列表. 这种类型的解析器大概需要配置文件中的信息, 因为它可能缺乏在域数据库中定位它的复杂性. 用户还需要验证列出的服务器是否会执行递归服务; 名称服务器可以自由拒绝为任何或所有客户端执行递归服务. 用户应咨询本地系统管理员以找到愿意执行该服务的名称服务器.

这种类型的服务有一些缺点. 由于递归请求可能需要任意时间来执行, 存根可能难以优化重传间隔以处理丢失的 UDP 数据包和死亡服务器; 如果名称服务器将重传解释为新请求, 过于热情的存根可能会使名称服务器过载. 使用 TCP 可能是一个答案, 但 TCP 可能会给主机的能力带来与真正解析器类似的负担.

5.3.2. 资源​

除了自身资源外, 解析器还可以共享访问本地名称服务器维护的区域. 这给解析器带来了更快访问的优势, 但解析器必须注意永远不要让缓存信息覆盖区域数据. 在本讨论中, "本地信息" 一词是指缓存和此类共享区域的并集, 理解是当两者都存在时, 权威数据始终优先于缓存数据.

以下解析器算法假设所有功能都已转换为通用查找功能, 并使用以下数据结构来表示解析器中正在进行的请求的状态:

SNAME​

我们正在搜索的域名.

STYPE​

搜索请求的 QTYPE.

SCLASS​

搜索请求的 QCLASS.

SLIST​

描述解析器当前尝试查询的名称服务器和区域的结构. 此结构跟踪解析器当前关于哪些名称服务器持有所需信息的最佳猜测; 当到达的信息改变猜测时, 它会更新. 此结构包括区域名称的等价物、区域的已知名称服务器、名称服务器的已知地址, 以及可用于建议下一个最佳尝试服务器的历史信息. 区域名称等价物是从根向下 SNAME 与正在查询的区域共有的标签数量的匹配计数; 这用作衡量解析器与 SNAME 的 "接近程度" 的指标.

SBELT​

与 SLIST 形式相同的 "安全带 (safety belt)" 结构, 从配置文件初始化, 列出当解析器没有任何本地信息来指导名称服务器选择时应使用的服务器. 匹配计数将为 -1, 表示没有已知匹配的标签.

CACHE​

存储先前响应结果的结构. 由于解析器负责丢弃 TTL 已过期的旧 RR, 大多数实现在将 RR 存储到缓存时将到达 RR 中指定的间隔转换为某种绝对时间. 解析器不是单独倒计时 TTL, 而是在搜索过程中遇到旧 RR 时忽略或丢弃它们, 或者在定期清扫以回收旧 RR 消耗的内存时丢弃它们.

5.3.3. 算法​

顶层算法有四个步骤:

  1. 查看答案是否在本地信息中, 如果是, 则将其返回给客户端.

  2. 找到最佳服务器进行询问.

  3. 向它们发送查询, 直到其中一个返回响应.

  4. 分析响应, 可能是:

    a. 如果响应回答了问题或包含名称错误, 将数据缓存并返回给客户端.

    b. 如果响应包含对其他服务器的更好委托, 缓存委托信息, 并转到步骤 2.

    c. 如果响应显示 CNAME 且这不是答案本身, 缓存 CNAME, 将 SNAME 更改为 CNAME RR 中的规范名称并转到步骤 1.

    d. 如果响应显示服务器故障或其他奇怪内容, 从 SLIST 中删除该服务器并返回步骤 3.

步骤 1: 搜索本地信息​

步骤 1 在缓存中搜索所需数据. 如果数据在缓存中, 则假定它对正常使用足够好. 一些解析器在用户界面有一个选项, 可以强制解析器忽略缓存数据并向权威服务器查询. 不推荐将此作为默认值. 如果解析器可以直接访问名称服务器的区域, 它应该检查所需数据是否以权威形式存在, 如果是, 则优先使用权威数据而不是缓存数据.

步骤 2: 找到最佳服务器​

步骤 2 查找名称服务器以请求所需数据. 一般策略是查找本地可用的名称服务器 RR, 从 SNAME 开始, 然后是 SNAME 的父域名, 祖父域名, 依此类推直到根. 因此, 如果 SNAME 是 Mockapetris.ISI.EDU, 此步骤将查找 Mockapetris.ISI.EDU 的 NS RR, 然后是 ISI.EDU, 然后是 EDU, 然后是 . (根). 找到名称服务器列表后, 解析器应根据偏好和预期性能对其进行排序. 然后解析器应按该顺序尝试联系服务器, 从最优先的开始.

步骤 3: 发送查询​

步骤 3 发出查询直到收到响应. 基本算法是循环遍历所有服务器的所有地址, 每次传输之间有超时. 解析器应尽可能使用 UDP 进行查询, 仅在响应被截断或解析器愿意承担 TCP 连接建立和拆除开销时才使用 TCP.

步骤 4: 分析响应​

步骤 4 分析响应. 解析器在解析响应时应高度谨慎. 它还应使用响应中的 ID 字段检查响应是否与其发送的查询匹配.