Nodejs开发进阶P-扩展网络模块DNS

本文是《Nodejs开发进阶-扩展网络模块》的第三个部分,主要内容是DNS。和前几个一样,这些内容都属于底层协议和技术,在实际工作中应用的场景和机会并不是很多,所以本文中主要做概念和了解性的探讨,不会过于深入。想要了解更深入详细的内容,可以参考nodejs官方技术文档的相关章节。

概述

DNS(Domain Name System)即域名系统,它最主要的用途(实际上远不止如此,后面有更详细的内容),是将URL中的主机名称,解析成为一个公共网络的IP地址,让应用程序可以使用TCP协议基于这个IP地址来进行工作。

理论上而言,DNS解析的功能,一般在操作系统的层面来实现,这个操作,对于应用程序而言是透明的,所以应用程序的开发通常无需考虑这个问题。

但由于可能是由于初始设计比较早的原因,或者nodejs的设计和开发人员,觉得有必要提供一定的可扩展性和灵活性,所以在nodejs中,包含和实现了DNS解析相关的功能,它通过DNS模块来提供。

从nodejs的文档来看,它提供的功能还是比较丰富而全面的。但由于这个模块笔者在日常开发中确实基本上没有使用,也可以猜想对于一般的开发者也不会接触太大,所以本文中,我们只做一个基本和简单的探讨,只讨论最常用的场景和最基础的功能。这些功能主要包括查找、解析、反向解析等等。

工作原理

这里先简单解释和说明一下DNS系统的工作原理。从整体上来看,DNS查询可以分为DNS客户端和DNS服务两个大的逻辑部分。DNS客户端使用用户提供的域名(如在浏览器中输入,或者配置在电子邮件客户端软件中的域名),向DNS服务发起查询请求,DNS服务响应查询的结果。当然,真实的情况并没有这么简单。

真实的情况是,要使DNS系统能够正常高效的工作,需要由多个不同层次和功能的服务器和网络系统构成的。它包括了一个DNS递归器(也称为递归解析器)、根服务器、顶级域名 (TLD) 名称服务器和权威服务器四种类型的DNS服务器。递归器就是表面上为客户端配置的那台DNS服务器,它负责接收客户端的查询请求,并使用递归的方式,来完成真实的解析过程。首先,递归器会解析域名查询请求的内容,分解出其中的顶级域名,并向根服务器查询所归属顶级域名服务器的地址;然后,递归器进一步向顶级域名服务器查询域名所归属的权威服务器地址;然后,向这个权威服务器查询并获得域名记录,这一般就是最后的查询结果,递归器会将其发送给客户端来完成整个查询过程。此外,DNS查询还有迭代的工作模式,即迭代器本身不执行查询,而是将客户端引向上级服务器来进行查询的方式。所有层次的DNS查询,都使用相同的网络协议和模式,DNS服务默认的端口是53。

可以看到,一个完全初始和完整的查询过程是比较复杂的,涉及很多的网络传输和信息查询工作。为了提高查询效率,DNS系统使用分层次的缓存机制,每次正确的查询都会被缓存到本层次的服务器上(甚至包括查询客户端),并且使用TTL记录来控制DNS记录的缓存时间,来保证实时有效。配合分层次的部署方式,可以大幅度减少每个层次查询所需要的操作。此外,DNS使用的传输协议是UDP,相对比较轻量和高效,其可靠性和可用性是在应用层面实现的。

DNS记录和解析类型

DNS协议是一个比较古早的协议,它基本上使用简单文本的方式来进行工作,比如域名信息存储和网络传输,这些内容通常称为DNS记录。一条记录,就是一行格式化的文本信息,它的结构基本如下:

<域名> <TTL设置> <类别> <类型> <后续数据字段大小> <解析内容,如IP>

如:

www.example.com. 69288 IN A 93.184.216.34

example.com. 69288 IN CNAME www.example.com

从这里我们也可以看到,DNS并不只是具有域名映射和查询功能,它还是一个可扩展的信息存储和查询系统,通过不同的信息记录类型和配置,来满足很多网络应用寻址甚至配置信息查询的需求。

以上面的记录为例,这里先有一条A记录(域名-IP映射),TTL是69288秒,它的类别是IN(internet,一般都是这个),解析内容是93.184.216.34,即这条记录表示应该将 www.example.com 这个域名指向IP地址93.184.216.34。还有一条CNAME记录,这是别名设置,可以将example.com 指向 www.example.com ,这时如果解析这个域名,它先指向 www.example.com 然后再解析到IP地址。

其他常见的DNS记录类型包括:

记录代码 用途和说明
A 默认,最常用的域名映射
AAAA A记录的IPv6版本,可以将域名映射到IPv6的地址
CNAME 别名,可以将一个域名映射到一个域名,如服务器多域名
NS 指定管辖当前域名的DNS服务器的地址
MX 用于电子邮件服务,指定当前域名的电子邮件服务器地址
PTR 反向解析
TXT 包含供用户或机器可读信息的文本(如Meta信息),一个域可以有多个TXT记录
SOA Start Of Authirity,标识DNS区域文件的开始
SRV 服务记录,为指定格式提供易于查询的统一格式记录,常用于应用某种服务的位置

在了解了DNS的基本原理,流程和操作等方面的内容之后,我们来看看在nodejs中,对于DNS协议操作的实现。它是基于dns这个模块的。相关的主要操作包括Lookup、Resolve等。

Lookup 查找

Lookup即查找,就是查询一个域名所对应的IP地址。按照官方文档的解释,这个功能在底层调用了系统级域名查询功能,所以实际上它应该是在操作系统基本实现的,在nodejs中,只是做了一个简单的调用和封装。所以这个能力的使用效果,完全取决于操作系统的实现方式,开发者是无法进行配置和控制的。

在nodejs的dns模块中,使用lookup方法来实现查找,使用的示例代码如下:

js 复制代码
const dns = require('node:dns');

dns.lookup('example.org', (err, address, family) => {
  console.log('address: %j family: IPv%s', address, family);
});
// address: "93.184.216.34" family: IPv4 

Resolve 解析

笔者理解,解析和查找的一个显著的区别,就是解析操作,是真正的DNS协议的实现。实际上,基于轻量化和效率的考虑,一般的DNS解析操作基于UDP协议,实际上是UDP协议的应用层的扩展协议。

一个完整的域名解析操作,需要先配置本地的DNS服务器地址;然后在需要进行域名解析的时候,一般会先查看本机hosts文件中的内容,然后查询本地缓存的DNS记录;如果都没有找到,才真正的通过网络向本地DNS服务器发起查询请求;本地DNS服务器收到查询请求后,会以递归的方式来执行这个查询,直到找到匹配结果,或者返回查询失败或者超时错误。

根据上面简单的原理和流程解释,我们可以看到在dns模块中,实现的示例代码如下:

js 复制代码
const { Resolver } = require('node:dns');
const resolver = new Resolver();
resolver.setServers(['4.4.4.4']);

// This request will use the server at 4.4.4.4, independent of global settings.
resolver.resolve4('example.org', (err, addresses) => {
  // ...
}); 

这段代码的要点包括:

  • 在解析开始之前,需要实例化一个解析器对象resolver
  • 可以为这个解析器对象,设置一个或者多个DNS服务器
  • 然后调用解析方法,来执行解析并从回调方法中获得解析结果
  • 可选解析IPv4地址或者IPv6的地址

类型解析

我们前面在讨论DNS原理的时候,已经了解到,通过不同的DNS记录类型,DNS还可以用于除了普通域名解析之外,很多其他类型的信息解析。在nodejs的dns模块中,它们是通过提供resolve* 系列函数来实现的。它们分别是:

  • resolver.resolve()
  • resolver.resolve4()
  • resolver.resolve6()
  • resolver.resolveAny()
  • resolver.resolveCaa()
  • resolver.resolveCname()
  • resolver.resolveMx()
  • resolver.resolveNaptr()
  • resolver.resolveNs()
  • resolver.resolvePtr()
  • resolver.resolveSoa()
  • resolver.resolveSrv()
  • resolver.resolveTxt()

坦率的讲,笔者并不是特别理解和认同这种"简单粗暴"的设计和实现方式。完全可以通过一个参数来控制解析的类型啊,参数可以使用枚举型,可以在规范的同时,保证很好的可扩展性。

Promise

dns模块设计了Promise的使用方式,它是通过dnsPromises对象实现的,参加下列示例代码:

js 复制代码
const dnsPromises = require('node:dns').promises;
dnsPromises.lookupService('127.0.0.1', 22).then((result) => {
  console.log(result.hostname, result.service);
  // Prints: localhost ssh
}); 

使用这个Promise对象,就可以使用.then().catch()这种方式来调用和执行异步解析操作了。

一个有意思的地方是,dnsPromises对象,提供了一个resolveAll()方法,可以获得当前这个域名相关的所有DNS记录的内容(但是注意却没有正常的resolveAll()这个方法),解析结果可以以数组方式呈现:

js 复制代码
[ { type: 'A', address: '127.0.0.1', ttl: 299 },
  { type: 'CNAME', value: 'example.com' },
  { type: 'MX', exchange: 'alt4.aspmx.l.example.com', priority: 50 },
  { type: 'NS', value: 'ns1.example.com' },
  { type: 'TXT', entries: [ 'v=spf1 include:_spf.example.com ~all' ] },
  { type: 'SOA',
    nsname: 'ns1.example.com',
    hostmaster: 'admin.example.com',
    serial: 156696742,
    refresh: 900,
    retry: 900,
    expire: 1800,
    minttl: 60 } ] 

这个结果也可以说明,不同的解析方法和类型,解析结果的数据的组成结构,也可能是不一样的,有很多和DNS类型相关的结构,需要开发者使用时进行了解和掌握。

反向解析

可以使用resolver.reverse(ip)或者dnsPromises.reverse(ip)等方法,来执行反向解析。例如:

js 复制代码
const 
dnsPromises = require('node:dns').promises,
IP = "204.79.197.200";  // bing.com
//"8.8.8.8";
//"110.242.68.66";

dnsPromises
.reverse(IP)
.then(r=>{
    console.log(r);
})
.catch(err=>{
    console.error(err);
})

// 结果是
// [ 'a-0001.a-msedge.net' ]

获得的结果是一个数组,这很好理解,因为多个域名可以指向同一个IP地址。但不好理解的地方是,这个IP地址的原始域名,应该是"bing.com"。顺便提一下,8.8.8.8 我们都知道是google提供的域名解析服务,但它的反向解析内容是 "dns.google",这是一个合理的域名吗?

DNS错误

nodejs官方技术文档,还提供了dns的错误代码,从中我们就可以理解DNS技术的复杂性,也可以作为我们设计一个系统,相关错误处理的思维框架。

  • dns.NODATA: DNS服务器返回无数据的结果
  • dns.FORMERR: DNS服务器声明查询请求格式错误
  • dns.SERVFAIL: DNS服务器返回一般错误
  • dns.NOTFOUND: 域名记录未找到
  • dns.NOTIMP: DNS服务器没有实现请求的操作
  • dns.REFUSED: DNS服务器拒绝查询
  • dns.BADQUERY: DNS查询格式错误
  • dns.BADNAME: 主机名称格式错误
  • dns.BADFAMILY: 不支持的地址家族
  • dns.BADRESP: DNS响应格式错误
  • dns.CONNREFUSED: 无法联系DNS服务器
  • dns.TIMEOUT: DNS服务器连接超时
  • dns.EOF: 文件结束
  • dns.FILE: 读取文件错误
  • dns.NOMEM: 内存溢出
  • dns.DESTRUCTION: 通道被销毁
  • dns.BADSTR: 错误的字符串
  • dns.BADFLAGS: 非法标记
  • dns.NONAME: 给定的主机名IP地址格式不合法
  • dns.BADHINTS: 错误的提示标记
  • dns.NOTINITIALIZED: c-ares库未完成初始化
  • dns.LOADIPHLPAPI: iphlpapi.dll加载错误
  • dns.ADDRGETNETWORKPARAMS: 无法找到GetNetworkParams方法
  • dns.CANCELLED: DNS查询取消

经过查询实现代码,知晓到它并没有使用如HTTP状态码那样的整数常数,而是使用ASCII文本短语就是字符串来表示这些内容。

小结

本文谈到了如何在nodejs中实现DNS的操作和其DNS模块,包括了DNS的基本工作和操作原理、记录类型、相关方法和参数,错误信息等内容。

相关推荐
刘大辉在路上2 小时前
突发!!!GitLab停止为中国大陆、港澳地区提供服务,60天内需迁移账号否则将被删除
git·后端·gitlab·版本管理·源代码管理
真的很上进3 小时前
如何借助 Babel+TS+ESLint 构建现代 JS 工程环境?
java·前端·javascript·css·react.js·vue·html
追逐时光者4 小时前
免费、简单、直观的数据库设计工具和 SQL 生成器
后端·mysql
初晴~5 小时前
【Redis分布式锁】高并发场景下秒杀业务的实现思路(集群模式)
java·数据库·redis·分布式·后端·spring·
盖世英雄酱581365 小时前
InnoDB 的页分裂和页合并
数据库·后端
滚雪球~5 小时前
npm error code ETIMEDOUT
前端·npm·node.js
沙漏无语5 小时前
npm : 无法加载文件 D:\Nodejs\node_global\npm.ps1,因为在此系统上禁止运行脚本
前端·npm·node.js
小_太_阳5 小时前
Scala_【2】变量和数据类型
开发语言·后端·scala·intellij-idea
直裾5 小时前
scala借阅图书保存记录(三)
开发语言·后端·scala