域名解析为什么值得单独讲
网络通信的起点是地址。应用程序面对的是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模块的时机通常是:需要获取记录本身的信息,而非仅仅完成一次网络请求。
本节课程知识要点
-
dns是Node.js内置模块,无需安装,推荐使用node:dns前缀导入。 -
dns.lookup()使用操作系统解析器,支持hosts文件;dns.resolve()直接查询DNS服务器,不支持hosts文件。 -
dns.lookup()通过线程池执行,高并发下可能成为瓶颈;dns.resolve()是纯网络I/O。 -
dns.resolve4()查A记录,resolve6()查AAAA记录,resolveMx()查邮件交换记录,resolveTxt()查文本记录,resolveCname()查别名记录。 -
dns.reverse()执行反向DNS查询,从IP找主机名。 -
dns/promises提供Promise版API,配合async-await使用。 -
Resolver类创建独立解析器实例,setServers只影响该实例。 -
全局
setServers和setDefaultResultOrder影响整个进程,生产环境慎用。 -
常见DNS错误码:
ENOTFOUND、EAI_AGAIN、ESERVFAIL、ETIMEOUT。 -
ENOTFOUND通常不需要重试,EAI_AGAIN适合配合重试机制。
练习与思考
-
编写一个函数,接收域名作为参数,同时查询其A记录和MX记录,返回一个包含两类记录的对象。如果任一查询失败,记录错误但继续执行另一项查询。
-
使用
Resolver实例,将DNS服务器设置为8.8.8.8,查询example.com的TXT记录,并打印结果。 -
思考:为什么
dns.lookup()在高并发场景下可能成为瓶颈?Node.js的线程池默认大小是多少?如何查看和调整? -
编写一个带超时控制的Promise版DNS查询函数,超时时间设为3秒,超时后抛出明确的错误信息。