bertking
2022/03/21阅读:353主题:默认主题
断点续传原理
昔日纵马任逍遥,俱是少年英豪

光阴似箭,时光荏苒,尤忆当年为了实现大文件下载功能,研究断点续传的光辉岁月。碰巧最近优化项目中的下载管理器功能,就在这里留下一笔。
断点续传的大概过程:
-
拆分: 将大文件拆分为N个小文件; -
并行下载:采用多线程技术来让每个线程独立负责小文件的下载工作,同时实现断点续传功能。 -
合并:将N个下载完成的小文件合并成一个大文件即可。
有些小伙伴可能有疑问,我家宽带的带宽是500M,下载个几十兆的资源要费这么多事,这不是脱裤子放屁吗?
这个问题留待后面解答。
1.断点续传
断点续传 其实可分为两个部分来理解:
-
断点:下载任务暂停时已下载的数据长度(位置)。(此时文件没有下载完成) -
续传:在文件没有被下载完成的前提下,从断点处继续下载的过程,被称为『续传』。
2.实现原理
每个同学身边都会有同事抱怨说,现在面试越来越卷了,八股文都背不完,怎么搞?
我的回答是该背还得背。
我们做移动开发的,为什么要考察HTTP协议呢?
这里就有答案:要想实现断点续传功能,那就必须得了解HTTP协议。因为支持断点续传的信息基本都隐藏在HTTP协议中。

2.1 状态码206
状态码206 表示该服务器已经成功处理了部分 GET 请求。该状态码主要用于处理Range请求信息,它表示服务器成功成功处理了客户端的范围请求。
2.2 请求头(Request Headers)
HTTP请求头中的range字段指定了分段请求下载的字节长度,这是实现断点续传功能的关键所在。
range字段指定范围的格式,共有以下几种:
range:bytes = 7464567-8042581(表示请求7464567~8042581范围内的字节数据) range:bytes = ~1000(表示请求0~1000范围内的字节数据) range:bytes = 1001- (表示请求1001之后的所有数据) range:bytes = 2000-3500,3501-5000(表示请求多个范围的数据) range:byes = 1001- 这个非常有用,经常用来处理大文件不能整除的情况。(如大文件为11byte,分为3段,最后的范围就可以指定为10~)
2.3 响应头(Response Headers)
-
accept-ranges字段:
并不是所有下载都支持断点续传,只有在response header中有 Accept-Ranges: bytes 字段时才支持断点续传。
-
Content-Length字段:
表示请求下载文件的大小。
-
Content-Range字段:
响应头中的Content-Range表示指定范围的实体内容,其对应着请求头中的range字段。
-
Last-Modified字段:
表示服务端文件最后修改时间,可以用于校验文件是否更改过,但不能保证文件内容是否更改过。
-
ETag字段:
校验文件是否修改过。
根据 HTTP 协议的规定,当文件更新时,是会生成新的 ETag 值的。
3. 具体实现
// 获取HttpURLConnection实例
URL httpUrl = new URL(url)
public HttpURLConnection httpConnection = (HttpURLConnection)httpUrl.openConnection();
// 通过HttpURLConnection 来获取文件大小
int contentLength = httpUrlConnection.getContentLength();
// 任务切分
long size = contentLength / DOWNLOAD_TASK_SIZE;
// 不能整除时的剩余部分
long lastSize = contentLength % DOWNLOAD_TASK_SIZE
// 计算range的起始位置
for (int i = 0; i < DOWNLOAD_TASK_SIZE; i++) {
long start = i * size;
long downloadRange = (i == DOWNLOAD_TASK_SIZE - 1) ? lastSize : size;
long end = start + downloadRange;
if(start != 0) ? start : start++;
// 开启子线程下载子任务
DownloadTask downloadTask = new DownloadTask(url, start, end, i, contentLength);
// 使用线程池管理线程
Future<Boolean> future = executor.submit(downloadTask);
// 利用Future来获取下载结果
futureList.add(future);
}
利用HttpURLConnection给请求头设置range参数。同时也展示了『续传』功能的大体实现。
public class DownloadTask implements Callable<Boolean> {
// 设置range属性
public static HttpURLConnection getRangeConnection(String url, long start, long end) throws IOException {
URL httpUrl = new URL(url)
HttpURLConnection httpConnection = (HttpURLConnection)httpUrl.openConnection();
if (end != 0) {
httpUrlConnection.setRequestProperty("Range", "bytes=" + start + "-" + end);
} else {
// range:bytes = start- 的使用场景
httpUrlConnection.setRequestProperty("Range", "bytes=" + start + "-");
}
return httpConnection;
}
// 获取本地文件大小
long localFileContentLength = FileUtils.getFileContentLength(httpFileName);
// 『续传』功能
httpUrlConnection.setRequestProperty("Range", "bytes=" + (start+localFileContentLength) + "-" + end);
// 利用RandomAccessFile的seek()方法快速定位到断点位置,进行续传的写入操作。
RandomAccessFile oSavedFile = new RandomAccessFile(httpFileName, "rw"))
oSavedFile.seek(localFileContentLength);
}
最后的合并操作也是要利用RandomAccessFile实现。
后面找机会深入研究一下RandomAccessFile
作者介绍