是不是被圖片騙進來了?哈哈不要著急走,其實也不是跟圖沒關係,我們這次就是說斷點續傳的那些事。
我們寫程式碼的有時候工作中會做一些上傳/下載圖片或者檔案的操作。萬一線路中斷,不具備斷點續傳的 HTTP/FTP 伺服器或下載軟體就只能從頭重傳,比較好的 HTTP/FTP 伺服器或下載軟體具有斷點續傳能力,允許使用者從上傳/下載斷線的地方繼續傳送,這樣大大減少了使用者的煩惱。
複製程式碼
獲取部分內容的範圍請求
其實啊,為了實現中斷恢復下載的需求,需要能下載指定下載的實體範圍:
- 請求頭中的Range來指定 資源的byte範圍
- 響應會返回狀態碼206響應報文
- 對於多重範圍的範圍請求,響應會在首部欄位Content-Type中標明multipart/byteranges 我們來模擬一下: 先開啟我們的命令列工具,寫上如下程式碼 (後面是我在百度上找的一個圖片)
curl -v --header "Range:bytes=0-10" http://pic26.nipic.com/20130117/10558908_124756651000_2.jpg
複製程式碼
執行一下得到的這個結果:
從http那段伺服器返回給我們的資訊可以看出如下資訊(206代表伺服器答應我們的range請求方式)
- Content-Type: image/jpeg 型別是圖片
- Accept-Ranges: bytes 單位是bytes
- Content-Range: bytes 0-10/292871 請求段為0-10,總共有292871bytes
- Content-Length: 11 請求長度為11 知道這些之後我們可以簡單模擬一下客戶端和伺服器端怎麼做的:
伺服器端程式碼
首先客戶端要發一個頭Range:bytes=0-10,然後服務端返回一個頭 // Accept-Ranges:bytes // Content-Range:0-10/總大小
// 獲取範圍請求
let http = require('http');
let fs = require('fs');
let path = require('path');
let { promisify } = require('util');//這個模組是包裝某個東西成為promise的。
let stat = promisify(fs.stat);
let server = http.createServer(async function (req, res) {
let p = path.join(__dirname, 'con.txt');
// 判斷當前檔案的大小
let statObj = await stat(p);
let start = 0;
let end = statObj.size - 1; // 讀流是包前又包後的:statObj.size 是檔案的大小。
let total = end
let range = req.headers['range'];
if (range) {//判斷客戶端是否有帶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])-1:end;
// 獲取成功並且檔案總大小是多少
// Content-Range:0-10/總大小
res.setHeader('Content-Range',`${start}-${end}/${total}`)
}
res.setHeader('Content-Type', 'text/plain;charset=utf8');
fs.createReadStream(p, { start, end }).pipe(res);
});
server.listen(3000);
複製程式碼
這樣一個簡單的伺服器端就寫好了。
客戶端程式碼
//首先要寫一個options
let options = {
hostname:'localhost',
port:3000,
path:'/',
method:'GET'
}
let fs = require('fs');
let path = require('path');
let http = require('http');
let ws = fs.createWriteStream('./download.txt');
let pause = false;
let start = 0;
// 下載,每次獲取10個
process.stdin.on('data',function(chunk){
chunk = chunk.toString();
if(chunk.includes('p')){ //輸入p就是暫停
pause = true
}else{
pause = false;
download();
}
});
function download(){
options.headers = {
Range:`bytes=${start}-${start+10}`
}
start+=10;
// 發請求
http.get(options,function(res){
let range = res.headers['content-range'];
let total = range.split('/')[1];
let buffers = [];//建立一個快取區,把讀到的資料都放在裡面
//,等到end的時候就整個取出來。
res.on('data',function(chunk){
buffers.push(chunk);
});
res.on('end',function(){
//將獲取的資料寫入到檔案中
ws.write(Buffer.concat(buffers));
setTimeout(function(){
if(pause === false&&start<total){
download();
}
},1000)
})
})
}
download();
複製程式碼
程式碼非常淺顯易懂。 最後我們在同級目錄下建立一個con.txt檔案。用node執行一下客戶端檔案,就會實現功能啦。
場景校驗
有時候即使終端發起續傳請求是url對應問問價在伺服器端已經發生變化,那麼很顯然此時需要一個標識問價唯一的方式,
- Etag(請求頭) ===> if-none-match
- Last-Modified(請求頭) ===> if-modified-since
Etag
Etag(Entity Tags)主要為了解決 Last-Modified 無法解決的一些問題。
- 一些檔案也許會週期性的更改,但是內容並不改變(僅改變修改時間),這時候我們並不希望客戶端認為這個檔案被修改了,而重新 GET。
- 某些檔案修改非常頻繁,例如:在秒以下的時間內進行修改(1s 內修改了 N 次),If-Modified-Since 能檢查到的粒度是 s 級的,這種修改無法判斷(或者說 UNIX 記錄 MTIME 只能精確到秒)。
- 某些伺服器不能精確的得到檔案的最後修改時間。
為此,HTTP/1.1 引入了 Etag。Etag 僅僅是一個和檔案相關的標記,可以是一個版本標記,例如:v1.0.0;或者說 “627-4d648041f6b80” 這麼一串看起來很神祕的編碼。但是 HTTP/1.1 標準並沒有規定 Etag 的內容是什麼或者說要怎麼實現,唯一規定的是 Etag 需要放在 “” 內,一般情況下Etag的值是時間+檔案大小
。
Last-Modified
If-Modified-Since,和 Last-Modified 一樣都是用於記錄頁面最後修改時間的 HTTP 頭資訊,只是 Last-Modified 是由伺服器往客戶端傳送的 HTTP 頭,而 If-Modified-Since 則是由客戶端往伺服器傳送的頭,可以看到,再次請求本地存在的 cache 頁面時,客戶端會通過 If-Modified-Since 頭將先前伺服器端發過來的 Last-Modified 最後修改時間戳傳送回去,這是為了讓伺服器端進行驗證,通過這個時間戳判斷客戶端的頁面是否是最新的,如果不是最新的,則返回新的內容,如果是最新的,則返回 304 告訴客戶端其本地 cache 的頁面是最新的,於是客戶端就可以直接從本地載入頁面了,這樣在網路上傳輸的資料就會大大減少,同時也減輕了伺服器的負擔。 如果想了解具體怎麼實現可以看看靜態服務裡面的快取。
是不是躍躍欲試??
如果對你有幫助請點個贊噻!