最近進行一次下載請求,想使用onprogress顯示進度時發現,onprogress中顯示的total總爲0。html
爲何呢?nginx
不知道你們有沒有遇到過有時候用下載軟件下載文件的時候,有些下載能夠顯示總大小,有些不可顯示,看着就好像出了bug同樣。其實這緣由和onprogress中顯示的total總爲0的狀況差很少。ajax
想要弄清楚緣由這時候要先了解下載文件的原理,而一般的文件下載是可斷點續傳,能夠從這方面入手。瀏覽器
爲了讓文件下載能夠暫停而後從新從暫停下載部分開始從新下載,這時候就要去了解HTTP中的content-length
、Accept-Ranges
、Content-Range
還有Range
。bash
content-length
:用於響應頭,表示響應內容的字節大小服務器
Accept-Ranges
:用於響應頭,告知客戶端能夠進行範圍請求,後面的值表示返回的內容單位,一般是bytes,如:Accept-Ranges:bytesapp
Content-Range
: 用於響應頭,用於描述響應請求內容的範圍和總體長度,好比Content-Range: bytes 201-220/326
表示服務器端返回請求資源中的201到220bytes範圍的內容,請求資源總大小爲326字節,若是總大小未知就會顯示Content-Range: bytes 201-220/\*
框架
Range
:用於請求頭,做用是告知服務器端返回哪一部分的內容,好比Range:bytes=500-1000
表示告知服務器我要拿這個文件中500至1000字節的內容。koa
利用HTTP
中的Range
、Content-Range
就能夠實現斷點下載。async
咱們能夠用ajax
來模擬一下斷點下載,代碼以下,其中請求的是nginx
服務器的一個index.html
文件。
let entryContentLength = 0,
entryContent = "";
getContentLength("http://localhost:8083/test/index.html").then(res => {
if (res) {
entryContentLength = res;
} else {
entryContentLength = "沒法獲知長度";
}
sectionDownload(0, 20, "http://localhost:8083/test/index.html");
});
function sectionDownload(start, end, url) {
const xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.setRequestHeader("Range", `bytes=${start}-${end}`);
xhr.onload = function() {
if (xhr.status == 206) {
entryContent += xhr.response;
//請求其中的某個部分
sectionDownload(end + 1, end + 20, url);
} else if (xhr.status == 416) {
//徹底下載後一系列操做
console.log(
"獲取的內容爲:\n" + entryContent,
"\n內容長度:\n" + entryContentLength
);
} else if (xhr.status == 200) {
console.log(
"獲取的內容爲:\n" + entryContent,
"\n內容長度:\n" + entryContentLength
);
} else {
console.log(xhr);
}
};
xhr.send();
}
function getContentLength(url) {
return new Promise(resolve => {
const xhr = new XMLHttpRequest();
xhr.open("HEAD", url);
xhr.onload = function() {
resolve(xhr.getResponseHeader("content-length"));
};
xhr.send();
});
}
複製代碼
說一下這段代碼的邏輯,這段代碼先向服務器發送一個HEAD
請求獲取響應頭content-length的大小,,也就是請求index.html
的大小,以後開始獲取index.html
內容,每次只獲取20字節,並拼湊到entryContent
變量中。最終沒有字節返回時,那麼entryContent
就是整個index.html的內容了。
有內容返回時HTTP響應頭:
請求範圍沒法知足時的HTTP響應頭:
你們能夠看到,當響應中有部分字節返回時,返回的狀態是206
,當客戶端請求的字節範圍超過了請求資源的大小時,狀態碼返回的是416
,206
狀態碼錶示抓取到了資源的部分數據,416
表示Range
請求的資源範圍沒法知足。咱們能夠根據這個返回狀態判斷是否繼續請求,從而判斷文件下載是否完成。
那麼咱們回到一開頭的問題,這時候你就發現文件總大小是從一開始就獲取到了,爲啥有的下載顯示下載的文件大小,有些不顯示了呢。
這時候若是服務器開啓gzip
壓縮,而後用HEAD
請求,你會發現HTTP
響應頭沒有content-length
返回,下面的這張圖是Nginx
開啓了gzip
壓縮後進行HEAD
請求時服務器返回的響應,能夠發現響應頭是沒有content-length
。這時候你是否是知道爲何有時下載文件的時候是不顯示總大小。
有時候服務器若是開啓壓縮或者爲了減小cpu壓力等等,是不會去計算文件的總大小的,這時候從響應頭中就沒法獲取資源的總大小
。
然而若是你使用的是Node
服務器,不使用任何插件
,你發現就算你請求帶上了Range:bytes=xx-xx
等請求頭,文件內容仍是完整獲取,這時你就會發現,分段請求下載這種能力是依靠服務器才能實現
,Nginx、Apache等服務器都有他們本身的實現方法,那麼Node
服務器如何實現呢?
下面的代碼基於koa
框架的實現具備分段下載文件的功能。
const fs = require("fs");
const path = require("path");
const Koa = require("koa");
const app = new Koa();
const PATH = "./public";
app.use(async ctx => {
const file = path.join(__dirname, `${PATH}${ctx.path}`);
// 一、404檢查
try {
fs.accessSync(file);
} catch (e) {
return (ctx.response.status = 404);
}
//ctx.set('content-encoding', 'gzip');
const method = ctx.request.method;
const { size } = fs.statSync(file);
// 二、響應head請求,返回文件大小
if ("HEAD" == method) {
return ctx.set("Content-Length", size);
}
const range = ctx.headers["range"];
// 三、通知瀏覽器能夠進行分部分請求
if (!range) {
//這裏若是客戶端不是分段請求就返回整個文件
ctx.body = fs.createReadStream(file);
return ctx.set("Accept-Ranges", "bytes");
} else {
const { start, end } = getRange(range);
// 四、檢查請求範圍
if (start >= size) {
ctx.response.status = 416;
return ctx.set("Content-Range", `bytes */${size}`);
}
// 五、206分部分響應
ctx.response.status = 206;
ctx.set("Accept-Ranges", "bytes");
ctx.set("Content-Range", `bytes ${start}-${end ? end : size - 1}/${size}`);
ctx.body = fs.createReadStream(file, { start, end });
}
});
app.listen(3000, () => console.log("partial content server start"));
function getRange(range) {
const match = /bytes=([0-9]*)-([0-9]*)/.exec(range);
const requestRange = {};
if (match) {
if (match[1]) requestRange.start = Number(match[1]);
if (match[2]) requestRange.end = Number(match[2]);
}
return requestRange;
}
複製代碼
你們能夠看到其實分段下載很簡單,就是Node
根據請求頭的Range
進行分段讀取文件二進制流。