Android Okhttp 斷點續傳面試解析

咱們在刷一下面試題的時候,有時候會看到一些大廠會問關於斷點續傳的原理,那麼今天在這裏從 HTTP 斷點續傳知識和 Android 中如何實現斷點續傳的思路來作一個關於 Android 斷點續傳原理的總結。java

Http 斷點續傳知識點

什麼是斷點續傳

指的是在上傳/下載時,將任務(一個文件或壓縮包)人爲的劃分爲幾個部分,每個部分採用一個線程進行上傳/下載,若是碰到網絡故障,能夠從已經上傳/下載的部分開始繼續上傳/下載未完成的部分,而沒有必要從頭開始上傳/下載。能夠節省時間,提升速度。面試

Http 怎麼支持斷點續傳的?

Http 1.1 協議中默認支持獲取文件的部份內容,這其中主要是經過頭部的兩個參數:Range 和 Content Range 來實現的。客戶端發請求時對應的是 Range ,服務器端響應時對應的是 Content-Range。緩存

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 同時指定幾個範圍
複製代碼

Content Range

在收到客戶端中攜帶 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-ModifiedEtag 來判斷續傳文件資源是否發生改變。curl

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:用於判斷實體是否發生改變,若是實體未改變,服務器發送客戶端丟失的部分,不然發送整個實體。通常格式:
If-Range: Etag | HTTP-Date
複製代碼

If-Range 可使用 Etag 或者 Last-Modified 返回的值。當沒有 ETage 卻有 Last-modified 時,能夠把 Last-modified 做爲 If-Range 字段的值ide

  • 驗證過程
    • step 1:客戶端發起續傳請求,頭部包含 Range 和 if-Range 參數
    • step 2:服務器中收到客戶端的請求以後,將客戶端和服務器的 Etag 進行比對
      • 相等:請求文件資源沒有發生變化,應答報文爲 206
      • 不相等:請求文件資源發生變化,應答報文爲 200

檢查服務器是否支持斷點續傳

05941F7C-66E3-4019-ACB1-4F02EBEE2006.png
咱們使用 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.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();

}
複製代碼
相關文章
相關標籤/搜索