Range/Content-Range與斷點續傳,瞭解一下?

Cris_冷崢子發表於2018-04-09
  • server
    • 思路
      • 關於索引位置協商
    • 示例
  • client
  • 測試

server

思路

斷點續傳的伺服器主要是要注意一個請求頭——Range,嗯,在node裡接收是這樣的

let range = req.headers['range'];

<<<
bytes = 0-999
複製程式碼

其中bytes = 0-999即是Range的值,於是乎我們就能利用整個值來控制讀取的startend

[info]值得注意的是,客戶端請求的這個range僅僅只是一個口頭要求,並不具備任何實質性的影響力,要怎麼返回資料給客戶端還是服務端說了算,so究竟從什麼數字編碼開始才是一個資原始檔開始的索引位置?嗯,只要伺服器願意,可以是0也可以是1甚至可以是10086。

拿到range後,我們就可以將range中的x-x和我們createStreamstart/end形成對映,從而達到控制輸出的目的。

fs.createReadStream(p, { start, end }).pipe(res);
複製程式碼

[danger] 注意: createStream API 的索引位置是包前又包後的。

另外還有一點需要注意的是我們要返回給客戶端兩個頭

  • Accept-Ranges:bytes

    表明伺服器是否支援指定範圍請求及哪種型別的分段請求

  • Content-Range:bytes start-end/total:

    告訴他這次我們返回的資料是哪裡到哪裡的,資料總共有多少位元組,這樣客戶端才能知道下次該從哪裡請求資料,是否已經拿完資料該結束請求了。

關於索引位置協商

介於建立可寫可讀流指定索引時是包前又包後的,

個人推設定 薦Range:bytes=start-end時候,第一個位元組因以1開始,並且請求的資料要包括end(即要包後),然後我們在伺服器端用fs.createReadStream()設定rs的start和end時候統一將從請求頭獲取的Range中的start和end減一。

>>> Range:bytes=1-9
...
let range = req.headers['range'];
let result = range.match(/bytes=(\d*)-(\d*)/);
let start = result[0];
let end = result[1];
...
res.setHeader('Accept-Range','bytes');
res.setHeader('Content-Range',`bytes ${start}-${end}/${statObj.size}`) //1-9/total
res.statusCode = 206;
...
fs.createReadStream(filepath,{
  start:start-1,end:end-1 //0-8
});
...
>>>  Range:bytes=10-18
>>>  ...
複製程式碼

示例

let http = require('http');
let fs = require('fs');
let path = require('path');
let { promisify } = require('util');
let stat = promisify(fs.stat);

let server = http.createServer(async function (req, res) {
    let p = path.join(__dirname, 'content.txt');
    let statObj = await stat(p);
    let total = statObj.size;
    let start = 0;
    let end = total;
    
    let range = req.headers['range'];
    if (range) {
        res.setHeader('Accept-Ranges','bytes');
        let result = range.match(/bytes=(\d*)-(\d*)/);
        start = result[1]?parseInt(result[1]):start;
        end = result[2]?parseInt(result[2]):end;
        res.setHeader('Content-Range',`bytes ${start}-${end}/${total}`)
    }
    res.setHeader('Content-Type', 'text/plain;charset=utf8');
    //     res.write('輸出開始');
    fs.createReadStream(p, { start, end }).pipe(res);
});
server.listen(8080);
複製程式碼

client

知道了服務端是怎麼控制輸出的,客戶端就簡直了~

要不,我們就直接上程式碼?

...

let options = {
    hostname:'localhost',
    port:8080,
    path:'/',
    method:'GET'
}

let ws = fs.createWriteStream('./download.txt');
let pause = false;
let start = 0;
let speed = 10;
let end = start+speed;

download();

process.stdin.on('data',function(chunk){
    chunk = chunk.slice(0,chunk.length-2);
    option = chunk.toString();
    switch(option){
        case 'p':
            pause = true;
            break;
        case 'c':
            pause = false;
            download();
        default:
            if(/^s\s-[0-9]+$/.test(option)){
                option = option.slice(3);
                speed = parseInt(option);                
            }
            
    }
});

//--- --- ---
function download(){
  options.headers = {
    Range:`bytes=${start}-${end}` //請求頭看這裡
  }

  http.get(options,function(res){
      let range = res.headers['content-range'];
      let total = range.split('/')[1];
      let buffers = [];
      let nextEnd;
      res.on('data',function(chunk){
        buffers.push(chunk);
      });
      res.on('end',function(){
          ws.write(Buffer.concat(buffers));
          setTimeout(function(){
              if(pause === false&&start<total){
              	start = end+1;
              	nextEnd = end+speed;
                end = nextEnd+1<total?nextEnd:total;
              	download();
              }   
          },1000)
      })
  })
}
複製程式碼

以上實現了一個支援暫停下載的斷點續傳demo,

唯一要稍微注意一點的是,我們是通過Range:Bytes=x-xx這頭來控制下載的。

測試

curl -v -H 'Range:bytes=0-9' http://localhost:8080 //本文中的栗子請求的資料索引是包前又包後的
複製程式碼
  • p:暫停下載
  • c:繼續下載
  • s -xx:修改下載速度

End

相關文章