Range 并发下载

Range 并发下载

在《Network-Notes》中我们讨论了 Range 的定义,这里我们讨论下在前端的实践。多线程的话,会比较麻烦一些,需要按片去下载,下载好后,需要进行合并再进行下载。服务端需要在 Node 中进行兼容处理:

router.get("/api/rangeFile", async (ctx) => {
  const { filename } = ctx.query;
  const { size } = fs.statSync(path.join(__dirname, "./static/", filename));
  const range = ctx.headers["range"];
  if (!range) {
    ctx.set("Accept-Ranges", "bytes");
    ctx.body = fs.readFileSync(path.join(__dirname, "./static/", filename));
    return;
  }
  const { start, end } = getRange(range);
  if (start >= size || end >= size) {
    ctx.response.status = 416;
    ctx.body = "";
    return;
  }
  ctx.response.status = 206;
  ctx.set("Accept-Ranges", "bytes");
  ctx.set("Content-Range", `bytes ${start}-${end ? end : size - 1}/${size}`);
  ctx.body = fs.createReadStream(path.join(__dirname, "./static/", filename), {
    start,
    end,
  });
});

如果是单线程下载代码,直接去请求以 blob 方式获取,然后用 blobURL 的方式下载:

download1.onclick = () => {
  console.time("直接下载");
  function download(url) {
    const req = new XMLHttpRequest();
    req.open("GET", url, true);
    req.responseType = "blob";
    req.onload = function (oEvent) {
      const content = req.response;
      const aTag = document.createElement("a");
      aTag.download = "360_0388.jpg";
      const blob = new Blob([content]);
      const blobUrl = URL.createObjectURL(blob);
      aTag.href = blobUrl;
      aTag.click();
      URL.revokeObjectURL(blob);
      console.timeEnd("直接下载");
    };
    req.send();
  }
  download(url);
};

如果是多线程部分,首先发送一个 head 请求,来获取文件的大小,然后根据 length 以及设置的分片大小,来计算每个分片是滑动距离。通过 Promise.all 的回调中,用 concatenate 函数对分片 buffer 进行一个合并成一个 blob,然后用 blobURL 的方式下载。

// script
function downloadRange(url, start, end, i) {
  return new Promise((resolve, reject) => {
    const req = new XMLHttpRequest();
    req.open("GET", url, true);
    req.setRequestHeader("range", `bytes=${start}-${end}`);
    req.responseType = "blob";
    req.onload = function (oEvent) {
      req.response.arrayBuffer().then((res) => {
        resolve({
          i,
          buffer: res,
        });
      });
    };
    req.send();
  });
}
// 合并buffer
function concatenate(resultConstructor, arrays) {
  let totalLength = 0;
  for (let arr of arrays) {
    totalLength += arr.length;
  }
  let result = new resultConstructor(totalLength);
  let offset = 0;
  for (let arr of arrays) {
    result.set(arr, offset);
    offset += arr.length;
  }
  return result;
}
download2.onclick = () => {
  axios({
    url,
    method: "head",
  }).then((res) => {
    // 获取长度来进行分割块
    console.time("并发下载");
    const size = Number(res.headers["content-length"]);
    const length = parseInt(size / m);
    const arr = [];
    for (let i = 0; i < length; i++) {
      let start = i * m;
      let end = i == length - 1 ? size - 1 : (i + 1) * m - 1;
      arr.push(downloadRange(url, start, end, i));
    }
    Promise.all(arr).then((res) => {
      const arrBufferList = res
        .sort((item) => item.i - item.i)
        .map((item) => new Uint8Array(item.buffer));
      const allBuffer = concatenate(Uint8Array, arrBufferList);
      const blob = new Blob([allBuffer], { type: "image/jpeg" });
      const blobUrl = URL.createObjectURL(blob);
      const aTag = document.createElement("a");
      aTag.download = "360_0388.jpg";
      aTag.href = blobUrl;
      aTag.click();
      URL.revokeObjectURL(blob);
      console.timeEnd("并发下载");
    });
  });
};
下一页