本篇使用 NodeJS 的 HTTP 服務建立客戶端,使用 Range 請求實現下載功能,並經過本篇的 Demo 擴展在業務中實現斷點續傳等功能的思路。node
咱們經過 http
模塊建立服務器處理 Range 請求,在服務器代碼中咱們爲了減小回調嵌套使用 async
函數,因此須要將異步的操做方法轉換成 Promise,以往咱們使用 util
的 promisify
來一個一個轉換異步方法,比較麻煩,咱們此次使用第三方模塊 mz
並直接引入轉換好的替代模塊。npm
使用 mz
以前須要先安裝:bash
npm install mz服務器
服務端代碼以下:curl
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
格式的請求頭,咱們能夠經過 req
的 headers
屬性獲取,在獲取請求頭時,本來大寫字母開頭 NodeJS 統一處理成小寫,因此獲取時應小寫。異步
若是是 Range 請求則經過可讀流讀取對應的內容返回客戶端,若是不是,則經過可讀流讀取整個文件返回客戶端,在響應 Range 請求的過程當中須要設置響應狀態爲 206
,須要設置響應頭 Accept-Ranges
值爲 bytes
,須要設置響應頭 Content-Range
值爲 byte 0-5/100
的格式,0
爲返回數據開始的索引,5
爲結束的索引(包含),100
爲文件的總字節數。async
在經過 url
和 path
模塊解析和拼接下載文件路徑時,應該進行錯誤檢測,若是文件不存在則直接返回客戶端 Not Found
。函數
咱們可使用 curl
命令來檢測咱們的服務端代碼,在命令行工具中輸入下面命令,在命令窗口查看返回值是否正確。工具
curl -v --header 「Range:bytes=0-5」 http://localhost:3000網站
在上面使用 curl
命令來訪問咱們的服務器時,只能請求固定範圍的數據,而不是相似於下載功能,每次都下載一個範圍的數據,可是想要屢次下載並自動維護 Range 的範圍須要藉助咱們本身實現的客戶端邏輯。
爲了簡便,咱們的下載客戶端是在命令行窗口運行的,經過指令來模擬實際項目中的開始下載、暫停和恢復按鈕,當在窗口中輸入 s
指令時開始下載,輸入 p
指令時暫停下載,輸入 r
指令時恢復下載。
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
,這樣的需求在一些上傳、下載資源的網站也很常見,其目的就是爲了讓咱們實現斷點續傳,不至於一次沒有上傳或下載完成的資源文件,在下一次的作一樣操做時須要從新來過,能夠接着上次的位置繼續,範圍請求在視頻網站上也普遍應用,邊請求邊觀看,不至於一次加載整個視頻資源,節省流量,節省時間。