斷點下載,這裏有你想知道的

最近進行一次下載請求,想使用onprogress顯示進度時發現,onprogress中顯示的total總爲0。html

爲何呢?nginx

不知道你們有沒有遇到過有時候用下載軟件下載文件的時候,有些下載能夠顯示總大小,有些不可顯示,看着就好像出了bug同樣。其實這緣由和onprogress中顯示的total總爲0的狀況差很少。ajax

想要弄清楚緣由這時候要先了解下載文件的原理,而一般的文件下載是可斷點續傳,能夠從這方面入手。瀏覽器

爲了讓文件下載能夠暫停而後從新從暫停下載部分開始從新下載,這時候就要去了解HTTP中的content-lengthAccept-RangesContent-Range還有Rangebash

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中的RangeContent-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,當客戶端請求的字節範圍超過了請求資源的大小時,狀態碼返回的是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}`);
  // 一、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進行分段讀取文件二進制流。

參考: blog.csdn.net/weixin_3383…

相關文章
相關標籤/搜索