常見的方法有:html
類別 | 說明 |
---|---|
1XX | Informational(信息性狀態碼 |
2XX | Success(成功狀態碼) |
3XX | Redirection(重定向) |
4XX | Client Error(客戶端錯誤狀態碼) |
5XX | Server Error(服務器錯誤狀態嗎) |
首部字段名 | 說明 |
---|---|
Cache-Control | 控制緩存行爲 |
Connection | 連接的管理 |
Date | 報文日期 |
Pragma | 報文指令 |
Trailer | 報文尾部的首部 |
Trasfer-Encoding | 指定報文主體的傳輸編碼方式 |
Upgrade | 升級爲其餘協議 |
Via | 代理服務器信息 |
Warning | 錯誤通知 |
首部字段名 | 說明 |
---|---|
Accept | 用戶代理可處理的媒體類型 |
Accept-Charset | 優先的字符集 |
Accept-Encoding | 優先的編碼 |
Accept-Langulage | 優先的語言 |
Authorization | Web認證信息 |
Expect | 期待服務器的特定行爲 |
From | 用戶的電子郵箱地址 |
Host | 請求資源所在的服務器 |
If-Match | 比較實體標記 |
If-Modified-Since | 比較資源的更新時間 |
If-None-Match | 比較實體標記 |
If-Range | 資源未更新時發送實體Byte的範圍請求 |
If-Unmodified-Since | 比較資源的更新時間(和If-Modified-Since相反) |
Max-Forwards | 最大傳輸跳數 |
Proxy-Authorization | 代理服務器須要客戶端認證 |
Range | 實體字節範圍請求 |
Referer | 請求中的URI的原始獲取方 |
TE | 傳輸編碼的優先級 |
User-Agent | HTTP客戶端程序的信息 |
首部字段名 | 說明 |
---|---|
Accept-Ranges | 是否接受字節範圍 |
Age | 資源的建立時間 |
ETag | 資源的匹配信息 |
Location | 客戶端重定向至指定的URI |
Proxy-Authenticate | 代理服務器對客戶端的認證信息 |
Retry-After | 再次發送請求的時機 |
Server | 服務器的信息 |
Vary | 代理服務器緩存的管理信息 |
www-Authenticate | 服務器對客戶端的認證 |
首部字段名 | 說明 |
---|---|
Allow | 資源可支持的HTTP方法 |
Content-Encoding | 實體的編碼方式 |
Content-Language | 實體的天然語言 |
Content-Length | 實體的內容大小(字節爲單位) |
Content-Location | 替代對應資源的URI |
Content-MD5 | 實體的報文摘要 |
Content-Range | 實體的位置範圍 |
Content-Type | 實體主體的媒體類型 |
Expires | 實體過時時間 |
Last-Modified | 資源的最後修改時間 |
我的以爲只要瞭解請求首部及響應首部便可。其中響應首部中的range(能夠作斷點續傳,會在下文說起),還有緩存(ETag),這些是必需要掌握的知識。前端
const http = require('http');
const server = http.createServer(function(req,res){
res.end(123);
});
server.listen(8080);
複製代碼
const http = require('http');
const server = http.createServer();
// req是請求 是一個可讀流 = socket
// res是響應 是一個可寫流
server.on('request',function(req,res){
let method = req.method;
let httpVersion = req.httpVersion;
let url = req.url;
let headers = req.headers; // 請求頭的名字都是小寫的
console.log(method,httpVersion,url,headers);
// 若是數據 大於64k data事件可能會觸發屢次
let buffers = [];
// 若是沒有請求體 不會走on('data'),沒有請求體也會觸發end事件
req.on('data',function(data){
console.log(1)
buffers.push(data);
});
req.on('end',function(){
console.log(Buffer.concat(buffers).toString());
// socket.write socket.end
res.write('hello');
res.end('world');
});
});
// 監聽請求的到來
server.on('connection',function(socket){
console.log('創建鏈接');
});
server.on('close',function(){
console.log('服務端關閉')
})
server.on('error',function(err){
console.log(err);
});
server.listen(8080);
複製代碼
咱們經過翻源碼,會發現上面兩種寫法的一致性:node
能夠經過它來處理請求的url,簡單demo以下:webpack
let url = require('url');
let u = 'http://www.baidu.com:80/abc/index.html?a=1&b=2#hash';
// 能夠將查詢字符串轉化成對象
let urlObj = url.parse(u,true);
console.log(urlObj.query); // 查詢字符串
console.log(urlObj.pathname); // 路徑
複製代碼
在上一篇的最後內容中,有說起req的一些屬性,那麼咱們來看一下res有哪些方法吧。git
可使用write方法發送響應內容github
response.write(chunk,[encoding]);
response.end([chunk],[encoding]);
複製代碼
res.writeHead(statusCode, [headers]);
複製代碼
res.setHeader(key, value);
複製代碼
它與writeHead的區別是:它不會真正的把響應頭寫給客戶端。web
即在writeHeader以後,再執行setHeader是會報錯的。算法
// server端
const http = require('http');
const server = http.createServer(function(req,res){
let contentType = req.headers['content-type'];
let buffers = [];
req.on('data',function(chunk){
buffers.push(chunk);
});
req.on('end',function(){
let content = Buffer.concat(buffers).toString();
if(contentType === 'application/json'){
console.log(JSON.parse(content).name)
}else if(contentType === 'application/x-www-form-urlencoded'){
let queryString = require('querystring');
console.log(queryString.parse(content).age)
}
res.end('hello');
});
});
server.listen(4000);
複製代碼
// client
const http = require('http');
const options = {
hostname:'localhost',
port:4000,
path: '/',
method:'get',
// 告訴服務端我當前要給你發什麼樣的數據
headers:{
'Content-Type':'application/x-www-form-urlencoded',
'Content-Length':5
}
}
const req = http.request(options, function(res) {
res.on('data',function(chunk){
console.log(chunk.toString());
});
});
req.end('age=1');
複製代碼
須要注意的是客戶端請求須要傳遞Content-Length,否則會有問題。數據庫
固然在實際工做中,可能直接就用下面兩個npm包了(一般用promise的那個):npm
能夠經過Accept-Language檢測瀏覽器的語言
const pack = {
'en': { title: 'english' },
'zh-CN': { title: '中文' }
}
const http = require('http');
const server = http.createServer(function (req, res) {
let lan = 'en';
let language = req.headers['accept-language'];
if (language) {
lan = language.split(',').map(function (item) {
let values = item.split(';');
return {
name: values[0],
q: values[1] ? parseInt(values[1]) : 1
}
}).sort((lang1, lang2) => lang2.q - lang1.q).shift().name;
console.log(lan)
}
res.end(pack[lan] ? pack[lan].title : pack['en'].title);
}).listen(4000);
複製代碼
這個QQ空間圖片比較常見,引用過去以後會變成裂圖。
實現原理:
const http = require('http');
const url = require('url');
const path = require('path');
const fs = require('fs');
const root = path.join(__dirname, 'public');
function removePort(host) {
return host.split(':')[0]
}
function getHostName(urlAddr) {
const { host } = url.parse(urlAddr);
return removePort(host);
}
function request(req, res) {
let refer = req.headers['referer'] || req.headers['referrer'];
if (refer) {
let referHost = getHostName(refer);
let host = removePort(req.headers['host']);
if (referHost != host) {
sendForbidden(req, res);
} else {
serve(req, res);
}
} else {
serve(req, res);
}
}
function serve(req, res) {
let {
pathname
} = url.parse(req.url);
let filepath = path.join(root, pathname);
console.log(req.url, filepath);
fs.stat(filepath, (err, stat) => {
if (err) {
res.end('Not Found');
} else {
fs.createReadStream(filepath).pipe(res);
}
});
}
function sendForbidden(req, res) {
res.end('防盜鏈');
}
const server = http.createServer();
server.on('request', request);
server.listen(8080);
複製代碼
let httpProxy = require('http-proxy');
let http = require('http');
let proxy = httpProxy.createProxyServer();
http.createServer(function (req, res) {
proxy.web(req, res, {
target: 'http://localhost:8000'
});
proxy.on('error', function (err) {
console.log('出錯了');
res.end(err.toString());
});
}).listen(8080);
複製代碼
上面代碼表示的是請求localhost:8080
時,轉發到http://localhost:8000
。像webpack-dev-server的轉發請求模塊:http-proxy-middleware,就是使用到了http-proxy
。
經過Host實現多個網站共用一個端口,多個網站共用一個服務器
const http = require('http');
const httpProxy = require('http-proxy');
const proxy = httpProxy.createProxyServer();
let hosts = {
'www.test1.com': 'http://localhost:8000',
'www.test2.com': 'http://localhost:9000'
}
http.createServer(function (req, res) {
let host = req.headers['host'];
host = host.split(':')[0];
let target = hosts[host];
proxy.web(req, res, {
target
});
}).listen(80);
複製代碼
當用戶在聽一首歌的時候,若是聽到一半(網絡下載了一半),網絡斷掉了,用戶須要繼續聽的時候,文件服務器不支持斷點的話,則用戶須要從新下載這個文件。而Range支持的話,客戶端應該記錄了以前已經讀取的文件範圍,網絡恢復以後,則向服務器發送讀取剩餘Range的請求,服務端只須要發送客戶端請求的那部份內容,而不用整個文件發送回客戶端,以此節省網絡帶寬。
爲了實現中斷恢復下載的需求,須要能下載指定下載的實體範圍
// 服務端
const http = require('http');
const fs = require('fs');
const path = require('path');
const { promisify } = require('util');
const stat = promisify(fs.stat);
// 客戶端要發一個頭Range:bytes=0-10
// 服務端返回一個頭
// Accept-Ranges:bytes
// Content-Range:0-10/總大小
const server = http.createServer(async function (req, res) {
let p = path.join(__dirname, 'content.txt');
// 判斷當前文件的大小
let statObj = await stat(p);
let start = 0;
let end = statObj.size - 1; // 讀流是包前又包後的
let total = end
let range = req.headers['range'];
if (range) {
// 告訴它支持範圍請求
res.setHeader('Accept-Ranges','bytes');
// ['匹配的字符串','第一個分組']
let result = range.match(/bytes=(\d*)-(\d*)/);
start = result[1]?parseInt(result[1]):start;
end = result[2]?parseInt(result[2])-1:end;
// 獲取成功而且文件總大小是多少
res.setHeader('Content-Range',`${start}-${end}/${total}`)
}
res.setHeader('Content-Type', 'text/plain;charset=utf8');
fs.createReadStream(p, { start, end }).pipe(res);
});
server.listen(3000);
複製代碼
// 客戶端
const options = {
hostname: 'localhost',
port: 3000,
path: '/',
method: 'GET'
}
const fs = require('fs');
const path = require('path');
const http = require('http');
const ws = fs.createWriteStream('./download.txt');
let pause = false;
let start = 0;
// 監聽鍵盤事件,若是有輸入p,則暫停
process.stdin.on('data', function (chunk) {
chunk = chunk.toString();
if (chunk.includes('p')) {
pause = true
} else {
pause = false;
download();
}
});
function download() {
options.headers = {
Range: `bytes=${start}-${start + 10}`
}
start += 10;
// 發請求
// 0-10
http.get(options, function (res) {
let range = res.headers['content-range'];
let total = range.split('/')[1];
let buffers = [];
res.on('data', function (chunk) {
buffers.push(chunk);
});
res.on('end', function () {
//將獲取的數據寫入到文件中
ws.write(Buffer.concat(buffers));
setTimeout(function () {
if (pause === false && start < total) {
download();
}
}, 1000)
})
})
}
download();
複製代碼
node的壓縮文檔
const fs = require('fs');
const path = require('path')
const zlib = require('zlib');
// 壓縮
function zip(src){
// 壓縮流 轉化流
const gzip = zlib.createGzip();
fs.createReadStream(src).pipe(gzip).pipe(fs.createWriteStream(src+'.gz'))
}
// zip(path.join(__dirname,'./1.txt'));
// 解壓
function unzip(src){
const gunzip = zlib.createGunzip();
fs.createReadStream(src)
.pipe(gunzip)
.pipe(fs.createWriteStream(path.basename(src,'.gz')));
}
// unzip(path.join(__dirname,'./1.txt.gz'));
複製代碼
在HTTP中,咱們能夠根據請求頭來判斷要不要對傳輸的內容進行壓縮。
const http = require('http');
const path = require('path');
const fs = require('fs');
const zlib = require('zlib');
http.createServer(function (req, res) {
const p = path.join(__dirname, '1.txt');
// Accept-Encoding: gzip, deflate, br 客戶端
const header = req.headers['accept-encoding'];
res.setHeader('Content-Type','text/html;charset=utf8');
if (header) {
if (header.match(/\bgzip\b/)) {
const gzip = zlib.createGzip();
res.setHeader('Content-Encoding','gzip');
fs.createReadStream(p).pipe(gzip).pipe(res);
} else if (header.match(/\bdeflate\b/)) {
const deflate = zlib.createDeflate();
res.setHeader('Content-Encoding','deflate')
fs.createReadStream(p).pipe(deflate).pipe(res);
}else{
fs.createReadStream(p).pipe(res);
}
}else{
fs.createReadStream(p).pipe(res);
}
}).listen(8080);
複製代碼
crypto是node.js中實現加密和解密的模塊 在node.js中,使用OpenSSL類庫做爲內部實現加密解密的手段 OpenSSL是一個通過嚴格測試的可靠的加密與解密算法的實現工具
散列算法也叫哈希算法,用來把任意長度的輸入變換成固定長度的輸出,常見的有md5,sha1等,它有如下特色:
var crypto = require('crypto');
var md5 = crypto.createHash('md5');//返回哈希算法
var md5Sum = md5.update('hello');//指定要摘要的原始內容,能夠在摘要被輸出以前使用屢次update方法來添加摘要內容
var result = md5Sum.digest('hex');//摘要輸出,在使用digest方法以後不能再向hash對象追加摘要內容。
console.log(result);
複製代碼
屢次update
const fs = require('fs');
const shasum = crypto.createHash('sha1');//返回sha1哈希算法
const rs = fs.createReadStream('./readme.txt');
rs.on('data', function (data) {
shasum.update(data);//指定要摘要的原始內容,能夠在摘要被輸出以前使用屢次update方法來添加摘要內容
});
rs.on('end', function () {
const result = shasum.digest('hex');//摘要輸出,在使用digest方法以後不能再向hash對象追加摘要內容。
console.log(result);
})
複製代碼
HMAC算法將散列算法與一個密鑰結合在一塊兒,以阻止對簽名完整性的破壞。
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const m = crypto.createHmac('sha1', fs.readFileSync(path.join(__dirname,'./content.txt'))); //將content文件中的內容做爲一個密鑰
m.update('ok');
console.log(m.digest('hex'));
複製代碼
blowfish算法是一種對稱的加密算法,對稱的意思就是加密和解密使用的是同一個密鑰。
私鑰生成方法:
openssl genrsa -out rsa_private.key 1024
複製代碼
const crypto = require('crypto');
const fs = require('fs');
let str = 'hello';
const cipher = crypto.createCipher('blowfish', fs.readFileSync(path.join(__dirname, 'rsa_private.key')));
let encry = cipher.update(str, 'utf8','hex');
encry += cipher.final('hex');
console.log(encry);
// 對稱解密
const deciper = crypto.createDecipher('blowfish', fs.readFileSync(path.join(__dirname, 'rsa_private.key')));
let deEncry = deciper.update(encry, 'hex','utf8');
deEncry += deciper.final('utf8');
console.log(deEncry);
複製代碼
爲私鑰建立公鑰
openssl rsa -in rsa_private.key -pubout -out rsa_public.key
複製代碼
const crypto = require('crypto');
const fs = require('fs');
let key = fs.readFileSync(path.join(__dirname, 'rsa_private.key'));
let cert = fs.readFileSync(path.join(__dirname, 'rsa_public.key'));
let secret = crypto.publicEncrypt(cert, buffer);//公鑰加密
let result = crypto.privateDecrypt(key, secret);//私鑰解密
console.log(result.toString());
複製代碼
在網絡中,私鑰的擁有者能夠在一段數據被髮送以前先對數據進行簽名獲得一個簽名 經過網絡把此數據發送給數據接收者以後,數據的接收者能夠經過公鑰來對該簽名進行驗證,以確保這段數據是私鑰的擁有者所發出的原始數據,且在網絡中的傳輸過程當中未被修改。
let private = fs.readFileSync(path.join(__dirname, 'rsa_private.key'), 'ascii');
let public = fs.readFileSync(path.join(__dirname, 'rsa_public.key'), 'ascii');
let str = 'hello';
let sign = crypto.createSign('RSA-SHA256');
sign.update(str);
let signed = sign.sign(private, 'hex');
let verify = crypto.createVerify('RSA-SHA256');
verify.update(str);
let verifyResult = verify.verify(public,signed,'hex'); //true
複製代碼
強制緩存,在緩存數據未失效的狀況下,能夠直接使用緩存數據,那麼瀏覽器是如何判斷緩存數據是否失效呢? 咱們知道,在沒有緩存數據的時候,瀏覽器向服務器請求數據時,服務器會將數據和緩存規則一併返回,緩存規則信息包含在響應header中。
// 當訪問 localhost:8080/a.js
const http = require('http');
const url = require('url');
const path = require('path');
const fs = require('fs');
const mime = require('mime');
const server = http.createServer(function(req,res){
let {pathname} = url.parse(req.url);
console.log(pathname);
let p = path.join(__dirname,'public','.'+pathname);
// 這裏可能會存在問題,p有多是個目錄
fs.stat(p,function(err,stat){
if(!err){
sendFile(req,res,p);
}else{
sendError(res);
}
})
});
function sendError(res){
res.statusCode = 404;
res.end();
}
function sendFile(req,res,p){
let date = new Date(Date.now()+10*1000);
// res.setHeader('Expires',date.toUTCString());
res.setHeader('Cache-Control','max-age=10');
res.setHeader('Content-Type',mime.getType(p)+';charset=utf8')
fs.createReadStream(p).pipe(res);
}
server.listen(8080);
複製代碼
這種緩存結果通常有兩種:memory cache 與 disk cache。前者是從內存中讀,後者是從磁盤讀,相比之下,後者會稍微花點時間。
在查閱了一些資料以後,獲得的結論是:
Simple Test: Open Chrome Developper Tools / Network. Reload a page multiple times. The table column "Size" will tell you that some files are loaded "from memory cache". Now close the browser, open Developper Tools / Network again and load that page again. All cached files are loaded "from disk cache" now, because your memory cache is empty.
同這篇文章的結論:在命中強緩存的狀況下,進程初次渲染會從磁盤讀取緩存資源。Chrome會將部分資源保存到內存中
經過最後修改時間來對比:
const http = require('http');
const url = require('url');
const path = require('path');
const fs = require('fs');
const mime = require('mime');
const server = http.createServer(function (req, res) {
let { pathname } = url.parse(req.url);
let p = path.join(__dirname, 'public', '.' + pathname);
fs.stat(p, function (err, stat) {
// 根據修改時間判斷
// if-modified-since Last-Modified
if (!err) {
let since = req.headers['if-modified-since'];
if(since){
if(since === stat.ctime.toUTCString()){
res.statusCode = 304;
res.end();
}else{
sendFile(req,res,p,stat);
}
}else{
sendFile(req,res,p,stat);
}
} else {
sendError(res);
}
})
});
function sendError(res) {
res.statusCode = 404;
res.end();
}
function sendFile(req, res, p,stat) {
res.setHeader('Cache-Control','no-cache')
res.setHeader('Last-Modified',stat.ctime.toUTCString());
res.setHeader('Content-Type', mime.getType(p) + ';charset=utf8')
fs.createReadStream(p).pipe(res);
}
server.listen(8080);
複製代碼
最後修改時間這個方案並非太靠譜,它存在下面這些問題:
對此,咱們能夠採用ETag的方案。它是根據實體內容生成的一段hash字符串,能夠標識資源的狀態。當資源發生改變時,ETag也隨之發生變化。 ETag是Web服務端產生的,而後發給瀏覽器客戶端。
const http = require('http');
const url = require('url');
const path = require('path');
const fs = require('fs');
const mime = require('mime');
const crypto = require('crypto');
// 根據的是最新修改時間 這回根據的是文件的內容
// ETag:md5加密 / if-none-match
const server = http.createServer(function (req, res) {
let { pathname } = url.parse(req.url);
let p = path.join(__dirname, 'public', '.' + pathname);
fs.stat(p, function (err, stat) {
let md5 = crypto.createHash('md5');
let rs = fs.createReadStream(p);
rs.on('data',function(data){
md5.update(data);
});
rs.on('end',function(){
let r = md5.digest('hex'); // 當前文件的惟一標識
// 下次再拿最新文件的加密值 和客戶端請求來比較
let ifNoneMatch = req.headers['if-none-match'];
if(ifNoneMatch){
if(ifNoneMatch === r){
res.statusCode = 304;
res.end();
}else{
sendFile(req,res,p,r);
}
}else{
sendFile(req,res,p,r);
}
});
})
});
function sendError(res) {
res.statusCode = 404;
res.end();
}
function sendFile(req, res, p,r) {
res.setHeader('Cache-Control','no-cache')
res.setHeader('Etag',r);
res.setHeader('Content-Type', mime.getType(p) + ';charset=utf8')
fs.createReadStream(p).pipe(res);
}
server.listen(8080);
複製代碼
固然若是文件比較大,好比說1G,每次這樣操做,性能比較低,因此也能夠考慮stat.ctime+stat.size來對比,或者把前面兩個方案一塊兒加上。
第一次請求:
第二次請求:
根據上面的代碼,其實差很少能夠擼一個簡單版本出來。大概是有如下幾個部分:
在寫腳手架的過程當中,有兩個工具,不得不提一下。
上面一個是生成option,help的一些選項,能用使用者快速明白這個腳手架是幹嗎的,下面一個則是交互的問答式命令行。
爲個人博客打個廣告,歡迎訪問:小翼的前端天地