断点下载,这里有你想知道的

1,844 阅读5分钟

最近进行一次下载请求,想使用onprogress显示进度时发现,onprogress中显示的total总为0。

为什么呢?

不知道大家有没有遇到过有时候用下载软件下载文件的时候,有些下载可以显示总大小,有些不可显示,看着就好像出了bug一样。其实这原因和onprogress中显示的total总为0的情况差不多。

想要弄清楚原因这时候要先了解下载文件的原理,而通常的文件下载是可断点续传,可以从这方面入手。

为了让文件下载可以暂停然后重新从暂停下载部分开始重新下载,这时候就要去了解HTTP中的content-lengthAccept-RangesContent-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中的RangeContent-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,当客户端请求的字节范围超过了请求资源的大小时,状态码返回的是416206状态码表示抓取到了资源的部分数据,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进行分段读取文件二进制流。

参考: blog.csdn.net/weixin_3383…