Http协议定义了分块传输的响应header字段,但是具体是否支持取决于Server的实现,可以指定请求头的range字段来验证服务器是否支持分块传输。例如可以利用curl命令来验证:
bogon:~ duwen$ curl -H "Range: bytes=0-10" http://download.dcloud.net.cn/HBuilder.9.0.2.macosx_64.dmg -v
# 请求头
> GET /HBuilder.9.0.2.macosx_64.dmg HTTP/1.1
> Host: download.dcloud.net.cn
> User-Agent: curl/7.54.0
> Accept: */*
> Range: bytes=0-10
# 响应头
< HTTP/1.1 206 Partial Content
< Content-Type: application/octet-stream
< Content-Length: 11
< Connection: keep-alive
< Date: Thu, 21 Feb 2019 06:25:15 GMT
< Content-Range: bytes 0-10/233295878
在请求头中添加"Range:bytes=0-10"的作用是,告诉服务器本次请求只想获取文件0-10(包括10,共11个字节)这块内容。如果服务器支持分块传输,则响应状态码为206,表示"部分内容",并且同时响应头中包含"COntent-Range"字段,如果不支持则会不包含:
Content-Range: bytes 0-10/233295878
0-1表示本次返回的区块,233295878代表文本的总长度,单位都是byte,也就是该文件大概233M。
基于此,设计一个简单的多线程的文件分块下载器,实现思路:
- 先检测是否支持分块传输,如果不支持,则直接下载;若支持,则将剩余内容分块下载。
- 各个分块下载时保存到各自临时文件,等到所有分块下载完成后合并临时文件。
- 删除临时文件。
实现
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
class SSLHttpChunkingDownload{
const SSLHttpChunkingDownload();
Future downloadChunks(url, savePath, {ProgressCallback? onReceiveProgress,}) async {
const firstChunkSize = 102;
const maxChunk = 3;
int total = 0;
var dio = Dio();
var progress = <int>[];
//进度回调
createCallback(no){
return (int received, _){
progress[no] = received;
debugPrint("ssl current number $no received $received\n");
if (onReceiveProgress != null && total != 0){
onReceiveProgress(progress.reduce((value, element) => value + element), total);
}
};
}
//单块下载
Future<Response> downloadChunk(url, start, end, no) async{
progress.add(0);
--end;
return dio.download(
url,
savePath + "temp$no",
onReceiveProgress: createCallback(no),
options:Options(
headers: {"range": "bytes=$start - $end"}
)
);
}
Future<Response>downloadFile(url) async{
return dio.download(url, savePath,onReceiveProgress: onReceiveProgress);
}
//合并文件
Future mergeTempFiles(chunk) async{
//原文件
File f = File(savePath+"temp0");
//打开文件并设置读写模式为后面追加
IOSink ioSink = f.openWrite(mode: FileMode.writeOnlyAppend);
for (int i = 1; i < chunk; ++ i){
//打开下载块文件
File tem = File(savePath + "temp$i");
//读取下载块文件,并将块文件追加到源文件
await ioSink.addStream(tem.openRead());
//删除 块文件
await tem.delete();
}
//关闭IO
await ioSink.close();
//源文件重命名
await f.rename(savePath);
}
Response response = await downloadChunk(url, 0, firstChunkSize, 0);
//判断是否支持分块下载
if (response.statusCode == 206){
debugPrint("current response header ${response.headers}\n");
total = int.parse(response.headers.value(HttpHeaders.contentRangeHeader)!.split("/").last);
int reserved = total = int.parse(response.headers.value(HttpHeaders.contentLengthHeader)!);
//获取返回数据,判断是否需要分块
int chunk = (reserved - firstChunkSize).ceil() + 1;
debugPrint("ssl chunk$chunk reserved$reserved\n");
if (chunk > 1){
int chunkSize = firstChunkSize;
if (chunk > maxChunk + 1){
chunk = maxChunk + 1;
chunkSize = (reserved / maxChunk).ceil();
}
var futures = <Future>[];
for (int i = 0; i < maxChunk; ++ i){
int start = firstChunkSize + i * chunkSize;
//分块后将每个块请求进行分发
futures.add(downloadChunk(url, start, start + chunkSize, i + 1));
}
debugPrint("ssl chunk num${futures.length}");
await Future.wait(futures);
await mergeTempFiles(chunk);
}else{
Future response = (await downloadFile(url)) as Future;
debugPrint("ssl response $response");
}
}
}
}
class SSLTestChunkDownload extends StatefulWidget{
const SSLTestChunkDownload({Key? key}):super(key: key);
@override
State<StatefulWidget> createState() {
// TODO: implement createState
return SSLTestChunkDownloadState();
}
}
class SSLTestChunkDownloadState extends State<SSLTestChunkDownload>{
int progress = 0;
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
appBar: AppBar(
title: const Text("SSL Chunk Test"),
),
body: Center(
child: Column(
children: [
Text("progress $progress %"),
ElevatedButton(
onPressed: (){
startChunksDownAction();
},
child: const Text("start download")
),
],
),
),
);
}
void startChunksDownAction() async {
var url = "http://download.dcloud.net.cn/HBuilder.9.0.2.macosx_64.dmg";
var path = (await getTemporaryDirectory()).path;
var savePath = "$path/HBuilder.9.0.2.macosx_64.dmg";
debugPrint("ssl save path $savePath");
var down = const SSLHttpChunkingDownload();
down.downloadChunks(url, savePath, onReceiveProgress: (received, total){
debugPrint("ssl current progress$received total $total\n");
if (total != -1){
setState(() {
progress = (received/ total * 100).floor();
debugPrint("current $progress%");
});
}
});
}
}
总结与思考:
分块下载是否能提高下载速度?
下载速度瓶颈取决于网络速度和服务器的出口速度,如果同一个数据源,分块下载的意义并不大,因为服务器是同一个,出口速度是确定的,主要取决于网速。即使我们设备的带宽大于任意一个源,下载速度依然不一定比单源单线下载快。
分块下载的实际用处是便于断电续传,可以将文件分为若干个,然后维护下载状态文件,用以记录每一块的状态,这样即使网络断开,还是可以恢复中断前的状态。