← Error 对象 Net 模块 →

Node.js DNS模块实战教程:域名解析、记录查询与Promise API全解

著
原创 2026-10-12 Node.js 已有人查阅

域名解析为什么值得单独讲

网络通信的起点是地址。应用程序面对的是api.example.com这样的域名,而TCP/IP协议栈需要的是192.0.2.1这样的IP地址。DNS就是这两者之间的翻译层。Node.js把这个翻译能力封装成了内置的dns模块,不需要引入axios、request之外的额外依赖。

很多开发者对DNS的认知停留在"把域名变成IP",但在项目中,DNS还承担着邮件路由发现、服务发现、域名所有权验证、反向解析等职责。理解dns模块的完整能力边界,能避免在需要查询MX记录时去调用外部HTTPAPI这类绕远路的做法。

代码号学习编程的过程中,DNS模块往往被跳过,因为它的API看起来简单。但简单背后有lookup与resolve两条不同的路径,选错方导致解析结果与预期不符。下面从模块导入开始,逐层拆解。

导入dns模块与API概览

dns是Node.js核心模块,无需npminstall,直接require即可。

// 代码号学习编程:导入 DNS 模块
const dns = require('node:dns');
console.log(dns);

控制台会打印出模块暴露的成员,包括lookup、lookupService、Resolver、getServers、setServers、resolve、resolve4、resolve6、reverse以及promisesgetter。

这里有一个细节值得注意:require('dns')和require('node:dns')在功能上等价,但node:前缀是Node.js官方推荐的写法,它明确标识这是一个内置模块,避免与node_modules中的同名包产生歧义。在新项目中建议统一使用node:前缀。

模块的API可以分成三组来理解:

  • 主机名与IP互转:lookup、lookupService、reverse

  • DNS记录查询:resolve系列,包括resolve4、resolve6、resolveMx、resolveTxt、resolveCname、resolveNs、resolveSoa、resolveSrv、resolveNaptr、resolvePtr

  • 解析器配置:getServers、setServers、Resolver类、getDefaultResultOrder、setDefaultResultOrder

Resolver类是一个容易被忽略但很有价值的设计。它允许创建独立的解析器实例,拥有自己的DNS服务器配置,不会影响全局解析行为。在多租户系统或需要针对特定域名使用特定DNS服务器的场景中,这个能力很关键。

lookup与resolve:两条路径的本质区别

这是DNS模块中最容易混淆的一对方法。两者都能把域名变成IP,但底层机制不同。

dns.lookup()调用的是操作系统的名称解析机制。它不直接向DNS服务器发送查询,而是委托给操作系统。这意味着它会读取/etc/hosts文件、使用操作系统的DNS缓存、遵循nsswitch.conf的配置顺序。对于本地开发环境,localhost能解析到127.0.0.1就是/etc/hosts在起作用。

dns.resolve()则绕过操作系统,直接向DNS服务器发送查询报文。它不读取/etc/hosts,不使用操作系统缓存,返回的是DNS记录本身。

对比维度 dns.lookup() dns.resolve()
解析机制 操作系统解析器 直接查询DNS服务器
读取hosts文件 支持 不支持
返回内容 IP地址与地址族 DNS记录(A、MX、TXT等)
记录类型支持 主要用于主机名到IP 支持多种记录类型
性能特征 通常较快,可利用系统缓存 需实际网络查询,可能较慢
典型场景 建立网络连接前的地址解析 查询特定DNS记录、诊断

个人经验:在需要判断"这个域名当前解析到哪个IP"时,用dns.resolve4更准确,因为它拿到的是权威DNS的答案。而dns.lookup拿到的可能是本地hosts文件里的覆盖值。曾经在排查一个"线上正常、本地异常"的问题时,就是因为本地hosts文件把测试域名指向了旧服务器,而dns.lookup忠实地返回了这个旧地址。

另一个实际考量是:dns.lookup在Node.js中是通过线程池执行的。当并发量很大时,线程池可能成为瓶颈。dns.resolve则是纯网络I/O,不受线程池限制。在高并发服务中,如果不需要hosts文件的支持,使用dns.resolve系列方法可以减轻线程池压力。

DNS记录类型与查询方法

DNS记录是域名配置的具体载体。Node.js为每种常见记录类型提供了对应的查询方法。

A记录:域名到IPv4地址的映射。

// 代码号学习编程:查询 A 记录
const dns = require('node:dns');

dns.resolve4('example.com', (err, addresses) => {
    if (err) {
        console.error(err);
        return;
    }
    console.log(addresses);
});

AAAA记录:域名到IPv6地址的映射,使用dns.resolve6()。

MX记录:邮件交换记录,指定负责接收邮件的服务器及其优先级。

// 代码号学习编程:查询 MX 记录
const dns = require('node:dns');

dns.resolveMx('google.com', (err, addresses) => {
    if (err) {
        console.error(err);
        return;
    }
    console.log(addresses);
});

返回结果是一个对象数组,每个对象包含exchange(邮件服务器域名)和priority(优先级,数值越小优先级越高)。

TXT记录:存储文本信息,常用于域名所有权验证、SPF邮件安全策略、DKIM密钥等。

CNAME记录:将一个域名别名指向另一个域名。查询CNAME时得到的是目标域名,而非IP。

NS记录:标识域名的权威名称服务器。

SOA记录:包含DNS区域的管理信息,如主名称服务器、管理员邮箱、序列号等。

SRV记录:指定服务的位置,包含主机名和端口号,常用于服务发现。

NAPTR记录:提供高级服务发现的重写规则,在VoIP和ENUM场景中使用。

PTR记录:反向DNS查询,从IP地址找到对应的主机名。使用dns.reverse()。

CAA记录:指定哪些证书颁发机构有权为该域名签发SSL/TLS证书。这是一个在证书自动化流程中越来越重要的记录类型。

项目实例:从域名解析到邮件路由检测

下面通过两个贴近实际的例子,把lookup和resolve的差异落到代码上。

实例一:获取域名的IP地址

// 代码号学习编程:使用 lookup 获取 IP 地址
const dns = require('node:dns');

dns.lookup('example.com', (err, address, family) => {
    if (err) {
        console.error(err);
        return;
    }
    console.log('address:', address);
    console.log('family:', family);
});

回调接收三个参数:错误对象、IP地址字符串、地址族(4表示IPv4,6表示IPv6)。family的值取决于操作系统解析器的返回结果。

实例二:检测域名的邮件服务器配置

// 代码号学习编程:查询 MX 记录
const dns = require('node:dns');

dns.resolveMx('google.com', (err, addresses) => {
    if (err) {
        console.error(err);
        return;
    }
    addresses
        .sort((a, b) => a.priority - b.priority)
        .forEach(record => {
            console.log(`优先级 ${record.priority}: ${record.exchange}`);
        });
});

对返回结果按优先级排序后输出,这在排查邮件投递问题时很实用。如果目标域名的MX记录为空或全部不可达,邮件就会投递失败。

PromiseAPI:更贴合现在写法

回调风格的DNS方法在嵌套场景下会变得难以阅读。dns/promises提供了Promise版本的API,可以直接配合async-await使用。

// 代码号学习编程:Promise 版 DNS 查询
const dns = require('node:dns/promises');

async function getIPAddress() {
    try {
        const res = await dns.lookup('example.com');
        console.log(res);
    } catch (err) {
        console.error('DNS lookup failed:', err);
    }
}

getIPAddress();

输出形如{address:'93.184.216.34',family:4}。await等待解析完成,结果直接可用,错误由catch统一处理。

也可以这样导入:

const dns = require('node:dns').promises;

两种写法等价。node:dns/promises是更明确的路径写法,推荐使用。

PromiseAPI同样覆盖了resolve4、resolveMx、resolveTxt、reverse等所有方法,命名与回调版本一致,只是返回值从回调参数变成了Promise的resolve值。

Resolver:自定义解析器实例

Resolver类允许创建独立的解析器,拥有自己的DNS服务器列表。这在这些场景中很有用:

  • 需要针对特定域名使用内部DNS服务器

  • 多租户环境中为不同租户配置不同解析器

  • 测试时需要指向模拟的DNS服务

// 代码号学习编程:自定义 Resolver
const { Resolver } = require('node:dns');
const resolver = new Resolver();

resolver.setServers(['8.8.8.8', '1.1.1.1']);

resolver.resolve4('example.com', (err, addresses) => {
    if (err) {
        console.error(err);
        return;
    }
    console.log(addresses);
});

setServers只影响这个resolver实例,不会改变全局的DNS服务器配置。这一点很重要:全局的dns.setServers()会影响整个进程,而Resolver实例提供了隔离性。

Resolver实例同样有Promise版本,通过require('node:dns/promises').Resolver获取。

全局DNS服务器配置与结果排序

dns.getServers()返回当前使用的DNS服务器IP列表。dns.setServers()可以修改这个列表。

// 代码号学习编程:查看当前 DNS 服务器
const dns = require('node:dns');
console.log(dns.getServers());

dns.getDefaultResultOrder()和dns.setDefaultResultOrder()控制dns.lookup()返回IPv4和IPv6地址的排序方式。支持的值包括'ipv4first'、'ipv6first'和'verbatim'。

在双栈环境中,这个设置会直接影响连接行为。如果应用优先使用IPv6但网络环境对IPv6支持不佳,连接超时会明显增加。setDefaultResultOrder('ipv4first')可以在不改动网络配置的情况下调整优先级。

个人建议:不要在生产代码中随意调用全局的setServers或setDefaultResultOrder。这些操作影响整个进程,可能干扰其他模块的DNS行为。需要自定义时,优先使用Resolver实例。

错误处理与常见

DNS查询失败是常态而非异常。网络抖动、域名不存在、DNS服务器无响应都会导致查询失败。回调风格的第一个参数是错误对象,需要优先判断。

常见的DNS错误码包括:

  • ENOTFOUND:域名不存在,无法解析

  • EAI_AGAIN:DNS服务器暂时不可用,通常是临时性故障

  • ESERVFAIL:DNS服务器返回失败

  • ETIMEOUT:查询超时

在代码号学习编程的实践中,ENOTFOUND和EAI_AGAIN需要区别对待。前者通常表示域名配置错误,重试没有意义;后者是临时故障,适合配合重试机制。

另一个是dns.lookup的family参数。默认情况下,它返回操作系统解析器给出的第一个结果。如果指定family:4,则只返回IPv4地址;指定family:6则只返回IPv6。在需要强制使用特定协议栈时,这个参数很有用。

现在框架中的惯用写法

在Express或Koa应用中,DNS查询通常出现在健康检查、服务发现或出站请求的地址解析环节。常见的封装方式是将Promise版DNS方法包装成带超时和重试的工具函数。

在NestJS中,DNS模块可以作为独立的Service注入,配合@nestjs/axios使用时,可以在请求拦截器中做自定义解析。不过大多数场景下,HTTP客户端自身已经处理了DNS解析,无需手动介入。手动调用DNS模块的时机通常是:需要获取记录本身的信息,而非仅仅完成一次网络请求。

本节课程知识要点

  1. dns是Node.js内置模块,无需安装,推荐使用node:dns前缀导入。

  2. dns.lookup()使用操作系统解析器,支持hosts文件;dns.resolve()直接查询DNS服务器,不支持hosts文件。

  3. dns.lookup()通过线程池执行,高并发下可能成为瓶颈;dns.resolve()是纯网络I/O。

  4. dns.resolve4()查A记录,resolve6()查AAAA记录,resolveMx()查邮件交换记录,resolveTxt()查文本记录,resolveCname()查别名记录。

  5. dns.reverse()执行反向DNS查询,从IP找主机名。

  6. dns/promises提供Promise版API,配合async-await使用。

  7. Resolver类创建独立解析器实例,setServers只影响该实例。

  8. 全局setServers和setDefaultResultOrder影响整个进程,生产环境慎用。

  9. 常见DNS错误码:ENOTFOUND、EAI_AGAIN、ESERVFAIL、ETIMEOUT。

  10. ENOTFOUND通常不需要重试,EAI_AGAIN适合配合重试机制。

练习与思考

  1. 编写一个函数,接收域名作为参数,同时查询其A记录和MX记录,返回一个包含两类记录的对象。如果任一查询失败,记录错误但继续执行另一项查询。

  2. 使用Resolver实例,将DNS服务器设置为8.8.8.8,查询example.com的TXT记录,并打印结果。

  3. 思考:为什么dns.lookup()在高并发场景下可能成为瓶颈?Node.js的线程池默认大小是多少?如何查看和调整?

  4. 编写一个带超时控制的Promise版DNS查询函数,超时时间设为3秒,超时后抛出明确的错误信息。

← Node.js错误处理全解析:Error对象、系统错误码与自定义异常教程 Node.js Net模块教程:TCP 服务端与客户端编程全解 →
分享笔记 (共有 篇笔记)
验证码:
微信公众号