b

bertking

V1

2022/03/21阅读:64主题:默认主题

断点续传原理

昔日纵马任逍遥,俱是少年英豪

光阴似箭,时光荏苒,尤忆当年为了实现大文件下载功能,研究断点续传的光辉岁月。碰巧最近优化项目中的下载管理器功能,就在这里留下一笔。

断点续传的大概过程:

  1. 拆分: 将大文件拆分为N个小文件
  2. 并行下载:采用多线程技术来让每个线程独立负责小文件的下载工作,同时实现断点续传功能。
  3. 合并:将N个下载完成的小文件合并成一个大文件即可。

有些小伙伴可能有疑问,我家宽带的带宽是500M,下载个几十兆的资源要费这么多事,这不是脱裤子放屁吗?

这个问题留待后面解答。


1.断点续传

断点续传 其实可分为两个部分来理解:

  1. 断点:下载任务暂停时已下载的数据长度(位置)。(此时文件没有下载完成)
  2. 续传:在文件没有被下载完成的前提下,从断点处继续下载的过程,被称为『续传』。

2.实现原理

每个同学身边都会有同事抱怨说,现在面试越来越卷了,八股文都背不完,怎么搞?

我的回答是该背还得背。

我们做移动开发的,为什么要考察HTTP协议呢?

这里就有答案:要想实现断点续传功能,那就必须得了解HTTP协议。因为支持断点续传的信息基本都隐藏在HTTP协议中。

一图胜千言
一图胜千言

2.1 状态码206

状态码206 表示该服务器已经成功处理了部分 GET 请求。该状态码主要用于处理Range请求信息,它表示服务器成功成功处理了客户端的范围请求

2.2 请求头(Request Headers)

HTTP请求头中的range字段指定了分段请求下载的字节长度,这是实现断点续传功能的关键所在。

range字段指定范围的格式,共有以下几种:

  1. range:bytes = 7464567-8042581(表示请求7464567~8042581范围内的字节数据)
  2. range:bytes = ~1000(表示请求0~1000范围内的字节数据)
  3. range:bytes = 1001- (表示请求1001之后的所有数据)
  4. 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

分类:

移动端开发

标签:

Android

作者介绍

b
bertking
V1