最近进行一次下载请求,想使用onprogress显示进度时发现,onprogress中显示的total总为0。
为什么呢?
不知道大家有没有遇到过有时候用下载软件下载文件的时候,有些下载可以显示总大小,有些不可显示,看着就好像出了bug一样。其实这原因和onprogress中显示的total总为0的情况差不多。
想要弄清楚原因这时候要先了解下载文件的原理,而通常的文件下载是可断点续传,可以从这方面入手。
为了让文件下载可以暂停然后重新从暂停下载部分开始重新下载,这时候就要去了解HTTP中的content-length
、Accept-Ranges
、Content-Range
还有Range
。
content-length
:用于响应头,表示响应内容的字节大小
Accept-Ranges
:用于响应头,告知客户端可以进行范围请求,后面的值表示返回的内容单位,通常是bytes,如:Accept-Ranges:bytes
Content-Range
: 用于响应头,用于描述响应请求内容的范围和整体长度,比如Content-Range: bytes 201-220/326
表示服务器端返回请求资源中的201到220bytes范围的内容,请求资源总大小为326字节,如果总大小未知就会显示Content-Range: bytes 201-220/\*
Range
:用于请求头,作用是告知服务器端返回哪一部分的内容,比如Range:bytes=500-1000
表示告知服务器我要拿这个文件中500至1000字节的内容。
利用HTTP
中的Range
、Content-Range
就可以实现断点下载。
我们可以用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}`);
// 1、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);
// 2、响应head请求,返回文件大小
if ("HEAD" == method) {
return ctx.set("Content-Length", size);
}
const range = ctx.headers["range"];
// 3、通知浏览器可以进行分部分请求
if (!range) {
//这里如果客户端不是分段请求就返回整个文件
ctx.body = fs.createReadStream(file);
return ctx.set("Accept-Ranges", "bytes");
} else {
const { start, end } = getRange(range);
// 4、检查请求范围
if (start >= size) {
ctx.response.status = 416;
return ctx.set("Content-Range", `bytes */${size}`);
}
// 5、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
进行分段读取文件二进制流。
参考:
https://blog.csdn.net/weixin_33836874/article/details/88720882
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。