Node搭建一個DNS服務器

Node搭建DNS服務器的過程

接下來請深呼吸一大片代碼正奔涌而來,該項目託管在https://github.com/MaxMin-she... 請各位同仁大神view指導。
一、route文件用來路由不一樣的actioncss

const DNSserver = require('./src/controller/DNSserver.js');
const Staticfiels = require('./src/controller/Staticfiels.js');
const SaveImg = require('./src/controller/Saveimg.js')

exports.getRouter = function (req, res) {
    console.log(url.parse(req.url));
    const pathname = url.parse(req.url).pathname;

    switch (pathname) {
        case '/dns':
          DNSserver.parse(req, res);
        break;
        case '':
        case '/':
        case '/index':
          Staticfiels.index(req, res);
        break;
        case '/post/img':
          SaveImg.saveimg (req, res);
        break;
        default:
         Staticfiels.loadfiels(req, res, pathname);
    }
}
module.exports = exports;

DNS服務器的有三個功能因此包含有三個模塊兒,開頭就引入了三個模塊兒,經過請求的url路徑名稱咱們路由到不一樣的處理模塊兒。這個簡易的DNS服務器總共有四個自定義的模塊:html

  • utile: 自定義的錯誤處理模塊兒
  • DNSserver:進行DNS域名解析,獲取域名所對應的IP地址
  • Staticfiles: 根據請求路徑加載靜態文件
  • Saveimg: 存儲圖片,返回一個自定的存儲路徑

接下來,咱們分別來介紹這幾個模塊兒的功能和做用:git

utile模塊兒

/**
 * handdle error
 * @param err
 * @param msg
 */

exports.errorHandle = function(err, type){
    const time = new Date();
    console.log(`------------------------\n
                 time: ${time}\n
                 err: ${err}\n
                 type: ${type}\n
                 ------------------------\n
                `);
}

module.exports = exports;

該模塊兒的做用是經過傳入的err,和提示的msg將錯誤的結果打印出來github

DNSserver模塊兒

/**
 * DNS解析
 */
const url = require('url');
const querystring = require('querystring');
const dns = require('dns');

const util = require('../utile/utile.js');

/**
 * @param req
 * @param res
 */

exports.parse = function(req, res){
    const query_url = url.parse(req.url);
    const query = querystring.parse(query_url.query);
    dns.resolve4(query['hostname'], function(err, addresses){
       if(err){
          util.errorHandle(err, 'DNS failed');
          res.writeHead(400);
          res.end();
       } else {
          res.writeHead(200);
          res.end(addresses.toString());
       }
    });
}

module.exports = exports;
  • req.url:req.url是一個包含着請求基本信息的字符串,以‘http://user:pass@host.com:8080/path?query=string#hash’爲例,主要包含的屬性字段有:
    一、protocal:‘http’,協議類型
    二、slashes:true,表示protocal冒號後面跟着兩個ASCII 斜槓字符
    三、auth:'user:pass',由username:user和password:pass組成
    四、host:'host.com:8080',由hostname(域名或者ip地址)和port(端口號)8080組成
    五、path:‘/path?query=string’,路徑,是由pathname(路徑名稱:‘/path’)和search(查詢名稱:‘?query=string’)組成
    六、query:‘query=string’,由搜索對象造成
  • url.parse():url模塊的parse方法是將上面所說的這些屬性值序列化成鍵值對對像。

假設咱們的請求是:‘http://localhost:3000/dns?hostname=www.google.com’json

  • querystring.parse(str[, sep[, eq[, options]]]):query的屬性的值相似於'hostname=www.google.com'這樣的值,querystring(查詢字符串)模塊兒的做用是用來解析和格式化url查詢字符串,其中的parse方法是將這種形式的字符串序列化成{hostname:google.com}這樣的鍵值對集合。
    str:須要分割的查詢字符串
    sep:用於界定查詢字符串中鍵值對的符號
    eq:用於界定查詢字符串中鍵與值的符號
    options:用來定義解碼查詢字符串的函數和解析鍵的最大數量
  • dns.resolve4(str,function(err, ad){}):dns(域名服務器模塊兒),這個模塊包含兩種函數:一、使用底層操做系統工具進行域名解析,無需進行網絡通信。二、連接到一個真實的DNS服務器進行域名解析,且始終使用網絡進行查詢。resolve4()屬於第二種函數。它的做用是使用DNS協議解析IPV4地址主機名,回調函數中的第一個參數是出現的錯誤,第二個參數是解析獲得的ip地址,注意:這裏返回的addresses是一個IPV4地址數組,可是res.end()的數據類型只能是string或者buffer,因此在響應是須要回調toString方法,將數組轉化成字符串。

Staticfiles模塊兒

/**
 * get static files
 */
const fs = require('fs');
const path = require('path');
const util = require('../utile/utile.js');
/**
 * read Fiels
 * @param req
 * @param res
 * @param pathname
 */

const readStaticFiles = function(req, res, filename){
  fs.readFile(filename, function(err, data){
     if(err){
       util.errorHandle(err, 'filed readFile');
       res.writeHead(404);
       res.end('We Got A Problem: File Not Found');
     } else {
         res.writeHead(200);
         res.end(data);
     }
  })
} 

/**
 * exports function of reading files
 */
exports.loadfiels = function(req, res, pathname){
  const filename = path.join('E:\static', pathname);
  console.log(filename);
  readStaticFiles(req, res, filename);
}

module.exports = exports;

/**
 * exports function of getting default page
 */
exports.index = function(req, res){
    const filename = path.join('\static', 'html/index.html');
    readStaticFiles(req, res, filename);
}

Staticfiles文件中有兩個輸出,index模塊是用來處理沒有輸入文件名時的默認值,loadfiels模塊則能夠根據文件名返回靜態文件,兩個模塊兒都使用的同一函數readStaticFiles進行文件的讀取操做。數組

  • path模塊兒:用來處理文件和目錄的路徑
  • path.join([...paths]):將給定的全部path片斷使用平臺特定的連接符連接成規範化路徑。在這個項目中,因爲全部靜態文件都放在該項目的static目錄下面。因此,請求路徑以前要加一個相對路徑'static',否則就會報路徑錯誤的error。
  • fs.readFile(path[,options],callback):根據路徑異步讀取文件,回調函數中返回兩個參數:第一個:error是讀取文件過程當中產生的錯誤,第二個:data是讀取文件的二進制數據流,若是在option中未指定編碼方式,返回的則是一個原始的buffer。

Saveimg模塊兒

在這個模塊中咱們將實現圖片上傳下載的功能。
首先在html中完成一個form表單:服務器

<!DOCTYPE HTML>
<html>
<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="/css/index.css" />
</head>
<body>
    <form enctype="multipart/form-data" action="post/img" method="POST">
        <input name="userfile1" type="file">
        <input type="submit" value="發送文件">
    </form>
</body>
</html>

'multipart/form-data'是post的一種數據提交方式,用於附件的上傳,表單中還有file類型的控件,用於上傳一張圖片:網絡

接下來,咱們瞭解一下請求報文頭和報文體的格式和內容:app

  • 請求報文頭 req.headers,以下圖所示:

圖片描述
在請求報文頭中能夠找到這些信息,其中Content-Type中的boundary屬性很重要,由於附件的數據量比較大,因此一個附件須要多部分提交才能完成,而boundary就是每一部份內容之間的分隔符;Content-Length是報文的長度。
報文體以下所示:dom

------WebKitFormBoundaryKXd7iAk5VsWqoaAY
Content-Disposition: form-data; name="userfile1"; filename="2.jpg"
Content-Type: image/jpeg


------WebKitFormBoundaryKXd7iAk5VsWqoaAY--

由於傳輸的數據量是未知的,因此經過boundary處理報文體是相當重要的一步。
在瞭解完附件上傳的報文形式之後,接下來咱們將一步步的來實現圖片上傳的全部功能:

exports.saveimg = function (req, res) {
    if (req.method.toLowerCase() === 'get') {
        getHandle(req, res);
    } else if (req.method.toLowerCase() === 'post') {
        postHandle(req, res);
    }
}

首先,咱們經過請求的方式來進行分支處理,上傳圖片的http請求方式必須是post,postHandle函數的具體實現過程以下:

function postHandle(req, res) {
    req.setEncoding('binary');
    let body = '';
    let filename = '';

    req.on('data', function (chunk) {
        body += chunk;
    });

    req.on('end', function () {
        const boundary = req.headers['content-type'].split(';')[1].replace('boundary=', '');  (1)
        const file = querystring.parse(body, '\r\n', ':');  (2)
        if (file['Content-Type'].indexOf('image') !== -1) {
            const fileAr = file['Content-Disposition'].split('; ')[2].replace('filename=', '').split('.');(3)
            let filename = fileAr[0];(4)
            const imageState = fileAr[1].substring(0, fileAr[1].length-1);(5)
            const entireData = body.toString();

            const contentType = file['Content-Type'].substring(1);
            const upperBound = entireData.indexOf(contentType) + contentType.length;(6)
            const tarStr = entireData.substring(upperBound).trim();
            const boundaryIndex = tarStr.length - boundary.length - 4;
            const binaryData = tarStr.substring(0, boundaryIndex);

            //從新設置文件名稱
            filename = randomImgString(filename);

                fs.writeFile(path.join(__dirname, `../../img/${filename}.${imageState}`), binaryData, { encoding: 'binary' }, (err) => {
                    if (err) {
                        utile.errorHandle(err, 'failed write file');
                    } else {
                        res.writeHead(200, {
                            'Content-Type': 'application/json'
                        });
                        const data = JSON.stringify({
                            'url':`http://127.0.0.1:3000/${filename}.${imageState}`
                        })
                        console.log(data);
                        res.write(data);
                        res.end();
                    }
                });
        }
    })
}
  • req.on('data',callback)綁定了用來監聽數據流的事件,req.on('end',callback)監聽數據傳輸完畢的事件,因而可知對傳輸來的數據進行的一系列操做都應該放在這個監聽事件的回調函數當中
  • body變量中存儲的是本次附件上傳中存儲的全部數據,以下圖所示(我只截取了body變量中的一部分):

如下截圖是body的開頭部分:
圖片描述
如下截圖則是body的結束部分:
圖片描述

  • (1)段代碼的做用是從請求報文頭的content-type屬性值中截取boundary分割符的內容
  • (2)段代碼的做用是提取出報文體中的鍵值對,querystring模塊兒中的parse方法上文有說起,解析後的具體內容以下圖所示:

圖片描述
這段代碼的目的是爲了獲取到報文體中的Content-Disposition字段和Content-Type字段,從Content-Disposition字段中能夠獲取到文件名稱和文件格式,代碼3,4,5則完成了這個功能。
從打印的返回的報文體來看,Content-Type之後的全部數據就是圖片的編碼,因此接下來的任務就是將這個編碼提取出來

  • (6)段代碼的做用是找到圖片編碼字符串開始的index
  • (7)段代碼的做用是找到圖片編碼字符串的結束index,由body的結尾截圖能夠看出,結束部分是由‘--boundary--’的形式組成,因此最後減去的除了boundary的長度還有兩個‘--’的長度4。
  • (8)段代碼中的binaryData則是圖片的完整編碼

隨機生成文件名稱的函數randomImgString的實現過程以下所示:

/**
 * option to generate randomString
 */
function randomImgString(filename){
    let outString = new Date().toTimeString();
    outString += filename.substring(0, filename.indexOf('.'));
    outString = hash.update(outString)
                    .digest('hex').substring(0, 15);
    return outString;
}

*fs.writeFile(file, data[, options], callback):一、file:文件的存儲路徑 二、data:文件編碼 三、options編碼方式 四、callback:寫入文件成功後的回調函數

相關文章
相關標籤/搜索