通過手寫檔案伺服器,說說前後端互動

寒東設計師發表於2018-03-06

前言

      最近用node寫了一個靜態檔案伺服器(已釋出),想通過這個小例子說說前後端基於HTTP協議互動過程中的一些常見問題。

程式碼地址

       https://github.com/alive1541/static-server
       下文中所貼出來的程式碼都在這個目錄下。

安裝方法

       npm install static-server2 -g

node版本

       使用了async函式,支援版本7.6以上

用法示例

通過手寫檔案伺服器,說說前後端互動
      按照前言的安裝法安裝到全域性後,命令列執行server-start後,會提示服務啟動成功。這時可以訪問localhost:8080,程式有以下兩個功能:

託管靜態檔案

      服務啟動成功後可以訪問localhost:8080檢視根目錄下的靜態檔案。
      命令列啟動時可以通過server-start -d來改變根目錄。還可以通過-o引數配置主機,-p引數配置埠,-h引數檢視幫助。

檔案上傳

      支援上傳檔案,可以通過暫停進行斷點續傳。

說說快取

      下面我就基於這個例子說說前後端互動過程中的幾個問題。首先說說快取,下面先上程式碼。
      這是例子中根目錄下index.js檔案中的一個方法。這個方法用來過濾請求,如果命中快取,返回304,未命中則返回新的資源。
      這個函式處理了強制快取和對比快取。

//快取處理函式
    handleCatch(req, res, fileStat) {
        //強制快取
        res.setHeader('Expries', new Date(Date.now() + 30 * 1000).toGMTString())
        res.setHeader('Catch-Control', 'private,max-age=30')
        //對比快取
        let ifModifiedSince = req.headers['if-modified-since']
        let ifNoneMatch = req.headers['if-none-match']
        let lastModified = fileStat.ctime.toGMTString()
        let eTag = fileStat.mtime.toGMTString()
        res.setHeader('Last-Modified', lastModified)
        res.setHeader('ETag', eTag)
        //任何一個對比快取頭不匹配,則不走快取
        if (ifModifiedSince && ifModifiedSince != lastModified) {
            return false
        }
        if (ifNoneMatch && ifNoneMatch != eTag) {
            return false
        }
        //當請求中存在任何一個對比快取頭,則返回304,否則不走快取
        if (ifModifiedSince || ifNoneMatch) {
            res.writeHead(304)
            res.end()
            return true
        } else {
            return false
        }
    }
複製程式碼

強制快取

      強制快取的好處是瀏覽器不需要傳送HTTP請求,一般不常更改的頁面都會設定一個較長的強制快取。
      可以通過清理瀏覽器快取和強制重新整理頁面(ctrl+F5)來跳過它強制請求資料。它主要是靠兩個HTTP頭來實現。

Cache-Control 和 Expires

      這兩個頭的作用是一樣的。都是告訴瀏覽器多長時間以內可以不傳送請求而是直接使用本地的快取。Cache-Control是HTTP1.1版本規範,而Expires是HTTP1.0版本規範,所以同時存在的話Catch-Control的優先順序更高。
      一般都是像我上面的程式碼一樣,兩個都設定。因為低版本瀏覽器不支援Cache-Control
      此外,Catch-Control還有更加細緻的配置項,可以更加精確的進行一些控制,規則如下:

public:客戶端和代理伺服器都可快取
private:僅客戶端可以快取,代理伺服器不可快取
no-cache:禁止強制快取
no-store:禁止強制快取和對比快取
must-revalidation/proxy-revalidation:如果快取的內容失效,請求必須傳送到伺服器/代理以進行重新驗證
max-age=xxx:快取的內容將在 xxx 秒後失效

對比快取

Last-Modified/If-Modified-Since

      Last-Modified是伺服器攜帶的頭,它代表這個資源的最後更新時間。
      If-Modified-Since是客戶端攜帶的頭。在瀏覽器中,如果不是第一次請求這個資源瀏覽器就會傳送這個頭。前提是上一次伺服器返回的頭中有Last-Modified,它的值也是上次返回的Last-Modified的值。

Etag/If-None-Match

      這兩個頭和上面的兩個頭的目的一樣,都是校驗資源。它們出現的目的是為了解決上面兩個頭存在的一些問題。例如:

1、在叢集伺服器上各個伺服器上的檔案時間可能不同。
2、有可能檔案做了更新,但是內容沒有變化。
3、last-modified時間精度為秒,如果檔案存在毫秒級的修改,last-modified不能識別

      ETag是資源標籤。如果資源沒有變化它就不會變。這樣就解決了上面說的三個問題。
      但是ETag解決問題的同時也創造出了新的問題,計算出ETag讀取檔案內容,這就會耗費額外的效能和時間。所以它並不能完全取代Last-Modified,需要根據實際需要權衡使用。
      在實際的開發中ETag的演算法也各不相同,像我在例子中的直接使用了mtime。

說說壓縮

通過手寫檔案伺服器,說說前後端互動
      如圖,瀏覽器每次傳送請求都會攜帶自己支援的壓縮型別,最常用的兩種是gzip和deflate。
      服務端可以根據Accept-Ecoding頭來返回響應的壓縮資源,同時設定Content-Encoding頭告訴瀏覽器你用了什麼壓縮方式,程式碼如下:

//處理壓縮
    handleZlib(req, res) {
        let acceptEncoding = req.headers['accept-encoding']
        if (/\bgzip\b/g.test(acceptEncoding)) {
            res.setHeader('Content-Encoding', 'gzip');
            //zlib是node的一個模組
            return zlib.createGzip()
        } else if (/\bdeflate\b/g.test(acceptEncoding)) {
            res.setHeader('Content-Encoding', 'deflate');
            return zlib.createDeflate()
        } else {
            return null
        }
    }
複製程式碼

說說斷點續傳

      先看程式碼,斷點續傳的原理就是利用HTTP頭中的Range來告訴伺服器我所上傳的檔案的內容區間。當然斷點續傳在不同的場景下也有不同的處理方法。這裡只是基於這種簡單場景做個示範。
      前端邏輯是這樣的:
      1、獲取使用者要上傳的檔案
      2、切割檔案,獲取到要上傳的第一部分
      3、呼叫後臺的上傳檔案介面,上傳這一部分
      4、介面返回成功後再切割檔案,上傳第二部分
      5、每次上傳用Range頭髮送檔案的位元組區間
      下面是切割檔案和xhr上傳的程式碼,完整程式碼在專案目錄/src/template/list.html中(使用了handlebars模版引擎)。

    if (end > file.size) {
        end = file.size
    }
    //切割檔案
    var blob = file.slice(start, end)
    var formData = new FormData();
    formData.append('filechunk', blob);
    formData.append('filename', file.name);
    //新增Range頭
    var range = 'bytes=' + start + '-' + end
    xhr.setRequestHeader('Range', range)
    //傳送
    xhr.send(formData);
複製程式碼

      下面看一下後端的處理邏輯:
      1、獲取檔名
      2、通過Range獲取檔案位置,如果是0開頭,說明是第一次上傳,刪除之前的檔案
      3、寫入檔案
      下面是核心程式碼:

let path = require('path')
let fs = require('fs')
function handleFile(req, res, fields, files, filepath) {
    //獲取檔名
    let name = fields.filename[0]
    //檔案讀取路徑
    let rdPath = files.filechunk[0].path
    //檔案寫入路徑
    let wsPath = path.join(filepath, name)
    //通過range判斷上傳檔案的位置
    let range = req.headers['range']
    let start = 0
    if (range) {
        start = range.split('=')[1].split('-')[0]
    }
    //從multiparty外掛中讀取檔案內容,然後寫入本地檔案
    let buf = fs.readFileSync(rdPath)
    fs.exists(wsPath, function (exists) {
        //如果是初次上傳,刪除public下的同名檔案
        if (exists && start == 0) {
            fs.unlink(wsPath, function () {
                fs.writeFileSync(wsPath, buf, { flag: 'a+' })
                res.end()
            })
        } else {
            fs.writeFileSync(wsPath, buf, { flag: 'a+' })
            res.end()
        }
    })

}
module.exports = handleFile
複製程式碼

      我這裡處理相對粗糙,實際的專案需求可能不止這麼簡單,但都是基於Range頭做相應的處理,希望我的描述能對大家有些幫助。

總結

      文章到這裡就結束了,上文引用的都是程式碼片段,只是為了展示處理邏輯,如果有興趣可以去gitHub檢視,程式執行中出現任何問題也歡迎指正。

相關文章