本篇使用 NodeJS 的 HTTP 服務建立客戶端,使用 Range 請求實現下載功能,並經過本篇的 Demo 擴展在業務中實現斷點續傳等功能的思路。node
咱們經過 http
模塊建立服務器處理 Range 請求,在服務器代碼中咱們爲了減小回調嵌套使用 async
函數,因此須要將異步的操做方法轉換成 Promise,以往咱們使用 util
的 promisify
來一個一個轉換異步方法,比較麻煩,咱們此次使用第三方模塊 mz
並直接引入轉換好的替代模塊。npm
使用 mz
以前須要先安裝:服務器
npm install mz
服務端代碼以下:curl
// 文件:server.js 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
指令時恢復下載。ui
// 文件:client.js 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
,這樣的需求在一些上傳、下載資源的網站也很常見,其目的就是爲了讓咱們實現斷點續傳,不至於一次沒有上傳或下載完成的資源文件,在下一次的作一樣操做時須要從新來過,能夠接着上次的位置繼續,範圍請求在視頻網站上也普遍應用,邊請求邊觀看,不至於一次加載整個視頻資源,節省流量,節省時間。