本文要聊聊瀏覽器可愛的頭頭頭……們。 javascript
當咱們隨便打開一個網址(好比你們常常拿來測試網絡的百度)時,打開Network
,會看到以下請求頭,響應頭: html
Content-Type
表示請求頭或響應頭的內容類型。做爲請求頭時,利用它能夠進行body-parser。 Sooo~ What is body-parser
?前端
body-parser是node經常使用的中間件,其做用是:
java
Parse incoming request bodies in a middleware before your handlers, available under the req.body property.node
即在處理數據以前用中間件對post請求體進行解析。 body-parser的例子爲:docker
下面的例子展現瞭如何給路由添加
body parser
。一般,這是在express
中最爲推薦的使用body-parser
的方法。express
var express = require('express')
var bodyParser = require('body-parser')
var app = express()
// create application/json parser
var jsonParser = bodyParser.json()
// create application/x-www-form-urlencoded parser
var urlencodedParser = bodyParser.urlencoded({ extended: false })
// POST /login gets urlencoded bodies
app.post('/login', urlencodedParser, function (req, res) {
if (!req.body) return res.sendStatus(400)
res.send('welcome, ' + req.body.username)
})
// POST /api/users gets JSON bodies
app.post('/api/users', jsonParser, function (req, res) {
if (!req.body) return res.sendStatus(400)
// create user in req.body
})
複製代碼
body-parser
核心源碼爲:npm
// this uses a switch for static require analysis
switch (parserName) {
case 'json':
parser = require('./lib/types/json')
break
case 'raw':
parser = require('./lib/types/raw')
break
case 'text':
parser = require('./lib/types/text')
break
case 'urlencoded':
parser = require('./lib/types/urlencoded')
break
}
複製代碼
以json
爲例:json
var contentType = require('content-type')
//...
/**
* Get the charset of a request.
*
* @param {object} req
* @api private
*/
function getCharset (req) {
try {
return (contentType.parse(req).parameters.charset || '').toLowerCase()
} catch (e) {
return undefined
}
}
//...
// assert charset per RFC 7159 sec 8.1
var charset = getCharset(req) || 'utf-8'
if (charset.substr(0, 4) !== 'utf-') {
debug('invalid charset')
next(createError(415, 'unsupported charset "' + charset.toUpperCase() + '"', {
charset: charset,
type: 'charset.unsupported'
}))
return
}
複製代碼
能夠看出:其背後工做原理就是經過分析請求頭中的Content-Type
的類型,根據不一樣的類型進行相應數據處理,咱們本身模擬一下:後端
step1: 先創建server.js
:
req.on('end',function (params) {
let r = Buffer.concat(arr).toString();
// body-parser 解析請求,根據不一樣的格式進行不一樣的解析
if (req.headers['content-type'] === www.js){
let querystring = require('querystring');
r = querystring.parse(r); // a=1&b=2
console.log(r,1);
} else if (req.headers['content-type'] === 'application/json'){
console.log(JSON.parse(r),2);
} else{
console.log(r,3);
}
res.end('end');
})
複製代碼
step2: 客戶端模擬請求:
let opts = {
host:'localhost',
port:3000,
path:'/hello',
headers:{
'a':1,
'Content-Type':'application/json',
"Content-Length":7 //模擬的時候須要帶上長度,否則客戶端會當成沒有傳遞數據
}
}
let http = require('http');
let client = http.request(opts,function (res) {
res.on('data',function (data) {
console.log(data.toString());
})
});
client.end("{\"a\":1}"); // 表示把請求發出去
複製代碼
step3: 測試。 先啓動server,再啓動client,服務端收到按照application/json
格式解析的數據: { a: 1 } 2
. Content-Type
與body-parser
之間的關係就先分析到這裏了。 後面咱們接着看請求頭。
請求頭經過Range:bytes能夠請求資源的某一部分。利用這個字段可模擬部分讀取。以下:
http.createServer(function (req, res) {
let range = req.headers['range'];
})
複製代碼
server:
let http = require('http');
let fs = require('fs');
let path = require('path');
// 當前要下載的文件的大小
let size = fs.statSync(path.join(__dirname, 'my.txt')).size;
let server = http.createServer(function (req, res) {
let range = req.headers['range']; // 0-3
if (range) {
// 模擬請求 curl -v --header "Range:bytes=0-3" http://localhost:3000
let [, start, end] = range.match(/(\d*)-(\d*)/);
start = start ? Number(start) : 0;
end = end ? Number(end) : size - 1; // 10個字節 size 10 (0-9)
res.setHeader('Content-Range', `bytes ${start}-${end}/${size - 1}`);
fs.createReadStream(path.join(__dirname, 'my.txt'), { start, end }).pipe(res);
} else {
// 會把文件的內容寫給客戶端
fs.createReadStream(path.join(__dirname, 'my.txt')).pipe(res);
//可讀流能夠經過pipe導到可寫流
}
});
server.listen(3000);
複製代碼
client:
let opts = {
host:'localhost',
port:3000,
headers:{}
}
let http = require('http');
let start = 0;
let fs = require('fs');
function download() {
opts.headers.Range = `bytes=${start}-${start+3}`;
start+=4;
console.log(`start is ${start}`)
let client = http.request(opts,function (res) {
let total = res.headers['content-range'].split('/')[1];
// console.log(half)
res.on('data',function (data) {
fs.appendFileSync('./download1.txt',data);
});
res.on('end',function () {
setTimeout(() => {
if ((!pause)&&(start < total))
download();
}, 1000);
})
});
client.end();
}
download()
複製代碼
分段讀取添加暫停功能,監聽用戶輸入
let pause = false;
process.stdin.on('data',function (data) {
if (data.toString().includes('p')){
pause = true
}else{
pause = false;
download()
}
})
複製代碼
測試結果:
分段讀取有如下好處:
模擬並行下載:
let halfFlag = 20
function download() {
opts.headers.Range = `bytes=${start}-${start+3}`;
start+=4;
console.log(`start is ${start}`)
let client = http.request(opts,function (res) {
let total = res.headers['content-range'].split('/')[1];
let halfFlag = Math.floor(total/2)
// console.log(half)
res.on('data',function (data) {
fs.appendFileSync('./download1.txt',data);
});
res.on('end',function () {
setTimeout(() => {
if ((!pause)&&(start < halfFlag))
download();
}, 1000);
})
});
client.end();
}
let half = halfFlag
function downloadTwo() {
opts.headers.Range = `bytes=${half}-${half+3}`;
half+=4;
console.log(`half is ${half}`)
let client = http.request(opts,function (res) {
let total = res.headers['content-range'].split('/')[1];
res.on('data',function (data) {
fs.appendFileSync('./download2.txt',data);
});
res.on('end',function () {
setTimeout(() => {
if (!pause&&half < total)
downloadTwo();
}, 1000);
})
});
client.end();
}
download();
downloadTwo();
複製代碼
運行結果,會把原文件分紅兩部分下載到download1.txt和download2.txt。 測試:
Response Header響應頭中Cache-Control: max-age=1233
能夠設置相對當前的時間的強制緩存,與它相關的 Expires
能夠設置某個絕對時間點限定讀取緩存的時間。 模擬實現:
let url = require('url'); // 專門用來處理url路徑的核心模塊
// http://username:password@hostname:port/pathname?query
let server = http.createServer(async function (req,res) {
console.log(req.url)
let { pathname,query} = url.parse(req.url,true);
// true就是將query轉化成對象
let readPath = path.join(__dirname, 'public', pathname);
try {
let statObj = await stat(readPath);
// 根客戶端說 10s 內走緩存
res.setHeader('Cache-Control','max-age=10');
res.setHeader('Expires',new Date(Date.now()+10*1000).toGMTString());
// 10s以內的請求都會走cache 返回200, (from disk cache)不發生請求
if (statObj.isDirectory()) {
let p = path.join(readPath, 'index.html');
await stat(p);
// 若是當前目錄下有html那麼就返回這個文件
fs.createReadStream(p).pipe(res);
} else {
fs.createReadStream(readPath).pipe(res);
}
}catch(e){
res.statusCode = 404;
res.end(`Not found`);
}
}).listen(3000);
複製代碼
測試:
對比響應頭Last-Modified and 與請求頭If-Modified-Since,能夠經過文件修改時間看文件是否修改,從而決定是從新請求仍是走緩存。 模擬以下: step1 不設置強制緩存
res.setHeader('Cache-Control','no-cache');
複製代碼
step2 應用文件修改時間比對是否修改,
res.setHeader('Last-Modified', statObj.ctime.toGMTString());
if (req.headers['if-modified-since'] === statObj.ctime.toGMTString()) {
res.statusCode = 304;
res.end();
return; // 走緩存
}
fs.createReadStream(readPath).pipe(res);
複製代碼
測試:
對比響應頭:Etag 與請求頭:If-None-Match,Etag和If-None-Match若是相等,即返回304。 etag如何添加?
根據文件內容,生成一個md5的摘要,給實體加一個標籤。
這種方法雖然比較耗性能,可是可以更加精確的對比出文件是否進行了修改。依靠文件修改時間進行對比並不夠準確。由於有時文件有改動Last-Modified發生了變化,可是文件的內容可能根本沒有變化。因此這種方案要優於2.4.
實現方法:
let rs = fs.createReadStream(p);
let md5 = crypto.createHash('md5'); // 不能寫完響應體再寫頭
let arr = [];
rs.on('data',function (data) {
md5.update(data);
arr.push(data);
});
複製代碼
設置Etag
rs.on('end',function () {
let r = md5.digest('base64');
res.setHeader('Etag', r);
if (req.headers['if-none-match'] === r ){
res.statusCode = 304;
res.end();
return;
}
res.end(Buffer.concat(arr));
})
複製代碼
測試:
依靠請求頭: Accept-Encoding: gzip, deflate, br告訴服務端可接受的數據格式。服務端返回後會把數據格式經過響應格式經過Content-Encoding來標記。 在客戶端接受gzip的格式下,後端可經過文件壓縮處理傳遞,提升性能。 node api中提供了zlib模塊:
zlib模塊提供經過 Gzip 和 Deflate/Inflate 實現的壓縮功能
下面咱們來應用zlib與請求頭Accept-Encoding來實現壓縮功能。
let zlib = require('zlib');
let fs = require('fs');
let path = require('path');
function gzip(filePath) {
let transform = zlib.createGzip();//轉化流經過transform壓縮,而後再寫
fs.createReadStream(filePath).pipe(transform).pipe(fs.createWriteStream(filePath+'.gz'));
}
gzip('2.txt')
複製代碼
解壓:
function gunzip(filePath) {
let transform = zlib.createGunzip();
fs.createReadStream(filePath).pipe(transform).pipe(fs.createWriteStream(path.basename(filePath,'.gz')));
}
複製代碼
path.basename(filePath,'.gz')
用來去掉filePath文件名的後綴.gz
。
根據請求頭接受的類型後端的具體操做 :
if(req.url === '/download'){
res.setHeader('Content-Disposition', 'attachment' )
return fs.createReadStream(path.join(__dirname, '1.html')).pipe(res);
}
複製代碼
let http = require('http');
let fs = require('fs');
let path = require('path');
let zlib = require('zlib');
http.createServer(function (req,res) {
if(req.url === '/download'){
res.setHeader('Content-Disposition', 'attachment' )
return fs.createReadStream(path.join(__dirname, '1.html')).pipe(res);
}
let rule = req.headers['accept-encoding'];
if(rule){
if(rule.match(/\bgzip\b/)){
res.setHeader('Content-Encoding','gzip');
fs.createReadStream(path.join(__dirname, '1.html'))
.pipe(zlib.createGzip())
.pipe(res);
} else if (rule.match(/\bdeflate\b/)){
res.setHeader('Content-Encoding', 'deflate');
fs.createReadStream(path.join(__dirname, '1.html'))
.pipe(zlib.createDeflate())
.pipe(res);
}else{
fs.createReadStream(path.join(__dirname, '1.html')).pipe(res);
}
}else{
fs.createReadStream(path.join(__dirname, '1.html')).pipe(res);
}
}).listen(3000);
複製代碼
test deflate:
curl -v --header "Accept-Encoding:deflate" http://localhost:3000
* Rebuilt URL to: http://localhost:3000/
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 3000 (#0)
> GET / HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.54.0
> Accept: */*
> Accept-Encoding:deflate
>
< HTTP/1.1 200 OK
< Content-Encoding: deflate
< Date: Thu, 23 Aug 2018 03:01:13 GMT
< Connection: keep-alive
< Transfer-Encoding: chunked
複製代碼
test others:
curl -v --header "Accept-Encoding:nn" http://localhost:3000
* Rebuilt URL to: http://localhost:3000/
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 3000 (#0)
> GET / HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.54.0
> Accept: */*
> Accept-Encoding:nn
>
< HTTP/1.1 200 OK
< Date: Thu, 23 Aug 2018 03:02:51 GMT
< Connection: keep-alive
< Transfer-Encoding: chunked
<
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
你好
</body>
* Connection #0 to host localhost left intact
</html>%
複製代碼
let http = require('http');
let fs = require('fs');
let url = require('url');
let path = require('path');
// 這是百度的服務器
let server = http.createServer(function (req,res) {
let { pathname } = url.parse(req.url);
let realPath = path.join(__dirname,pathname);
fs.stat(realPath,function(err,statObj) {
if(err){
res.statusCode = 404;
res.end();
}else{
let referer = req.headers['referer'] || req.headers['referred'];
if(referer){
let current = req.headers['host'] // 表明的是當前圖片的地址
referer = url.parse(referer).host // 引用圖片的網址
if (current === referer){
fs.createReadStream(realPath).pipe(res);
}else{
fs.createReadStream(path.join(__dirname,'images/2.jpg')).pipe(res);
}
}else{
fs.createReadStream(realPath).pipe(res);
}
}
})
}).listen(3000);
複製代碼
請求頭:Accept-Language: zh-CN,zh;q=0.9 多個語言用 ',' 分隔,權重用 '=' 表示',沒有默認權重爲1
後端根據請求接受語言的權重一次查找,查找到就返回,找不到就用默認語言
let langs = {
en: 'hello world',
'zh-CN':'你好世界',
zh:'你好',
ja: 'こんにちは、世界'
}
let defualtLanguage = 'en'
// 多語言之服務端方案:來作 (瀏覽器會發一個頭) 前端來作
// 經過url實現多語言
let http = require('http');
http.createServer(function (req,res) {
let lan = req.headers['accept-language'];
//[[zh,q=0.9],[zh-CN]] =>[{name:'zh-CN',q=1},{name:'zh',q:0.9}]
if(lan){
lan = lan.split(',');
lan = lan.map(l=>{
let [name,q] = l.split(';');
q = q?Number(q.split('=')[1]):1
return {name,q}
}).sort((a,b)=>b.q-a.q); // 排出 權重數組
for(let i = 0 ;i <lan.length;i++){
// 將每一個人的名字 取出來
let name= lan[i].name;
if(langs[name]){ //去語言包查找 查找到就返回
res.end(langs[name]);
return;
}
}
res.end(langs[defualtLanguage]); // 默認語言
}else{
res.end(langs[defualtLanguage]); // 默認語言
}
}).listen(3000);
複製代碼
測試:
請求頭與響應頭在先後端聯調時會常用。瞭解了他們的妙用先後端配合會更加和諧順暢~
Author: Yanni Jia
Nickname: 很是兔
Email: 385067638@qq.com