Tornado 中的 C-Ares 异步 DNS 解析器:`tornado.platform.caresresolver` 原理与弃用演进
2026/9/20 23:57:33 网站建设 项目流程

Tornado 中的 C-Ares 异步 DNS 解析器:tornado.platform.caresresolver原理与弃用演进

【免费下载链接】tornadoTornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed.项目地址: https://gitcode.com/gh_mirrors/to/tornado

tornado.platform.caresresolver是 Tornado 中基于 c-ares 库(及其 Python 绑定pycares)实现的异步 DNS 解析模块。本文以 docs/caresresolver.rst 为核心,结合 tornado/platform/caresresolver.py 源码、tornado/netutil.py 中的 Resolver 体系与相关测试,系统讲解CaresResolver的设计动机、底层工作机制、AF_INET/AF_UNSPEC地址族限制、接入方式,以及它在 Tornado 6.2 被弃用、7.0 移除的完整演进脉络,帮助读者理解"无线程、非阻塞" DNS 解析方案的取舍,并掌握迁移到默认线程解析器的路径。

一、模块定位:什么是CaresResolver

CaresResolver是 Tornado 提供的一种基于 c-ares 库的异步域名解析器。c-ares 是一个用 C 语言编写的异步 DNS 解析库,其 Python 绑定为pycares。该类的完整定义位于 tornado/platform/caresresolver.py:

class CaresResolver(Resolver): """Name resolver based on the c-ares library. This is a non-blocking and non-threaded resolver. It may not produce the same results as the system resolver, but can be used for non-blocking resolution when threads cannot be used. ... """

其核心设计目标在类文档中交代得很清楚,同时也在模块文档 docs/caresresolver.rst 中重申:

  • 非阻塞(non-blocking):解析过程不会阻塞事件循环;
  • 无线程(non-threaded):不依赖工作线程池,适合"线程不可用"的场景(例如某些受限运行环境或对线程使用有严格限制的程序);
  • 结果差异:它"可能不会产生与系统解析器相同的结果",这是使用该类前必须接受的取舍。

从源码结构看,CaresResolver直接继承自tornado.netutil.Resolver(tornado/netutil.py),而Resolver是一个Configurable类,即 Tornado 中"可插拔解析器体系"的抽象接口。Resolver.resolve()的约定返回值是"一个(family, address)对的列表,其中 address 是可直接传给socket.connect()的元组(IPv4 为(host, port),IPv6 可能带额外字段)"。

二、核心 API 与解析流程

2.1initialize():建立 c-ares 通道

def initialize(self) -> None: self.io_loop = IOLoop.current() self.channel = pycares.Channel(sock_state_cb=self._sock_state_cb) self.fds: dict[int, int] = {}

(tornado/platform/caresresolver.py)

这里创建了一个pycares.Channel(c-ares 的解析通道对象),并注册sock_state_cb回调——这是 c-ares 向事件循环报告"哪些 socket 处于可读/可写状态"的桥梁,下文第三节详述。在 Tornado 5.0 之前,initialize还接受io_loop参数,5.0 起该参数(自 4.1 已弃用)被彻底移除,统一使用IOLoop.current()

2.2resolve():宿主名到地址元组的转换

resolve()是一个@gen.coroutine协程方法(tornado/platform/caresresolver.py),签名与Resolver接口一致:

@gen.coroutine def resolve(self, host: str, port: int, family: int = 0) -> ...:

其执行流程可以拆解为四个步骤:

  1. IP 直通短路:若host本身就是合法 IP 字符串(经tornado.netutil.is_valid_ip校验),则直接跳过 DNS 查询,以[host]作为解析结果。is_valid_ip的实现位于 tornado/netutil.py,它通过socket.getaddrinfo(..., AI_NUMERICHOST)同时校验 IPv4 与 IPv6,并额外处理了空串、\x00截断、超长输入(>63 字符触发 idna UnicodeError)等边界情况。
  2. 异步查询:对非 IP 的宿主名,调用self.channel.gethostbyname(host, family, callback)发起 c-ares 异步查询,并用一个Future承接回调结果:
fut: Future[tuple[Any, Any]] = Future() self.channel.gethostbyname( host, family, lambda result, error: fut.set_result((result, error)) ) result, error = yield fut

这里刻意采用gethostbyname(而非gethostbyname4),源码注释给出了原因:gethostbyname不支持把回调作为关键字参数传入,因此用位置参数包裹。 3.错误归一化:若 c-ares 返回错误码,则抛出OSError,错误信息中包含 c-ares 错误码及其可读文本:

raise OSError( "C-Ares returned error %s: %s while resolving %s" % (error, pycares.errno.strerror(error), host) )
  1. 结果标准化:遍历返回的地址,根据地址中是否含.:判定AF_INET/AF_INET6,组装成(address_family, (address, port))元组列表;同时校验请求的family与返回族是否一致,不一致则抛出OSError"Requested socket family %d but got %d")。

该返回格式与Resolver接口约定(tornado/netutil.py)完全一致,可直接作为socket.connect/IOStream.connect的输入,因此CaresResolver可以无缝嵌入 Tornado 的连接管线。

三、底层机制:c-ares 如何与 Tornado 事件循环协作

CaresResolver之所以能做到"无线程、非阻塞",关键在于把 c-ares 自己的 socket 事件接入 Tornado 的IOLoop。这一过程由一对回调完成:

_sock_state_cb(fd, readable, writable)(tornado/platform/caresresolver.py)——c-ares 在内部 socket 状态变化时调用它:

state = (IOLoop.READ if readable else 0) | (IOLoop.WRITE if writable else 0) if not state: self.io_loop.remove_handler(fd) del self.fds[fd] elif fd in self.fds: self.io_loop.update_handler(fd, state) self.fds[fd] = state else: self.io_loop.add_handler(fd, self._handle_events, state) self.fds[fd] = state

逻辑为:状态归零则从IOLoop移除该 fd;fd 已注册则更新监听事件(READ/WRITE);否则新增监听。self.fds字典用于跟踪当前已注册的 fd 与其监听状态。

_handle_events(fd, events)(tornado/platform/caresresolver.py)——IOLoop在 fd 就绪时调用它,把事件喂回 c-ares:

read_fd = pycares.ARES_SOCKET_BAD write_fd = pycares.ARES_SOCKET_BAD if events & IOLoop.READ: read_fd = fd if events & IOLoop.WRITE: write_fd = fd self.channel.process_fd(read_fd, write_fd)

process_fd是 c-ares 的"驱动函数":告诉 c-ares 哪个 fd 可读、哪个 fd 可写,c-ares 随即完成收发 DNS 报文、超时处理、触发查询回调等内部工作。整个过程中没有任何线程阻塞等待,DNS 报文收发全部由IOLoop的事件驱动完成——这正是"非阻塞且非线程化"的源码级答案。

四、地址族限制:为什么只推荐AF_INET

原文档明确指出 c-ares 的一个关键限制:

c-ares fails to resolve some names whenfamilyisAF_UNSPEC, so it is only recommended for use inAF_INET(i.e. IPv4).

源码中的表述更进一步(tornado/platform/caresresolver.py):

pycareswill not return a mix ofAF_INETandAF_INET6whenfamilyisAF_UNSPEC, so it is only recommended for use inAF_INET.

这意味着:当请求AF_UNSPEC(即"IPv4/IPv6 皆可")时,pycares 不会同时返回两类地址的混合结果,某些域名甚至会解析失败。因此:

  • 仅推荐在AF_INET(IPv4)场景使用CaresResolver
  • tornado.simple_httpclient默认使用AF_INET,与CaresResolver契合;
  • 但其他库可能默认AF_UNSPEC,此时接入CaresResolver会遇到解析异常。

此外还有一个可观察到的行为差异:在 tornado/test/websocket_test.py 的测试注释中提到 "CaresResolver may return ipv6-only results for localhost",即CaresResolverlocalhost可能只返回 IPv6 结果,这与系统解析器返回127.0.0.1的行为不同——再次印证了"结果可能与系统解析器不一致"的警告,也是测试用例特意跳过 macOS / Windows 的原因之一。

五、接入方式:如何配置并投入使用

5.1 通过Resolver.configure全局替换

由于ResolverConfigurable类,可借助其configure类方法在全局启用CaresResolver,方式与 tornado/netutil.py 文档中给出的示例一致:

from tornado.netutil import Resolver Resolver.configure('tornado.platform.caresresolver.CaresResolver')

配置后,凡是经由Resolver()获取解析器的地方(如AsyncHTTPClientTCPClient等)都会使用 c-ares 进行解析。

5.2 在 HTTP 客户端中显式传入

tornado.simple_httpclient的构造函数接受resolver参数(tornado/simple_httpclient.py),未指定时内部创建默认Resolver()并自持其生命周期(own_resolver=True);显式传入时则由调用方负责解析器的关闭。同时,若提供了hostname_mapping,解析器还会被OverrideResolver包装,实现本地主机名重定向——这对测试环境非常有用。

5.3 直接实例化调用

也可以直接构造并调用(与 maint/scripts/test_resolvers.py 中的用法一致):

import socket from tornado.ioloop import IOLoop from tornado.platform.caresresolver import CaresResolver async def main(): resolver = CaresResolver() result = await resolver.resolve("example.com", 80, socket.AF_INET) print(result) # [(socket.AF_INET, ('93.184.216.34', 80)), ...] resolver.close() IOLoop.current().run_sync(main)

注意resolvefamily参数应显式传socket.AF_INET,以规避上文所述的AF_UNSPEC问题。

六、测试与验证:仓库中的证据

仓库对CaresResolver的验证主要落在 tornado/test/netutil_test.py。该测试类继承_ResolverTestMixin,通过统一的解析用例(含localhost、域名解析、bad_host错误场景)验证行为,但有以下值得注意的跳过条件与注释:

  • pycares未安装时跳过(pycares is None);
  • Windows 上跳过(pycares doesn't return loopback on windows);
  • macOS 上跳过(pycares doesn't return 127.0.0.1 on darwin);
  • 注释说明不测试错误场景的原因:部分 DNS 劫持型 ISP(如 Time Warner)在返回 NXDOMAIN 状态码的同时仍返回非空结果,多数解析器将其视为错误,而 c-ares 会返回这些结果,导致bad_host测试不可靠;且 c-ares 甚至会尝试解析带空格的畸形名称。

另外,maint/scripts/test_resolvers.py 提供了一个手工对比脚本:它会同时实例化默认Resolver()ThreadedResolverDefaultExecutorResolver以及(若安装了 pycares)CaresResolver,对localhostwww.google.com等真实域名逐一解析并打印结果,可通过--family=inet|inet6|unspec切换地址族(默认unspec)。该脚本需要联网,其文档注明"将在 Tornado 7.0 移除可插拔解析器体系时一并删除"。

七、版本演进与弃用:为什么你应改用默认解析器

CaresResolver的完整生命周期可以从仓库的发布记录中还原:

  • Tornado 3.0:引入tornado.platform.caresresolver.CaresResolver(见 docs/releases/v3.0.0.rst);
  • Tornado 5.0:移除io_loop构造参数(docs/releases/v5.0.0.rst),同时默认解析器从BlockingResolver演进为DefaultExecutorResolver
  • Tornado 6.2CaresResolver被正式标记为弃用,明确"将在 Tornado 7.0 移除"(docs/releases/v6.2.0.rst);同一版本中默认解析器已从DefaultExecutorResolver切换为DefaultLoopResolver——后者直接基于asyncio.loop.getaddrinfo(tornado/netutil.py),已不再需要线程或 c-ares;
  • pycares 版本约束CaresResolver要求pycares 4,且不会升级支持 pycares 5。仓库 tox.ini 中明确写道:"Pycares 5 has some backwards-incompatible changes that we don't support. And since CaresResolver is deprecated, I do not expect to fix it",因此测试环境将 pycares 固定为<5
  • Tornado 6.5:同属"无线程 DNS"阵营的TwistedResolver被直接删除(docs/releases/v6.5.0.rst),原因之一是 RFC 8482(针对 ANY 查询的 DNS 最小化响应)使其对大多数域名失效。发布说明将该类的主要使命——"提供无线程的非阻塞 DNS 解析"——转述给tornado.platform.caresresolver作为"次优选择",同时再次强调它同样已弃用,"大多数用户应切换到使用线程的默认解析器"。

迁移建议

如果你当前使用了CaresResolver,推荐的迁移路径是移除显式配置、回归默认解析器

  • 默认DefaultLoopResolver基于asyncio事件循环的getaddrinfo,无需额外安装 pycares,行为与系统解析器一致,兼容 IPv4/IPv6;
  • 若确需控制线程资源,可使用仍保留的ThreadedResolver(tornado/netutil.py,默认线程池 10,支持num_threads配置)或ExecutorResolver(可自定义 executor,tornado/netutil.py);
  • 需要本地 DNS 覆盖时,用OverrideResolver包装任意解析器,测试场景下非常实用(tornado/netutil.py)。

一句话总结:CaresResolver是 Tornado 在"既不想用线程、又需要非阻塞 DNS"这一历史约束下的精妙实现——它把 c-ares 的 socket 事件桥接到IOLoop,完成了无线程的非阻塞解析;但随着asyncio.getaddrinfo成为默认方案,它的历史使命已经完成,理解其原理的价值更多在于领会 Tornado 可插拔解析器体系的架构思想与事件驱动编程模式。

【免费下载链接】tornadoTornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed.项目地址: https://gitcode.com/gh_mirrors/to/tornado

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询