NodeJS使用Range請求實現(xiàn)下載功能的方法示例
前言
本篇使用 NodeJS 的 HTTP 服務(wù)創(chuàng)建客戶端,使用 Range 請求實現(xiàn)下載功能,并通過本篇的 Demo 擴展在業(yè)務(wù)中實現(xiàn)斷點續(xù)傳等功能的思路。
服務(wù)端的實現(xiàn)
我們通過 http 模塊創(chuàng)建服務(wù)器處理 Range 請求,在服務(wù)器代碼中我們?yōu)榱藴p少回調(diào)嵌套使用 async 函數(shù),所以需要將異步的操作方法轉(zhuǎn)換成 Promise,以往我們使用 util 的 promisify 來一個一個轉(zhuǎn)換異步方法,比較麻煩,我們這次使用第三方模塊 mz 并直接引入轉(zhuǎn)換好的替代模塊。
使用 mz 之前需要先安裝:
npm install mz
服務(wù)端代碼如下:
// 文件:server.js
const http = require("http");
const path = require("path");
const url = require("url");
// 引入 mz 模塊轉(zhuǎn)換成 Promise 的 fs 模塊
const fs = require("mz/fs");
// 請求處理函數(shù)
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 請求頭將返回范圍請求的數(shù)據(jù)
if (range) {
// 獲取范圍請求的開始和結(jié)束位置
let [, start, end] = range.match(/(\d*)-(\d*)/);
// 錯誤處理
try {
let statObj = await fs.stat(p);
} catch (e) {
res.end("Not Found");
}
// 文件總字節(jié)數(shù)
let total = statObj.size;
// 處理請求頭中范圍參數(shù)不傳的問題
start = start ? ParseInt(start) : 0;
end = end ? ParseInt(end) : total - 1;
// 響應(yīng)客戶端
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 請求頭時將整個文件內(nèi)容返回給客戶端
fs.createReadStream(p).pipe(res);
}
}
// 創(chuàng)建服務(wù)器
const server = http.createServer(listener);
// 監(jiān)聽端口
server.listen(3000, () => {
console.log("server start 3000");
});
在上面服務(wù)端的代碼中,需要兼容 Range 請求和普通請求,兩種請求的區(qū)別是,如果客戶端發(fā)送的是 Range 請求,會攜帶 Range:bytes=0-5 格式的請求頭,我們可以通過 req 的 headers 屬性獲取,在獲取請求頭時,原本大寫字母開頭 NodeJS 統(tǒng)一處理成小寫,所以獲取時應(yīng)小寫。
如果是 Range 請求則通過可讀流讀取對應(yīng)的內(nèi)容返回客戶端,如果不是,則通過可讀流讀取整個文件返回客戶端,在響應(yīng) Range 請求的過程中需要設(shè)置響應(yīng)狀態(tài)為 206,需要設(shè)置響應(yīng)頭 Accept-Ranges 值為 bytes,需要設(shè)置響應(yīng)頭 Content-Range 值為 byte 0-5/100 的格式,0 為返回數(shù)據(jù)開始的索引,5 為結(jié)束的索引(包含),100 為文件的總字節(jié)數(shù)。
在通過 url 和 path 模塊解析和拼接下載文件路徑時,應(yīng)該進行錯誤檢測,如果文件不存在則直接返回客戶端 Not Found。
我們可以使用 curl 命令來檢測我們的服務(wù)端代碼,在命令行工具中輸入下面命令,在命令窗口查看返回值是否正確。
curl -v --header "Range:bytes=0-5" http://localhost:3000
客戶端的實現(xiàn)
在上面使用 curl 命令來訪問我們的服務(wù)器時,只能請求固定范圍的數(shù)據(jù),而不是類似于下載功能,每次都下載一個范圍的數(shù)據(jù),但是想要多次下載并自動維護 Range 的范圍需要借助我們自己實現(xiàn)的客戶端邏輯。
為了簡便,我們的下載客戶端是在命令行窗口運行的,通過指令來模擬實際項目中的開始下載、暫停和恢復按鈕,當在窗口中輸入 s 指令時開始下載,輸入 p 指令時暫停下載,輸入 r 指令時恢復下載。
// 文件: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; // 每次請求字符個數(shù)
let pause = false; // 暫停狀態(tài)
let total; // 文件總長度
// 創(chuàng)建可寫流
let ws = fs.createWriteStream(path.resolve(__dirname, config.path.slice(1)));
// 下載函數(shù)
function download() {
// 配置,每次范圍請求 step 個字節(jié)
config.headers = {
"Range": `bytes=${start}-${start + step - 1}`;
};
// 維護下次 start 的值
start += step;
// 發(fā)送請求
http.request(config, res => {
// 獲取文件總長度
if (typeof total !== "number") {
total = res.headers["content-ranges"].match(/\/(\d*)/)[1];
}
// 讀取返回數(shù)據(jù)
let buffers = [];
res.on("data", data => buffers.push(data));
res.on("end", () => {
// 合并數(shù)據(jù)并寫入文件
let buf = Buffer.concat(buffers);
ws.write(buf);
// 遞歸進行下一次請求
if (!pause && start < total) {
download();
}
});
}).end();
}
// 監(jiān)控輸入
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 屬性配置,每次調(diào)用 download 函數(shù)下載時都會重新計算當前范圍請求的初始位置和結(jié)束位置,并設(shè)置 Range 請求頭,下一次請求靠遞歸 download 來實現(xiàn)。
在執(zhí)行時需先啟動我們的服務(wù)器,在通過命令行輸入 node client.js 來啟動客戶端,在命令窗口輸入對應(yīng)的指令進行開始下載、暫停下載和恢復下載操作。
總結(jié)
相信現(xiàn)在已經(jīng)了解什么是范圍請求,范圍請求客戶端和服務(wù)端需要做些什么,其實說白了就是對應(yīng)的請求頭和響應(yīng)頭的使用,需要注意的是范圍請求的響應(yīng)狀態(tài)碼為 206,這樣的需求在一些上傳、下載資源的網(wǎng)站也很常見,其目的就是為了讓我們實現(xiàn)斷點續(xù)傳,不至于一次沒有上傳或下載完成的資源文件,在下一次的做同樣操作時需要重新來過,可以接著上次的位置繼續(xù),范圍請求在視頻網(wǎng)站上也廣泛應(yīng)用,邊請求邊觀看,不至于一次加載整個視頻資源,節(jié)省流量,節(jié)省時間。
以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Windows 系統(tǒng)下設(shè)置Nodejs NPM全局路徑
這篇文章主要介紹了Windows 系統(tǒng)下設(shè)置Nodejs NPM全局路徑2016-04-04
在windows上用nodejs搭建靜態(tài)文件服務(wù)器的簡單方法
這篇文章主要介紹了在windows上用nodejs搭建靜態(tài)文件服務(wù)器的簡單方法,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2016-08-08
Nodejs+express+html5 實現(xiàn)拖拽上傳
文件上傳是一個比較常見的功能,傳統(tǒng)的選擇方式的上傳比較麻煩,需要先點擊上傳按鈕,然后再找到文件的路徑,然后上傳。給用戶體驗帶來很大問題。html5開始支持拖拽上傳的需要的api。nodejs也是一個最近越來越流行的技術(shù),這也是自己第一次接觸nodejs。2014-08-08
nodejs npm錯誤Error:UNKNOWN:unknown error,mkdir ''D:\Develop\n
今天小編就為大家分享一篇關(guān)于nodejs npm錯誤Error:UNKNOWN:unknown error,mkdir 'D:\Develop\nodejs\node_global'at Error,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧2019-03-03
Node.js 實現(xiàn)搶票小工具 & 短信通知提醒功能
這篇文章主要介紹了Node.js 實現(xiàn)搶票小工具 & 短信通知提醒功能,本文通過實例代碼給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友可以參考下2019-10-10

