經過手寫文件服務器,說說先後端交互

前言

      最近用node寫了一個靜態文件服務器(已發佈),想經過這個小例子說說先後端基於HTTP協議交互過程當中的一些常見問題。html

代碼地址

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

安裝方法

       npm install static-server2 -gnode

node版本

       使用了async函數,支持版本7.6以上git

用法示例

      按照前言的安裝法安裝到全局後,命令行執行 server-start後,會提示服務啓動成功。這時能夠訪問 localhost:8080,程序有如下兩個功能:

託管靜態文件

      服務啓動成功後能夠訪問localhost:8080查看根目錄下的靜態文件。
      命令行啓動時能夠經過server-start -d來改變根目錄。還能夠經過-o參數配置主機,-p參數配置端口,-h參數查看幫助。github

文件上傳

      支持上傳文件,能夠經過暫停進行斷點續傳。算法

說說緩存

      下面我就基於這個例子說說先後端交互過程當中的幾個問題。首先說說緩存,下面先上代碼。
      這是例子中根目錄下index.js文件中的一個方法。這個方法用來過濾請求,若是命中緩存,返回304,未命中則返回新的資源。
      這個函數處理了強制緩存和對比緩存。npm

//緩存處理函數
    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

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

一、在集羣服務器上各個服務器上的文件時間可能不一樣。
二、有可能文件作了更新,可是內容沒有變化。
三、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來告訴服務器我所上傳的文件的內容區間。固然斷點續傳在不一樣的場景下也有不一樣的處理方法。這裏只是基於這種簡單場景作個示範。
      前端邏輯是這樣的:
      一、獲取用戶要上傳的文件
      二、切割文件,獲取到要上傳的第一部分
      三、調用後臺的上傳文件接口,上傳這一部分
      四、接口返回成功後再切割文件,上傳第二部分
      五、每次上傳用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);
複製代碼

      下面看一下後端的處理邏輯:
      一、獲取文件名
      二、經過Range獲取文件位置,若是是0開頭,說明是第一次上傳,刪除以前的文件
      三、寫入文件
      下面是核心代碼:

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查看,程序運行中出現任何問題也歡迎指正。

相關文章
相關標籤/搜索