基於NodeJS的HTTP server Plus 1:Range (範圍請求)

前言

本篇使用 NodeJS 的 HTTP 服務建立客戶端,使用 Range 請求實現下載功能,並經過本篇的 Demo 擴展在業務中實現斷點續傳等功能的思路。node

服務端的實現

咱們經過 http 模塊建立服務器處理 Range 請求,在服務器代碼中咱們爲了減小回調嵌套使用 async 函數,因此須要將異步的操做方法轉換成 Promise,以往咱們使用 utilpromisify 來一個一個轉換異步方法,比較麻煩,咱們此次使用第三方模塊 mz 並直接引入轉換好的替代模塊。npm

使用 mz 以前須要先安裝:bash

npm install mz服務器

服務端代碼以下:curl

文件:server.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
複製代碼
const http = require("http");
const path = require("path");
const url = require("url");

// 引入 mz 模塊轉換成 Promise 的 fs 模塊
const fs = require("mz/fs");

// 請求處理函數
async function listener(req, res) {
    // 獲取 range 請求頭,格式爲 Range:bytes=0-5
    let range = req.headers["range"];

    // 下載文件路徑
    let p = path.resovle(__dirname, url.parse(url, true).pathname);

    // 存在 range 請求頭將返回範圍請求的數據
    if (range) {
        // 獲取範圍請求的開始和結束位置
        let [, start, end] = range.match(/(\d*)-(\d*)/);

        // 錯誤處理
        try {
            let statObj = await fs.stat(p);
        } catch (e) {
            res.end("Not Found");
        }

        // 文件總字節數
        let total = statObj.size;

        // 處理請求頭中範圍參數不傳的問題
        start = start ? ParseInt(start) : 0;
        end = end ? ParseInt(end) : total - 1;

        // 響應客戶端
        res.statusCode = 206;
        res.setHeader("Accept-Ranges", "bytes");
        res.setHeader("Content-Range", `bytes ${start}-${end}/${total}`);
        fs.createReadStream(p, { start, end }).pipe(res);
    } else {
        // 沒有 range 請求頭時將整個文件內容返回給客戶端
        fs.createReadStream(p).pipe(res);
    }
}

// 建立服務器
const server = http.createServer(listener);

// 監聽端口
server.listen(3000, () => {
    console.log("server start 3000");
});
複製代碼

在上面服務端的代碼中,須要兼容 Range 請求和普通請求,兩種請求的區別是,若是客戶端發送的是 Range 請求,會攜帶 Range:bytes=0-5 格式的請求頭,咱們能夠經過 reqheaders 屬性獲取,在獲取請求頭時,本來大寫字母開頭 NodeJS 統一處理成小寫,因此獲取時應小寫。異步

若是是 Range 請求則經過可讀流讀取對應的內容返回客戶端,若是不是,則經過可讀流讀取整個文件返回客戶端,在響應 Range 請求的過程當中須要設置響應狀態爲 206,須要設置響應頭 Accept-Ranges 值爲 bytes,須要設置響應頭 Content-Range 值爲 byte 0-5/100 的格式,0 爲返回數據開始的索引,5 爲結束的索引(包含),100 爲文件的總字節數。async

在經過 urlpath 模塊解析和拼接下載文件路徑時,應該進行錯誤檢測,若是文件不存在則直接返回客戶端 Not Found函數

咱們可使用 curl 命令來檢測咱們的服務端代碼,在命令行工具中輸入下面命令,在命令窗口查看返回值是否正確。工具

curl -v --header 「Range:bytes=0-5」 http://localhost:3000網站

客戶端的實現

在上面使用 curl 命令來訪問咱們的服務器時,只能請求固定範圍的數據,而不是相似於下載功能,每次都下載一個範圍的數據,可是想要屢次下載並自動維護 Range 的範圍須要藉助咱們本身實現的客戶端邏輯。

爲了簡便,咱們的下載客戶端是在命令行窗口運行的,經過指令來模擬實際項目中的開始下載、暫停和恢復按鈕,當在窗口中輸入 s 指令時開始下載,輸入 p 指令時暫停下載,輸入 r 指令時恢復下載。

文件:client.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
複製代碼
const http = require("http");
const fs = require("fs");
const path = require("path");

// 請求配置
let config = {
    host: "localhost",
    port: 3000,
    path: "/download.txt"
};

let start = 0; // 請求初始值
let step = 5; // 每次請求字符個數
let pause = false; // 暫停狀態
let total; // 文件總長度

// 建立可寫流
let ws = fs.createWriteStream(path.resolve(__dirname, config.path.slice(1)));

// 下載函數
function download() {
    // 配置,每次範圍請求 step 個字節
    config.headers = {
        "Range": `bytes=${start}-${start + step - 1}`;
    };

    // 維護下次 start 的值
    start += step;

    // 發送請求
    http.request(config, res => {
        // 獲取文件總長度
        if (typeof total !== "number") {
            total = res.headers["content-ranges"].match(/\/(\d*)/)[1];

        }

        // 讀取返回數據
        let buffers = [];
        res.on("data", data => buffers.push(data));
        res.on("end", () => {
            // 合併數據並寫入文件
            let buf = Buffer.concat(buffers);
            ws.write(buf);

            // 遞歸進行下一次請求
            if (!pause && start < total) {
                download();
            }
        });
    }).end();
}

// 監控輸入
process.stdin.on("data", data => {
    // 獲取指令
    let ins = data.toString().match(/(\w*)\/r/)[1];
    switch (ins) {
        case "s":
        case "r":
            pause = false;
            download();
            break;
        case "p":
            pause = true;
            break;
    }
});
複製代碼

在上面代碼中下載的文件經過 config 中的 path 屬性配置,每次調用 download 函數下載時都會從新計算當前範圍請求的初始位置和結束位置,並設置 Range 請求頭,下一次請求靠遞歸 download 來實現。

在執行時需先啓動咱們的服務器,在經過命令行輸入 node client.js 來啓動客戶端,在命令窗口輸入對應的指令進行開始下載、暫停下載和恢復下載操做。

總結

相信如今已經瞭解什麼是範圍請求,範圍請求客戶端和服務端須要作些什麼,其實說白了就是對應的請求頭和響應頭的使用,須要注意的是範圍請求的響應狀態碼爲 206,這樣的需求在一些上傳、下載資源的網站也很常見,其目的就是爲了讓咱們實現斷點續傳,不至於一次沒有上傳或下載完成的資源文件,在下一次的作一樣操做時須要從新來過,能夠接着上次的位置繼續,範圍請求在視頻網站上也普遍應用,邊請求邊觀看,不至於一次加載整個視頻資源,節省流量,節省時間。

原文出自:https://www.pandashen.com

相關文章
相關標籤/搜索