HTTP從入門到放棄

henryzp發表於2018-04-18

基本知識

  • 請求的一方叫客戶端,響應的一方叫伺服器端
  • 通過請求和響應達成通訊
  • HTTP是一種不儲存狀態的協議

請求報文

alt

常見的方法有:

  • GET 獲取資源
  • POST 向伺服器端傳送資料,傳輸實體主體
  • PUT 傳輸檔案
  • HEAD 獲取報文首部
  • DELETE 刪除檔案
  • OPTIONS 詢問支援的方法
  • TRACE 追蹤路徑

響應報文

alt

狀態碼

類別 說明
1XX Informational(資訊性狀態碼
2XX Success(成功狀態碼)
3XX Redirection(重定向)
4XX Client Error(客戶端錯誤狀態碼)
5XX Server Error(伺服器錯誤狀態嗎)

2XX 成功

  • 200(OK 客戶端發過來的資料被正常處理
  • 204(Not Content 正常響應,沒有實體
  • 206(Partial Content 範圍請求,返回部分資料,響應報文中由Content-Range指定實體內容

3XX 重定向

  • 301(Moved Permanently) 永久重定向
  • 302(Found) 臨時重定向,規範要求方法名不變,但是都會改變
  • 303(See Other) 和302類似,但必須用GET方法
  • 304(Not Modified) 狀態未改變 配合(If-Match、If-- Modified-Since、If-None_Match、If-Range、If-Unmodified-Since)
  • 307(Temporary Redirect) 臨時重定向,不該改變請求方法

4XX 客戶端錯誤

  • 400(Bad Request) 請求報文語法錯誤
  • 401 (unauthorized) 需要認證
  • 403(Forbidden) 伺服器拒絕訪問對應的資源
  • 404(Not Found) 伺服器上無法找到資源

5XX 伺服器端錯誤

  • 500(Internal Server Error)伺服器故障
  • 503(Service Unavailable) 伺服器處於超負載或正在停機維護

headers

通用首部欄位

首部欄位名 說明
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),這些是必須要掌握的知識。

建立http

  • 方式一:
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);
複製程式碼

我們通過翻原始碼,會發現上面兩種寫法的一致性:

alt

alt

url模組

可以通過它來處理請求的url,簡單demo如下:

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); //  路徑
複製程式碼

res

在上一篇的最後內容中,有提及req的一些屬性,那麼我們來看一下res有哪些方法吧。

write && end

可以使用write方法傳送響應內容

response.write(chunk,[encoding]);
response.end([chunk],[encoding]);
複製程式碼

writeHead

res.writeHead(statusCode, [headers]);
複製程式碼

setHeader

res.setHeader(key, value);
複製程式碼

它與writeHead的區別是:它不會真正的把響應頭寫給客戶端。

即在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的那個):

alt

多語言切換

可以通過Accept-Language檢測瀏覽器的語言

  • 請求頭格式 Accept-Language: Accept-Language:zh-CN,zh;q=0.9
  • 響應頭格式 Content-Language:zh-CN
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空間圖片比較常見,引用過去之後會變成裂圖。

實現原理:

  • 從一個網站跳轉,或者網頁引用到某個資原始檔時,HTTP請求中帶有Referer表示來源網頁的URL
  • 通過檢查請求頭中的Referer來判斷來源網頁的域名
  • 如果來源域名不在白名單內,則返回錯誤提示 用瀏覽器直接訪問圖片地址是沒有referer的
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支援的話,客戶端應該記錄了之前已經讀取的檔案範圍,網路恢復之後,則向伺服器傳送讀取剩餘Range的請求,服務端只需要傳送客戶端請求的那部分內容,而不用整個檔案傳送回客戶端,以此節省網路頻寬。

獲取部分內容的範圍請求

為了實現中斷恢復下載的需求,需要能下載指定下載的實體範圍

  • 請求頭中的Range來指定 資源的byte範圍
  • 響應會返回狀態碼206響應報文
  • 對於多重範圍的範圍請求,響應會在首部欄位Content-Type中標明multipart/byteranges

alt

alt

如何應用

// 服務端
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

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演算法

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演算法是一種對稱的加密演算法,對稱的意思就是加密和解密使用的是同一個金鑰。

alt

私鑰生成方法:

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);
複製程式碼

非對稱加密

  • 非對稱加密演算法需要兩個金鑰:公開金鑰(publickey)和私有金鑰(privatekey)
  • 公鑰與私鑰是一對,如果用公鑰對資料進行加密,只有用對應的私鑰才能解密,如果私鑰加密,只能公鑰解密
  • 因為加密和解密使用的是兩個不同的金鑰,所以這種演算法叫作非對稱加密演算法

alt

為私鑰建立公鑰

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());
複製程式碼

簽名

在網路中,私鑰的擁有者可以在一段資料被髮送之前先對資料進行簽名得到一個簽名 通過網路把此資料傳送給資料接收者之後,資料的接收者可以通過公鑰來對該簽名進行驗證,以確保這段資料是私鑰的擁有者所發出的原始資料,且在網路中的傳輸過程中未被修改。

alt

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會將部分資源儲存到記憶體中

對比快取

  • 對比快取,顧名思義,需要進行比較判斷是否可以使用快取。
  • 瀏覽器第一次請求資料時,伺服器會將快取標識與資料一起返回給客戶端,客戶端將二者備份至快取資料庫中。
  • 再次請求資料時,客戶端將備份的快取標識傳送給伺服器,伺服器根據快取標識進行判斷,判斷成功後,返回304狀態碼,通知客戶端比較成功,可以使用快取資料。

通過最後修改時間來對比:

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);
複製程式碼

最後修改時間這個方案並不是太靠譜,它存在下面這些問題:

  • 某些伺服器不能精確得到檔案的最後修改時間, 這樣就無法通過最後修改時間來判斷檔案是否更新了。
  • 某些檔案的修改非常頻繁,在秒以下的時間內進行修改. Last-Modified只能精確到秒。
  • 一些檔案的最後修改時間改變了,但是內容並未改變。 我們不希望客戶端認為這個檔案修改了。
  • 如果同樣的一個檔案位於多個CDN伺服器上的時候內容雖然一樣,修改時間不一樣。

對此,我們可以採用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來對比,或者把前面兩個方案一起加上。

請求流程

第一次請求:

alt

第二次請求:

alt

Cache-Control

  • private 客戶端可以快取
  • public 客戶端和代理伺服器都可以快取
  • max-age=60 快取內容將在60秒後失效
  • no-cache 需要使用對比快取驗證資料,強制向源伺服器再次驗證
  • no-store 所有內容都不會快取,強制快取和對比快取都不會觸發

實現一個靜態伺服器的腳手架

根據上面的程式碼,其實差不多可以擼一個簡單版本出來。大概是有以下幾個部分:

  • 判斷URL是資料夾還是檔案,如果是檔案,則跳到下一步,資料夾的話,顯示一個列表即可
  • 判斷該檔案是否快取過,如果有快取,直接讀取快取內容,沒有(需要設定一個頭,如etag等),則跳到下一步
  • gzip壓縮檔案
  • 支援Range的功能

在寫腳手架的過程中,有兩個工具,不得不提一下。

上面一個是生成option,help的一些選項,能用使用者快速明白這個腳手架是幹嘛的,下面一個則是互動的問答式命令列。

alt

最後的最後

為我的部落格打個廣告,歡迎訪問:小翼的前端天地

相關文章