Node搭建一个DNS服务器
Node搭建DNS服务器的过程
接下来请深呼吸一大片代码正奔涌而来,该项目托管在https://github.com/MaxMin-she... 请各位同仁大神view指导。
1、route文件用来路由不同的action
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服务器总共有四个自定义的模块:
- utile: 自定义的错误处理模块儿
- DNSserver:进行DNS域名解析,获取域名所对应的IP地址
- Staticfiles: 根据请求路径加载静态文件
- Saveimg: 存储图片,返回一个自定的存储路径
接下来,我们分别来介绍这几个模块儿的功能和作用:
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将错误的结果打印出来
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:[email protected]:8080/path?query=string#hash’为例,主要包含的属性字段有:
1、protocal:‘http’,协议类型
2、slashes:true,表示protocal冒号后面跟着两个ASCII 斜杠字符
3、auth:'user:pass',由username:user和password:pass组成
4、host:'host.com:8080',由hostname(域名或者ip地址)和port(端口号)8080组成
5、path:‘/path?query=string’,路径,是由pathname(路径名称:‘/path’)和search(查询名称:‘?query=string’)组成
6、query:‘query=string’,由搜索对象形成 - url.parse():url模块的parse方法是将上面所说的这些属性值序列化成键值对对像。
假设我们的请求是:‘http://localhost:3000/dns?hostname=www.google.com’
- 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(域名服务器模块儿),这个模块包含两种函数:1、使用底层操作系统工具进行域名解析,无需进行网络通讯。2、链接到一个真实的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类型的控件,用于上传一张图片:
接下来,我们了解一下请求报文头和报文体的格式和内容:
- 请求报文头 req.headers,如下图所示:
在请求报文头中可以找到这些信息,其中Content-Type中的boundary属性很重要,因为附件的数据量比较大,所以一个附件需要多部分提交才能完成,而boundary就是每一部分内容之间的分隔符;Content-Length是报文的长度。
报文体如下所示:
------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):1、file:文件的存储路径 2、data:文件编码 3、options编码方式 4、callback:写入文件成功后的回调函数