咱們在刷一下面試題的時候,有時候會看到一些大廠會問關於斷點續傳的原理,那麼今天在這裏從 HTTP 斷點續傳知識和 Android 中如何實現斷點續傳的思路來作一個關於 Android 斷點續傳原理的總結。java
指的是在上傳/下載時,將任務(一個文件或壓縮包)人爲的劃分爲幾個部分,每個部分採用一個線程進行上傳/下載,若是碰到網絡故障,能夠從已經上傳/下載的部分開始繼續上傳/下載未完成的部分,而沒有必要從頭開始上傳/下載。能夠節省時間,提升速度。面試
Http 1.1 協議中默認支持獲取文件的部份內容,這其中主要是經過頭部的兩個參數:Range 和 Content Range 來實現的。客戶端發請求時對應的是 Range ,服務器端響應時對應的是 Content-Range。緩存
客戶端想要獲取文件的部份內容,那麼它就須要請求頭部中的 Range 參數中指定獲取內容的起始字節的位置和終止字節的位置,它的格式通常爲:bash
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 同時指定幾個範圍
複製代碼
在收到客戶端中攜帶 Range 的請求後,服務器會在響應的頭部中添加 Content Range 參數,返回可接受的文件字節範圍及其文件的總大小。它的格式以下:網絡
Content-Range: bytes (unit first byte pos) - [last byte pos]/[entity legth]
複製代碼
例如:dom
Content-Range: bytes 0-499/22400 // 0-499 是指當前發送的數據的範圍,而 22400 則是文件的總大小。
複製代碼
HTTP/1.1 200 Ok
複製代碼
HTTP/1.1 206 Partial Content
複製代碼
在現實的場景中,服務器中的文件是會有發生變化的狀況的,那麼咱們發起續傳的請求確定是失敗的,那麼爲了處理這種服務器文件資源發生改變的問題,在 RFC2616 中定義了 Last-Modified 和 Etag 來判斷續傳文件資源是否發生改變。curl
If-Range: Etag | HTTP-Date
複製代碼
If-Range 可使用 Etag 或者 Last-Modified 返回的值。當沒有 ETage 卻有 Last-modified 時,能夠把 Last-modified 做爲 If-Range 字段的值ide
DownloadTask.javaui
/**
* 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
public class DownloadListener {
/**
* 通知當前的下載進度
* @param progress
*/
void onProgress(int progress);
/**
* 通知下載成功
*/
void onSuccess();
/**
* 通知下載失敗
*/
void onFailed();
/**
* 通知下載暫停
*/
void onPaused();
/**
* 通知下載取消事件
*/
void onCanceled();
}
複製代碼