浏览器如何实现断点续传
一、概述
所谓 断点续传 ,就是要从文件已经下载完成的地方开始接着下载,一般用于处理大文件下载的情况。
断点续传 是从HTTP/1.1
开始支持的,主要用到的是请求头中的Range
字段和响应头中的Content-Range
字段。
常用于:
- 断点续传:用于下载文件被中断后,继续下载,可避免已下载部分重复下载;
- 大文件指定区块下载:常用语视频、音频拖动播放,直接定位到指定位置下载内容,可避免每次都读取、传输整个文件,从而提升服务性能;
- 大文件分包批量下载:再合并为完整文件,可提高下载速度。
二、Range
用于请求头中,指定要索取文件的字节范围,格式如下:
Range: (unit=first byte pos)-[last byte pos]
如,请求下载整个文件:
GET /test.rar HTTP/1.1
Connection: close
Host: 116.1.219.219
Range: bytes=0-801 //一般请求下载整个文件是bytes=0- 或不用这个头
三、Content-Range
用于响应头,指定整个实体中一部分的插入位置,也只是了整个实体的长度,一般格式为:
Content-Range: bytes (unit first byte pos) - [last byte pos]/[entity legth]
如,上述请求的正常回应:
HTTP/1.1 200 OK
Content-Length: 801
Content-Type: application/octet-stream
Content-Range: bytes 0-800/801 //801:文件总大小
四、Accept-Ranges
用于响应头,返回当前服务器是否支持Range
请求。
如果请求一个资源时, HTTP响应中出现如下所示的 Accept-Ranges
, 且其值不是none
, 那么服务器支持范围请求。
curl -I http://i.imgur.com/z4d4kWk.jpg
HTTP/1.1 200 OK
...
Accept-Ranges: bytes
Content-Length: 146515
在如上响应中,Accept-Ranges: bytes
代表可以使用字节作为单位来定义请求范围。这里的 Response Headers中的 Content-Length: 146515
则代表该资源的完整大小。
如果站点响应中未返回 Accept-Ranges
响应头,或者其值为none
,那么这意味着server不支持HTTP range请求。
五、示例
使用Node
作为服务端支持断点续传。
服务端代码如下:
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);
客户端代码如下:
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)
})
})
}