服务器之家:专注于VPS、云服务器配置技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - Android - Android Okhttp断点续传面试深入解析

Android Okhttp断点续传面试深入解析

2022-10-20 14:05芦苇科技App技术团队 Android

这篇文章主要给大家介绍了关于Android Okhttp断点续传面试的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用Android具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧

前言

我们在刷一下面试题的时候,有时候会看到一些大厂会问关于断点续传的原理,那么今天在这里从 HTTP 断点续传知识和 Android 中如何实现断点续传的思路来做一个关于 Android 断点续传原理的总结。

Http 断点续传知识点

什么是断点续传

指的是在上传/下载时,将任务(一个文件或压缩包)人为的划分为几个部分,每一个部分采用一个线程进行上传/下载,如果碰到网络故障,可以从已经上传/下载的部分开始继续上传/下载未完成的部分,而没有必要从头开始上传/下载。可以节省时间,提高速度。

Http 怎么支持断点续传的?

Http 1.1 协议中默认支持获取文件的部分内容,这其中主要是通过头部的两个参数:Range 和 Content Range 来实现的。客户端发请求时对应的是 Range ,服务器端响应时对应的是 Content-Range。

Range

客户端想要获取文件的部分内容,那么它就需要请求头部中的 Range 参数中指定获取内容的起始字节的位置和终止字节的位置,它的格式一般为:

Range:(unit=first byte pos)-[last byte pos]

例如:

Range: bytes=0-499      表示第 0-499 字节范围的内容
Range: bytes=500-999    表示第 500-999 字节范围的内容
Range: bytes=-500       表示最后 500 字节的内容
Range: bytes=500-       表示从第 500 字节开始到文件结束部分的内容
Range: bytes=0-0,-1     表示第一个和最后一个字节
Range: bytes=500-600,601-999 同时指定几个范围

Content Range

在收到客户端中携带 Range 的请求后,服务器会在响应的头部中添加 Content Range 参数,返回可接受的文件字节范围及其文件的总大小。它的格式如下:

Content-Range: bytes (unit first byte pos) - [last byte pos]/[entity legth]

例如:

Content-Range: bytes 0-499/22400    // 0-499 是指当前发送的数据的范围,而 22400 则是文件的总大小。

使用断点续传和不使用断点续传的响应内容区别

不使用断点续传

?
1
HTTP/1.1 200 Ok

使用断点续传

?
1
HTTP/1.1 206 Partial Content

处理请求资源发生改变的问题

在现实的场景中,服务器中的文件是会有发生变化的情况的,那么我们发起续传的请求肯定是失败的,那么为了处理这种服务器文件资源发生改变的问题,在 RFC2616 中定义了 Last-Modified 和  Etag 来判断续传文件资源是否发生改变。

Last-Modified & If-Modified-Since(文件最后修改时间)

Last-Modified:记录 Http 页面最后修改时间的 Http 头部参数,Last-Modified 是由服务端发送给客户端的
If-Modified-Since:记录 Http 页面最后修改时间的 Http 头部参数,If-Modified-Since 是有客户端发送给服务端的
验证过程

  • step 1:客户端缓存从服务端获取的页面
  • step 1:客户端访问相同页面时,客户端将服务器发送过来的  Last-Modified 通过 If-Modified-Since 发送给服务器
  • step 2:服务器通过客户端发送过来的 If-Modified-Since 进行判断客户端当前的缓存的页面是否为最新的
    • 如果不是最新的,那么就发送最新的页面给客户端
    • 如果是最新的,那么就发送 304 告诉客户端它本地缓存的页面是最新的

Etag & if-Range(文件唯一标志)

Etag:作为文件的唯一标志,这个标志可以是文件的 hash 值或者是一个版本

if-Range:用于判断实体是否发生改变,如果实体未改变,服务器发送客户端丢失的部分,否则发送整个实体。一般格式:

?
1
If-Range: Etag | HTTP-Date

If-Range 可以使用 Etag 或者 Last-Modified 返回的值。当没有 ETage 却有 Last-modified 时,可以把 Last-modified 作为 If-Range 字段的值

验证过程

  • step 1:客户端发起续传请求,头部包含 Range 和 if-Range 参数
  • step 2:服务器中收到客户端的请求之后,将客户端和服务器的 Etag 进行比对
    • 相等:请求文件资源没有发生变化,应答报文为 206
    • 不相等:请求文件资源发生变化,应答报文为 200

检查服务器是否支持断点续传

Android Okhttp断点续传面试深入解析

我们使用 curl 进行检测,可以看出以下的几个关键信息

  • HTTP/1.1 206 Partial Content
  • Content-Range: bytes 10-222/7877
  • Etag: "1ec5-502264e2ae4c0"
  • Last-Modified: Wed, 03 Sep 2014 10:00:27 GMT

OkHttp 断点下载

断点下载思路

  • step 1:判断检查本地是否有下载文件,若存在,则获取已下载的文件大小 downloadLength,若不存在,那么本地已下载文件的长度为 0
  • step 2:获取将要下载的文件总大小(HTTP 响应头部的 content-Length)
  • step 3:比对已下载文件大小和将要下载的文件总大小(contentLength),判断要下载的长度
  • step 4:再即将发起下载请求的 HTTP 头部中添加即将下载的文件大小范围(Range: bytes = downloadLength - contentLength)

Okhttp 简单短断点下载代码示例

DownloadTask.java

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
/**
 * String 在执行AsyncTask时需要传入的参数,可用于在后台任务中使用。
 * Integer 后台任务执行时,如果需要在界面上显示当前的进度,则使用这里指定的泛型作为进度单位。
 * Integer 当任务执行完毕后,如果需要对结果进行返回,则使用这里指定的泛型作为返回值类型。
 */
public class DownloadTask extends AsyncTask<String, Integer, Integer> {
 
  public static final int TYPE_SUCCESS = 0;
 
  public static final int TYPE_FAILED = 1;
 
  public static final int TYPE_PAUSED = 2;
 
  public static final int TYPE_CANCELED = 3;
 
  private DownloadListener listener;
 
  private boolean isCanceled = false;
 
  private boolean isPaused = false;
 
  private int lastProgress;
 
  public DownloadTask(DownloadListener listener) {
    this.listener = listener;
  }
 
  /**
   * 这个方法中的所有代码都会在子线程中运行,我们应该在这里处理所有的耗时任务。
   *
   * @param params
   * @return
   */
  @Override
  protected Integer doInBackground(String... params) {
    InputStream is = null;
    RandomAccessFile savedFile = null;
    File file = null;
    long downloadLength = 0//记录已经下载的文件长度
    //文件下载地址
    String downloadUrl = params[0];
    //下载文件的名称
    String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/"));
    //下载文件存放的目录
    String directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();
    //创建一个文件
    file = new File(directory + fileName);
    if (file.exists()) {
      //如果文件存在的话,得到文件的大小
      downloadLength = file.length();
    }
    //得到下载内容的大小
    long contentLength = getContentLength(downloadUrl);
    if (contentLength == 0) {
      return TYPE_FAILED;
    } else if (contentLength == downloadLength) {
      //已下载字节和文件总字节相等,说明已经下载完成了
      return TYPE_SUCCESS;
    }
    OkHttpClient client = new OkHttpClient();
    /**
     * HTTP请求是有一个Header的,里面有个Range属性是定义下载区域的,它接收的值是一个区间范围,
     * 比如:Range:bytes=0-10000。这样我们就可以按照一定的规则,将一个大文件拆分为若干很小的部分,
     * 然后分批次的下载,每个小块下载完成之后,再合并到文件中;这样即使下载中断了,重新下载时,
     * 也可以通过文件的字节长度来判断下载的起始点,然后重启断点续传的过程,直到最后完成下载过程。
     */
    Request request = new Request.Builder()
        .addHeader("RANGE", "bytes=" + downloadLength + "-" + contentLength) //断点续传要用到的,指示下载的区间
        .url(downloadUrl)
        .build();
    try {
      Response response = client.newCall(request).execute();
      if (response != null) {
        is = response.body().byteStream();
        savedFile = new RandomAccessFile(file, "rw");
        savedFile.seek(downloadLength);//跳过已经下载的字节
        byte[] b = new byte[1024];
        int total = 0;
        int len;
        while ((len = is.read(b)) != -1) {
          if (isCanceled) {
            return TYPE_CANCELED;
          } else if (isPaused) {
            return TYPE_PAUSED;
          } else {
            total += len;
            savedFile.write(b, 0, len);
            //计算已经下载的百分比
            int progress = (int) ((total + downloadLength) * 100 / contentLength);
            //注意:在doInBackground()中是不可以进行UI操作的,如果需要更新UI,比如说反馈当前任务的执行进度,
            //可以调用publishProgress()方法完成。
            publishProgress(progress);
          }
 
        }
        response.body().close();
        return TYPE_SUCCESS;
      }
    } catch (IOException e) {
      e.printStackTrace();
    } finally {
      try {
        if (is != null) {
          is.close();
        }
        if (savedFile != null) {
          savedFile.close();
        }
        if (isCanceled && file != null) {
          file.delete();
        }
      } catch (Exception e) {
        e.printStackTrace();
      }
    }
    return TYPE_FAILED;
  }
 
  /**
   * 当在后台任务中调用了publishProgress(Progress...)方法之后,onProgressUpdate()方法
   * 就会很快被调用,该方法中携带的参数就是在后台任务中传递过来的。在这个方法中可以对UI进行操作,利用参数中的数值就可以
   * 对界面进行相应的更新。
   *
   * @param values
   */
  @Override
  protected void onProgressUpdate(Integer... values) {
    int progress = values[0];
    if (progress > lastProgress) {
      listener.onProgress(progress);
      lastProgress = progress;
    }
  }
 
  /**
   * 当后台任务执行完毕并通过Return语句进行返回时,这个方法就很快被调用。返回的数据会作为参数
   * 传递到此方法中,可以利用返回的数据来进行一些UI操作。
   *
   * @param status
   */
  @Override
  protected void onPostExecute(Integer status) {
    switch (status) {
      case TYPE_SUCCESS:
        listener.onSuccess();
        break;
      case TYPE_FAILED:
        listener.onFailed();
        break;
      case TYPE_PAUSED:
        listener.onPaused();
        break;
      case TYPE_CANCELED:
        listener.onCanceled();
        break;
      default:
        break;
    }
  }
 
  public void pauseDownload() {
    isPaused = true;
  }
 
  public void cancelDownload() {
    isCanceled = true;
  }
 
  /**
   * 得到下载内容的完整大小
   *
   * @param downloadUrl
   * @return
   */
  private long getContentLength(String downloadUrl) {
    OkHttpClient client = new OkHttpClient();
    Request request = new Request.Builder().url(downloadUrl).build();
    try {
      Response response = client.newCall(request).execute();
      if (response != null && response.isSuccessful()) {
        long contentLength = response.body().contentLength();
        response.body().close();
        return contentLength;
      }
    } catch (IOException e) {
      e.printStackTrace();
    }
    return 0;
  }
 
}

DownloadListener.java

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class DownloadListener {
 
 
  /**
   * 通知当前的下载进度
   * @param progress
   */
  void onProgress(int progress);
 
  /**
   * 通知下载成功
   */
  void onSuccess();
 
  /**
   * 通知下载失败
   */
  void onFailed();
 
  /**
   * 通知下载暂停
   */
  void onPaused();
 
  /**
   * 通知下载取消事件
   */
  void onCanceled();
 
}

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对服务器之家的支持。

原文链接:https://juejin.im/post/5ceab960e51d4510926a7acf

延伸 · 阅读

精彩推荐