用 Node.js 手寫一個(gè) DNS 服務(wù)器

以下文章來(lái)源于神光的編程秘籍 ,作者神說要有光

DNS 是實(shí)現(xiàn)域名到 IP 轉(zhuǎn)換的網(wǎng)絡(luò)協(xié)議,當(dāng)訪問網(wǎng)頁(yè)的時(shí)候,瀏覽器首先會(huì)通過 DNS 協(xié)議把域名轉(zhuǎn)換為 IP,然后再向這個(gè) IP 發(fā)送 HTTP 請(qǐng)求。

DNS 是我們整天在用的協(xié)議,不知道大家是否了解它的實(shí)現(xiàn)原理呢?

這篇文章我們就來(lái)深入下 DNS 的原理,并且用 Node.js 手寫一個(gè) DNS 服務(wù)器吧。

DNS 的原理
不知道大家有沒有考慮過,為什么要有域名?

我們知道,標(biāo)識(shí)計(jì)算機(jī)是使用 IP 地址,IPv4 有 32 位,IPv6 有 128 位。

IPv4 一般用十進(jìn)制表示:

192.10.128.240
IPv6 太長(zhǎng)了,一般是用十六進(jìn)制表示:

3C0B:0000:2667:BC2F:0000:0000:4669:AB4D
不管是 IPv4 還是 IPv6,這串?dāng)?shù)字都太難記了,如果訪問網(wǎng)頁(yè)要輸入這樣一串?dāng)?shù)字也太麻煩了。

而且 IP 也不是固定的,萬(wàn)一機(jī)房做了遷移之類的,那 IP 也會(huì)變。

怎么通過一種既好記又不限制為固定 IP 的方式來(lái)訪問目標(biāo)服務(wù)器呢?

可以起一個(gè)名字,客戶端不通過 IP,而是通過這個(gè)名字來(lái)訪問目標(biāo)機(jī)器。

名字和 IP 的綁定關(guān)系是可以變的,每次訪問都要經(jīng)歷一次解析名字對(duì)應(yīng)的 IP 的過程。

這個(gè)名字就叫做域名。

那怎么維護(hù)這個(gè)域名和 IP 的映射關(guān)系呢?

最簡(jiǎn)單的方式就是在一個(gè)文件里記錄下所有的域名和 IP 的對(duì)應(yīng)關(guān)系,每次解析域名的時(shí)候都到這個(gè)文件里查一下。

最開始確實(shí)是這么設(shè)計(jì)的,這樣的文件叫做 hosts 文件,記錄了世界上所有的主機(jī)(host)。

那時(shí)候全世界也沒多少機(jī)器,所以這樣的方式是可行的。

當(dāng)然,這個(gè) hosts 的配置是統(tǒng)一維護(hù)的,當(dāng)新的主機(jī)需要聯(lián)網(wǎng)的話就到這里注冊(cè)一下自己的域名和 IP。其他機(jī)器拉取下最新的配置就能訪問到這臺(tái)主機(jī)了。

但是隨著機(jī)器的增多,這種方式就不太行了,有兩個(gè)突出的問題:

全世界都從某一臺(tái)機(jī)器來(lái)同步配置,這臺(tái)機(jī)器壓力會(huì)太大。

當(dāng)域名多了以后,命名上很容易沖突。

所以域名服務(wù)器得是分布式的,通過多臺(tái)服務(wù)器來(lái)提供服務(wù),并且最好還能通過命名空間來(lái)劃分,減少命名沖突。

因此才產(chǎn)生了域名,例如 baidu.com 這個(gè) com 就是一個(gè)域,叫頂級(jí)域,baidu 就是 com 域的二級(jí)域。

這樣如果再有一個(gè) baidu.xyz 也是可以的,因?yàn)?xyz 和 com 是不同的域,之下有獨(dú)立的命名空間。

這樣就減少了命名沖突。


分布式的話就要?jiǎng)澐质裁从蛎屖裁捶?wù)器來(lái)處理,把請(qǐng)求的壓力分散開。

很容易想到的是頂級(jí)域、二級(jí)域、三級(jí)域分別放到不同的服務(wù)器來(lái)解析。

所有的頂級(jí)域服務(wù)器也有個(gè)目錄,叫做根域名服務(wù)器。

這樣查詢某個(gè)域名的 IP 時(shí)就先向根域名服務(wù)器查一下頂級(jí)域的地址,然后有二級(jí)域的話再查下對(duì)應(yīng)服務(wù)器的地址,一層層查,直到查到最終的 IP。

當(dāng)然,之前的 hosts 的方式也沒有完全廢棄,還是會(huì)先查一下 hosts,如果查不到的話再去請(qǐng)求域名服務(wù)器。

也就是這樣的:



比如查 www.baidu.com 這個(gè)域名的 IP,就先查本地 hosts,沒有查到的話就向根域名服務(wù)器查 com 域的通用頂級(jí)域名服務(wù)器的地址,之后再向這個(gè)頂級(jí)域名服務(wù)器查詢 baidu.com 二級(jí)域名服務(wù)器的地址,這樣一層層查,直到查到最終的 IP。

這樣就通過分布式的方式來(lái)分散了服務(wù)器的壓力。

但是這樣設(shè)計(jì)還是有問題的,每一級(jí)域一個(gè)服務(wù)器,如果域名的層次過多,那么就要往返查詢好多次,效率也不高。

所以 DNS(Domain Name System)只分了三級(jí)域名服務(wù)器:

根域名服務(wù)器:記錄著所有頂級(jí)域名服務(wù)器的地址,是域名解析的入口
頂級(jí)域名服務(wù)器:記錄著各個(gè)二級(jí)域名對(duì)應(yīng)的服務(wù)器的地址
權(quán)威域名服務(wù)器:該域下二級(jí)、三級(jí)甚至更多級(jí)的域名都在這里解析
其實(shí)就是把二、三、四、五甚至更多級(jí)的域名都合并在一個(gè)服務(wù)器解析了,叫做權(quán)威域名服務(wù)器(Authoritative Domain Name Server)。

這樣既通過分布式減輕了服務(wù)器的壓力,又避免了層數(shù)過多導(dǎo)致的解析慢。



當(dāng)然,每次查詢還是比較耗時(shí)的,查詢完之后要把結(jié)果緩存下來(lái),并且設(shè)置一個(gè)過期時(shí)間,域名解析記錄在 DNS 服務(wù)器上的緩存時(shí)間叫做 TTL(Time-To-Live)。

但現(xiàn)在只是在某一臺(tái)機(jī)器上緩存了這個(gè)解析結(jié)果,可能某個(gè)區(qū)域的其他機(jī)器在訪問的時(shí)候還是需要解析的。

所以 DNS 設(shè)計(jì)了一層本地域名服務(wù)器,由它來(lái)負(fù)責(zé)完成域名的解析,并且把結(jié)果緩存下來(lái)。

這樣某臺(tái)具體的機(jī)器只要向這個(gè)本地域名服務(wù)器發(fā)請(qǐng)求就可以了,而且解析結(jié)果其他機(jī)器也可以直接用。



這樣的本地域名服務(wù)器是移動(dòng)、聯(lián)通等 ISP(因特網(wǎng)服務(wù)提供商)提供的,一般在每個(gè)城市都有一個(gè)。某臺(tái)機(jī)器訪問了某個(gè)域名,解析之后會(huì)把結(jié)果緩存下來(lái),其他機(jī)器訪問這個(gè)域名就不用再次解析了。

這個(gè)本地域名服務(wù)器的地址是可以修改的,在 mac 里可以打開系統(tǒng)偏好設(shè)置 --> 網(wǎng)絡(luò) --> 高級(jí) --> DNS來(lái)查看和修改本地域名服務(wù)器的地址。



這就是 DNS 的原理。

不知道大家看到本地域名服務(wù)器的配置可以修改的時(shí)候,是否有自己實(shí)現(xiàn)一個(gè) DNS 服務(wù)器的沖動(dòng)。

確實(shí),這個(gè) DNS 服務(wù)器完全可以自己實(shí)現(xiàn),接下來(lái)我們就用 Node.js 實(shí)現(xiàn)一下。

我們先來(lái)分析下思路:

DNS 服務(wù)器實(shí)現(xiàn)思路分析
DNS 是應(yīng)用層的協(xié)議,協(xié)議內(nèi)容的傳輸還是要通過傳輸層的 UDP 或者 TCP。

我們知道,TCP 會(huì)先三次握手建立連接,之后再發(fā)送數(shù)據(jù)包,并且丟失了會(huì)重傳,確保數(shù)據(jù)按順序送達(dá)。

它適合一些需要進(jìn)行多次請(qǐng)求、響應(yīng)的通信,因?yàn)檫@種通信需要保證處理順序,典型的就是 HTTP。

但這樣的可靠性保障也犧牲了一定的性能,效率比較低。

而 UDP 是不建立連接,直接發(fā)送數(shù)據(jù)報(bào)給對(duì)方,效率比較高。適合一些不需要保證順序的場(chǎng)景。

顯然,DNS 的每次查詢請(qǐng)求都是獨(dú)立的,沒有啥順序的要求,比較適合 UDP。

所以我們需要用 Node.js 起一個(gè) UDP 的服務(wù)來(lái)接收客戶端的 DNS 數(shù)據(jù)報(bào),自己實(shí)現(xiàn)域名的解析,或者轉(zhuǎn)發(fā)給其他域名服務(wù)器來(lái)處理。之后發(fā)送解析的結(jié)果給客戶端。

創(chuàng)建 UDP 服務(wù)和發(fā)送數(shù)據(jù)使用 Node.js 的 dgram 這個(gè)包。

類似這樣:

const dgram = require('dgram');

const server = dgram.createSocket('udp4')

server.on('message', (msg, rinfo) => {
    // 處理 DNS 協(xié)議的消息
})

server.on('error', (err) => {
    // 處理錯(cuò)誤
})
 
server.on('listening', () => {
    // 當(dāng)接收方地址確定時(shí)
});

server.bind(53);
具體代碼后面再細(xì)講,這里知道接收 DNS 協(xié)議數(shù)據(jù)需要啟 UDP 服務(wù)就行。

DNS 服務(wù)器上存儲(chǔ)著域名和 IP 對(duì)應(yīng)關(guān)系的記錄,這些記錄有 4 種類型:

A:域名對(duì)應(yīng)的 IP
CNAME:域名對(duì)應(yīng)的別名
MX:郵件名后綴對(duì)應(yīng)的域名或者 IP
NS:域名需要去另一個(gè) DNS 服務(wù)器解析
PTR:IP 對(duì)應(yīng)的域名
其實(shí)還是很容易理解的:

類型 A 就是查詢到了域名對(duì)應(yīng)的 IP,可以直接告訴客戶端。

類型 NS 是需要去另一臺(tái) DNS 服務(wù)器做解析,比如頂級(jí)域名服務(wù)器需要進(jìn)一步去權(quán)威域名服務(wù)器解析。

CNAME 是給當(dāng)前域名起個(gè)別名,兩個(gè)域名會(huì)解析到同樣的 IP。

PTR 是由 IP 查詢域名用的,DNS 是支持反向解析的

而 MX 是郵箱對(duì)應(yīng)的域名或者 IP,用于類似 @xxx.com 的郵件地址的解析。

當(dāng) DNS 服務(wù)器接收到 DNS 協(xié)議數(shù)據(jù)就會(huì)去這個(gè)記錄表里查找對(duì)應(yīng)的記錄,然后通過 DNS 協(xié)議的格式返回。

那 DNS 協(xié)議格式是怎么樣的呢?

大概是這樣:








內(nèi)容還是挺多的,我們挑幾個(gè)重點(diǎn)來(lái)看一下:

Transction ID 是關(guān)聯(lián)請(qǐng)求和響應(yīng)用的。

Flags 是一些標(biāo)志位:



比如 QR 是標(biāo)識(shí)是請(qǐng)求還是響應(yīng)。OPCODE 是標(biāo)識(shí)是正向查詢,也就是域名到 IP,還是反向查詢,也就是 IP 到域名。

再后面分別是問題的數(shù)量、回答的數(shù)量、授權(quán)的數(shù)量、附加信息的數(shù)量。

之后是問題、回答等的具體內(nèi)容。

問題部分的格式是這樣的:



首先是查詢的名字,比如 baidu.com,然后是查詢的類型,就是上面說的那些 A、NS、CNAME、PTR 等類型。最后一個(gè)查詢類一般都是 1,表示 internet 數(shù)據(jù)。

回答的格式是這樣的:

Name 也是查詢的域名,Type 是 A、NS、CNAME、PTR 等,Class 也是和問題部分一樣,都是 1。

然后還要指定 Time to live,也就是這條解析記錄要緩存多長(zhǎng)時(shí)間。DNS 就是通過這個(gè)來(lái)控制客戶端、本地 DNS 服務(wù)器的緩存過期時(shí)間的。

最后就是數(shù)據(jù)的長(zhǎng)度和內(nèi)容了。

這就是 DNS 協(xié)議的格式。

我們知道了如何啟 UDP 的服務(wù),知道了接收到的 DNS 協(xié)議數(shù)據(jù)是什么格式的,那么就可以動(dòng)手實(shí)現(xiàn) DNS 服務(wù)器了。解析出問題部分的域名,然后自己實(shí)現(xiàn)解析,并返回對(duì)應(yīng)的響應(yīng)數(shù)據(jù)。

大概理清了原理,我們來(lái)寫下代碼:

手寫 DNS 服務(wù)器
首先,我們創(chuàng)建 UDP 的服務(wù),監(jiān)聽 53 號(hào)端口,這是 DNS 協(xié)議的默認(rèn)端口。

const dgram = require('dgram')

const server = dgram.createSocket('udp4')

server.on('message', (msg, rinfo) => {
    console.log(msg)
});

server.on('error', (err) => {
    console.log(`server error:\n${err.stack}`)
    server.close()
})
 
server.on('listening', () => {
    const address = server.address()
    console.log(`server listening ${address.address}:${address.port}`)
})
 
server.bind(53)
通過 dgram 模塊創(chuàng)建 UDP 服務(wù),啟動(dòng)在 53 端口,處理開始監(jiān)聽的事件,打印服務(wù)器地址和端口,處理錯(cuò)誤的事件,打印錯(cuò)誤堆棧。收到消息時(shí)直接打印。



修改系統(tǒng)偏好設(shè)置的本地 DNS 服務(wù)器地址指向本機(jī):



這樣再訪問網(wǎng)頁(yè)的時(shí)候,我們的服務(wù)控制臺(tái)就會(huì)打印收到的消息了:



一堆 Buffer 數(shù)據(jù),這就是 DNS 協(xié)議的消息。

我們從中把查詢的域名解析出來(lái)打印下,也就是這部分:



問題前面的部分有 12 個(gè)字節(jié),所以我們截取一下再 parse:

server.on('message', (msg, rinfo) => {
  const host = parseHost(msg.subarray(12))
  console.log(`query: ${host}`)
})
msg 是 Buffer 類型,是 Uint8Array 的子類型,也就是無(wú)符號(hào)整型。(整型存儲(chǔ)的時(shí)候可以帶符號(hào)也可以不帶符號(hào),不帶符號(hào)的話可以存儲(chǔ)的數(shù)字會(huì)大一倍。)

調(diào)用它的 subarray 方法,截取掉前面 12 個(gè)字節(jié)。

然后解析問題部分:



問題的最開始就是域名,我們只要把域名解析出來(lái)就行。

我們表示域名是通過 . 來(lái)區(qū)分,但是存儲(chǔ)的時(shí)候不是,是通過

當(dāng)前域長(zhǎng)度 + 當(dāng)前域內(nèi)容 + 當(dāng)前域長(zhǎng)度 + 當(dāng)前域內(nèi)容 + 當(dāng)前域長(zhǎng)度 + 當(dāng)前域內(nèi)容 + 0

這樣的格式,以 0 作為域名的結(jié)束。

所以解析邏輯是這樣的:

function parseHost(msg) {
  let num = msg.readUInt8(0);
  let offset = 1;
  let host = "";
  while (num !== 0) {
    host += msg.subarray(offset, offset + num).toString();
    offset += num;

    num = msg.readUInt8(offset);
    offset += 1;

    if (num !== 0) {
      host += '.'
    }
  }
  return host
}
通過 Buffer 的 readUInt8 方法來(lái)讀取一個(gè)無(wú)符號(hào)整數(shù),通過 Buffer 的 subarray 方法來(lái)截取某一段內(nèi)容。

這兩個(gè)方法都要指定 offet,也就是從哪里開始。

我們先讀取一個(gè)數(shù)字,也就是當(dāng)前域的長(zhǎng)度,然后讀這段長(zhǎng)度的內(nèi)容,然后繼續(xù)讀下一段,直到讀到 0,代表域名結(jié)束。

把中間的這些域通過 . 連接起來(lái)。比如 3 www 5 baidu 3 com 處理之后就是 www.baidu.com。

之后我們重啟下服務(wù)器測(cè)試下效果:



我們成功的從 DNS 協(xié)議數(shù)據(jù)中把 query 的域名解析了出來(lái)!

解析 query 部分只是第一步,接下來(lái)還要返回對(duì)應(yīng)的響應(yīng)。

這里我們只自己處理一部分域名,其余的域名還是交給別的本地 DNS 服務(wù)器處理:

server.on('message', (msg, rinfo) => {
    const host = parseHost(msg.subarray(12))
    console.log(`query: ${host}`);

    if (/guangguangguang/.test(host)) {
        resolve(msg, rinfo)
    } else {
        forward(msg, rinfo)
    }
});
解析出的域名如果包含 guangguangguang,那就自己處理,構(gòu)造對(duì)應(yīng)的 DNS 協(xié)議消息返回。

否則就轉(zhuǎn)發(fā)到別的本地 DNS 服務(wù)器處理,把結(jié)果返回給客戶端。

先實(shí)現(xiàn) forward 部分:

轉(zhuǎn)發(fā)到別的 DNS 服務(wù)器,那就是創(chuàng)建一個(gè) UDP 的客戶端,把收到的消息傳給它,收到消息后再轉(zhuǎn)給客戶端。

也就是這樣的:

function forward(msg, rinfo) {
    const client = dgram.createSocket('udp4');

    client.on('error', (err) => {
      console.log(`client error:\n${err.stack}`);
      client.close();
    });

    client.on('message', (fbMsg, fbRinfo) => {
      server.send(fbMsg, rinfo.port, rinfo.address, (err) => {
        err && console.log(err)
      })
      client.close();
    });

    client.send(msg, 53, '192.168.199.1', (err) => {
      if (err) {
        console.log(err)
        client.close()
      }
    });
}





通過 dgram.createSocket 創(chuàng)建一個(gè) UDP 客戶端,參數(shù)的 udp4 代表是 IPv4 的地址。

處理錯(cuò)誤、監(jiān)聽消息,把 msg 轉(zhuǎn)發(fā)給目標(biāo) DNS 服務(wù)器(這里的 DNS 服務(wù)器地址大家可以換成別的)。

收到返回的消息之后傳遞給客戶端。

客戶端的 ip 和端口是通過參數(shù)傳進(jìn)來(lái)的。

這樣就實(shí)現(xiàn)了 DNS 協(xié)議的中轉(zhuǎn),我們先測(cè)試下現(xiàn)在的效果。

使用 nslookup 命令來(lái)查詢某個(gè)域名的地址:



可以看到,查詢 baidu.com 是能拿到對(duì)應(yīng)的 IP 地址的,在瀏覽器里也就可以訪問。

而 guangguangguang.ddd.com 沒有查找到對(duì)應(yīng)的 IP。

接下來(lái)實(shí)現(xiàn) resolve 方法,自己構(gòu)造一個(gè) DNS 協(xié)議的消息返回 。

還是這樣的格式:



大概這樣構(gòu)造:

會(huì)話 ID 從傳過來(lái)的 msg 取,flags 也設(shè)置下,問題數(shù)回答數(shù)都是 1,授權(quán)數(shù)、附加數(shù)都是 0。

問題區(qū)域和回答區(qū)域按照對(duì)應(yīng)的格式來(lái)設(shè)置:




需要用 Buffer.alloc 創(chuàng)建一個(gè) buffer 對(duì)象。

過程中還會(huì)用到 buffer.writeUInt16BE 來(lái)寫一些無(wú)符號(hào)的雙字節(jié)整數(shù)。

這里的 BE 是 Big Endian,大端序,也就是高位放在右邊的、低位放在左邊,

比如 00000000 00000001 是大端序的雙字節(jié)無(wú)符號(hào)整數(shù) 1。而小端序的 1 則是 00000001 00000000,也就是高位放在左邊。

拼裝 DNS 協(xié)議的消息還是挺麻煩的,大家簡(jiǎn)單看一下就行:

function copyBuffer(src, offset, dst) {
    for (let i = 0; i < src.length; ++i) {
      dst.writeUInt8(src.readUInt8(i), offset + i)
    }
  }

function resolve(msg, rinfo) {
    const queryInfo = msg.subarray(12)
    const response = Buffer.alloc(28 + queryInfo.length)
    let offset = 0


    // Transaction ID
    const id  = msg.subarray(0, 2)
    copyBuffer(id, 0, response)  
    offset += id.length
    
    // Flags
    response.writeUInt16BE(0x8180, offset)  
    offset += 2

    // Questions
    response.writeUInt16BE(1, offset)  
    offset += 2

    // Answer RRs
    response.writeUInt16BE(1, offset)  
    offset += 2

    // Authority RRs & Additional RRs
    response.writeUInt32BE(0, offset)  
    offset += 4
    copyBuffer(queryInfo, offset, response)
    offset += queryInfo.length

     // offset to domain name
    response.writeUInt16BE(0xC00C, offset)
    offset += 2
    const typeAndClass = msg.subarray(msg.length - 4)
    copyBuffer(typeAndClass, offset, response)
    offset += typeAndClass.length

    // TTL, in seconds
    response.writeUInt32BE(600, offset)  
    offset += 4

    // Length of IP
    response.writeUInt16BE(4, offset)  
    offset += 2
    '11.22.33.44'.split('.').forEach(value => {
      response.writeUInt8(parseInt(value), offset)
      offset += 1
    })
    server.send(response, rinfo.port, rinfo.address, (err) => {
      if (err) {
        console.log(err)
        server.close()
      }
    })
}

最后把拼接好的 DNS 協(xié)議的消息發(fā)送給對(duì)方。

這樣,就實(shí)現(xiàn)了 guangguangguang 的域名的解析。

上面代碼里我把它解析到了 11.22.33.44 的 IP。

我們用 nslookup 測(cè)試下:



可以看到,對(duì)應(yīng)的域名解析成功了!

這樣我們就通過 Node.js 實(shí)現(xiàn)了 DNS 服務(wù)器。

貼一份完整代碼,大家可以自己跑起來(lái),然后把電腦的本地 DNS 服務(wù)器指向它試試:

const dgram = require('dgram')

const server = dgram.createSocket('udp4')

function parseHost(msg) {
    let num = msg.readUInt8(0);
    let offset = 1;
    let host = "";
    while (num !== 0) {
      host += msg.subarray(offset, offset + num).toString();
      offset += num;
 
      num = msg.readUInt8(offset);
      offset += 1;
 
      if (num !== 0) {
        host += '.'
      }
    }
    return host
}

function copyBuffer(src, offset, dst) {
    for (let i = 0; i < src.length; ++i) {
      dst.writeUInt8(src.readUInt8(i), offset + i)
    }
  }

function resolve(msg, rinfo) {
    const queryInfo = msg.subarray(12)
    const response = Buffer.alloc(28 + queryInfo.length)
    let offset = 0

    // Transaction ID
    const id  = msg.subarray(0, 2)
    copyBuffer(id, 0, response)  
    offset += id.length
    
    // Flags
    response.writeUInt16BE(0x8180, offset)  
    offset += 2

    // Questions
    response.writeUInt16BE(1, offset)  
    offset += 2

    // Answer RRs
    response.writeUInt16BE(1, offset)  
    offset += 2

    // Authority RRs & Additional RRs
    response.writeUInt32BE(0, offset)  
    offset += 4
    copyBuffer(queryInfo, offset, response)
    offset += queryInfo.length

     // offset to domain name
    response.writeUInt16BE(0xC00C, offset)
    offset += 2
    const typeAndClass = msg.subarray(msg.length - 4)
    copyBuffer(typeAndClass, offset, response)
    offset += typeAndClass.length

    // TTL, in seconds
    response.writeUInt32BE(600, offset)  
    offset += 4

    // Length of IP
    response.writeUInt16BE(4, offset)  
    offset += 2
    '11.22.33.44'.split('.').forEach(value => {
      response.writeUInt8(parseInt(value), offset)
      offset += 1
    })
    server.send(response, rinfo.port, rinfo.address, (err) => {
      if (err) {
        console.log(err)
        server.close()
      }
    })
}

function forward(msg, rinfo) {
    const client = dgram.createSocket('udp4')
    client.on('error', (err) => {
      console.log(`client error:\n${err.stack}`)
      client.close()
    })
    client.on('message', (fbMsg, fbRinfo) => {
      server.send(fbMsg, rinfo.port, rinfo.address, (err) => {
        err && console.log(err)
      })
      client.close()
    })
    client.send(msg, 53, '192.168.199.1', (err) => {
      if (err) {
        console.log(err)
        client.close()
      }
    })
}

server.on('message', (msg, rinfo) => {
    const host = parseHost(msg.subarray(12))
    console.log(`query: ${host}`);

    if (/guangguangguang/.test(host)) {
        resolve(msg, rinfo)
    } else {
        forward(msg, rinfo)
    }
});
 
server.on('error', (err) => {
    console.log(`server error:\n${err.stack}`)
    server.close()
})
 
server.on('listening', () => {
    const address = server.address()
    console.log(`server listening ${address.address}:${address.port}`)
})
 
server.bind(53)
總結(jié)
本文我們學(xué)習(xí)了 DNS 的原理,并且用 Node.js 自己實(shí)現(xiàn)了一個(gè)本地 DNS 服務(wù)器。

域名解析的時(shí)候會(huì)先查詢 hosts 文件,如果沒查到就會(huì)請(qǐng)求本地域名服務(wù)器,這個(gè)是 ISP 提供的,一般每個(gè)城市都有一個(gè)。

本地域名服務(wù)器負(fù)責(zé)去解析域名對(duì)應(yīng)的 IP,它會(huì)依次請(qǐng)求根域名服務(wù)器、頂級(jí)域名服務(wù)器、權(quán)威域名服務(wù)器,來(lái)拿到最終的 IP 返回給客戶端。



電腦可以設(shè)置本地域名服務(wù)器的地址,我們把它指向了用 Node.js 實(shí)現(xiàn)的本地域名服務(wù)器。

DNS 協(xié)議是基于 UDP 傳輸?shù)模晕覀兺ㄟ^ dgram 模塊啟動(dòng)了 UDP 服務(wù)在 53 端口。

然后根據(jù) DNS 協(xié)議的格式,解析出域名,對(duì)目標(biāo)域名自己做處理,構(gòu)造出 DNS 協(xié)議的消息返回。其他域名則是轉(zhuǎn)發(fā)給另一臺(tái)本地 DNS 服務(wù)器做解析,把它返回的消息傳給客戶端。

這樣,我們就用 Node.js 實(shí)現(xiàn)了本地 DNS 服務(wù)器。

作者:神說要有光



歡迎關(guān)注微信公眾號(hào) :前端晚間課

更多文章,收錄于小程序-互聯(lián)網(wǎng)小兵