vivo 應用商店中的斷點續傳技術剖析

一、業務背景

目前,vivo 平臺有很多的業務都涉及到文件的下載:譬如說應用商店、遊戲中心的C端用戶下載更新應用或遊戲;開放平臺B端用戶通過接口傳包能力更新應用或遊戲,需要從用戶服務器上下載apk、圖片等文件,來完成用戶的一次版本更新。

二、面臨的挑戰

針對上述C端用戶,平臺需要提供良好的下載環境,並且客戶端需要兼容手機上用戶的異常操作。

針對上述B端用戶,平臺亟需解決的問題就是從用戶服務器上,拉取各種資源文件。

下載本身也是一個很複雜的問題,會涉及到網絡問題、URL重定向、超大文件、遠程服務器文件變更、本地文件被刪除等各種問題。這就需要我們保證平臺具備快速下載文件的能力,同時兼具有有對異常場景的快速預警、容錯處理的機制。

三、業務實現方案

基於前面提到的挑戰,我們設計實現方案的時候,引用了行業常用的解決方法:斷點下載。

針對B端用戶場景,我們的處理方案入下圖:

一、極速下載:通過分析文件大小,智能選擇是否採用直接下載、單線程斷點下載、多線程斷點下載的方案;在使用多線程下載方案時,對"多線程"的使用,有兩種方式:

  • 分組模式:單個文件採用固定最大N個線程來進行下載,分組的好處是能保證服務節點線程數量可控,劣勢就是遇到大文件的時候,下載耗時相對會比較長;
  • 分片模式:採用單個線程,固定下載N個字節大小空間,分片的好處是遇到大文件的時候,下載耗時仍然會相對短,劣勢是會導致服務器節點線程數量突增,對服務節點穩定性有干擾;

在二者之間,我們選擇了分組模式。

二、容錯處理:在我們處理下載過程中,會遇到下載過程中網絡不穩定、本地文件刪除,遠程文件變更等各種場景,這就需要我們能夠兼容處理這些場景,失敗後的任務,會有定時任務自動重新調起執行,也有後臺管理系統界面,進行人工調起;

三、完整性校驗:文件下載完成之後,需要對文件的最終一致性做校驗,來確保文件的正確性;

四、異常預警:對於單次任務在嘗試多次下載操作後仍然失敗的情況,及時發起預警警告。

對於C端用戶,業務方案相對更簡單,因爲文件服務器有vivo平臺提供,網絡環境相對可控,這裏就不再贅述。接下來,我們將對文件下載裏面的各種技術細節,進行詳盡的剖析。

四、斷點下載原理剖析

在進行原理分析前,先給大家普及一下,什麼叫斷點下載?相信大家都有過使用迅雷下載網絡文件的經歷吧,有沒有注意到迅雷的下載任務欄裏面,有一個“暫停”和“開始下載”按鈕,會隨着任務的當前狀態顯示不同的按鈕。當你在下載一個100M的文件,下載到50M的時候,你點擊了“暫停”,然後點擊了“開始下載”,你會發現文件的下載竟然是從已經下載好的50M以後接着下載的。沒錯,這就是斷點下載的真實應用。

4.1 HTTP 斷點下載之祕密:Range

在講解這個知識點前,大家有必要了解一下http的發展歷史,HTTP(HyperText Transfer Protocol),超文本傳輸協議,是目前萬維網(World Wide Web)的基礎協議,已經經歷四次的版本迭代:HTTP/0.9,HTTP/1.0,HTTP/1.1,HTTP/2.0。在HTTP/1.1(RFC2616)協議中,定義了HTTP1.1標準所包含的所有頭字段的相關語法和含義,其中就包括咱們要講到的Accept-Ranges,服務端支持範圍請求(range requests)。有了這個重要的屬性,才使得我們的斷點下載成爲可能。

基於HTTP不同版本之間的適配性,所以當我們在決定是否需要使用斷點下載能力的時候,需要提前識別文件地址是否支持斷點下載,怎麼識別呢?方法很多,如果採用curl命令,命令爲:curl -I url

CURL驗證是否支持範圍請求:

如果服務端的響應信息裏面包含了上圖中Accept-Ranges: bytes,這個屬性,那麼說該URL是支持範圍請求的。如果URL返回消息體裏面,Accept-Ranges: none 或者壓根就沒有 Accept-Ranges這個屬性,那麼這個URL就是不支持範圍請求,也就是不支持斷點下載。

前面我們有看到,當使用curl命令獲取URL的響應時,服務端返回了一大段文本信息,我們要實現文件的斷點下載,就要從這些文本信息裏面獲取咱們斷點下載需要的重要參數,有了這些參數後才能實現我們想要達到的效果。

4.2 HTTP 斷點下載之Range語法說明

HTTP/1.1 中定義了一個 Range 的請求頭,來指定請求實體的範圍。它的範圍取值是在 0 - Content-Length 之間,使用 - 分割。

4.2.1 單區間段範圍請求

curl https://swsdl.vivo.com.cn/appstore/test-file-range-download.txt -i -H "Range: bytes=0-100"
HTTP/1.1 206 Partial Content
Date: Sun, 20 Dec 2020 03:06:43 GMT
Content-Type: text/plain
Content-Length: 101
Connection: keep-alive
Server: AliyunOSS
x-oss-request-id: 5FDEBFC33243A938379F9410
Accept-Ranges: bytes
ETag: "1FFD36BD1B06EB6C287AF8D788458808"
Last-Modified: Sun, 20 Dec 2020 03:04:33 GMT
x-oss-object-type: Normal
x-oss-hash-crc64ecma: 5148872045942545519
x-oss-storage-class: Standard
Content-MD5: H/02vRsG62woevjXiEWICA==
x-oss-server-time: 2
Content-Range: bytes 0-100/740
X-Via: 1.1 PShnzssxek171:14 (Cdn Cache Server V2.0), 1.1 x71:12 (Cdn Cache Server V2.0), 1.1 PS-FOC-01z6n168:27 (Cdn Cache Server V2.0)
X-Ws-Request-Id: 5fdebfc3_PS-FOC-01z6n168_36519-1719
Access-Control-Allow-Origin: *


4.2.2 多區間段範圍請求

curl https://swsdl.vivo.com.cn/appstore/test-file-range-download.txt -i -H "Range: bytes=0-100,200-300"
HTTP/1.1 206 Partial Content
Date: Sun, 20 Dec 2020 03:10:27 GMT
Content-Type: multipart/byteranges; boundary="Cdn Cache Server V2.0:37E1D9B3B2B94DF2F1D84393694C7E8A"
Content-Length: 506
Connection: keep-alive
Server: AliyunOSS
x-oss-request-id: 5FDEC030BDB66C33302A497E
Accept-Ranges: bytes
ETag: "1FFD36BD1B06EB6C287AF8D788458808"
Last-Modified: Sun, 20 Dec 2020 03:04:33 GMT
x-oss-object-type: Normal
x-oss-hash-crc64ecma: 5148872045942545519
x-oss-storage-class: Standard
Content-MD5: H/02vRsG62woevjXiEWICA==
x-oss-server-time: 2
Age: 1
X-Via: 1.1 xian23:7 (Cdn Cache Server V2.0), 1.1 PS-NTG-01KKN43:8 (Cdn Cache Server V2.0), 1.1 PS-FOC-01z6n168:27 (Cdn Cache Server V2.0)
X-Ws-Request-Id: 5fdec0a3_PS-FOC-01z6n168_36013-8986
Access-Control-Allow-Origin: *


--Cdn Cache Server V2.0:37E1D9B3B2B94DF2F1D84393694C7E8A
Content-Type: text/plain
Content-Range: bytes 0-100/740

--Cdn Cache Server V2.0:37E1D9B3B2B94DF2F1D84393694C7E8A
Content-Type: text/plain
Content-Range: bytes 200-300/740


看完上述請求的響應結果信息,我們發現使用單範圍區間請求時:Content-Type: text/plain,使用多範圍區間請求時:Content-Type: multipart/byteranges; boundary="Cdn Cache Server V2.0:37E1D9B3B2B94DF2F1D84393694C7E8A",並且在尾部信息裏面,攜帶了單個區間片段的Content-Type和Content-Range。另外,不知道大家有沒有發現一個很重要的信息,咱們的HTTP響應的狀態並非我們預想中的200,而是HTTP/1.1 206 Partial Content,這個狀態碼非常重要,因爲它標識着當次下載是否支持範圍請求。

4.3 異常場景之資源變更

有一種場景,不知道大家有沒有思考過,就是我們在下載一個大文件的時候,在未下載完成的時候,遠程文件已經發生了變更,如果我們繼續使用斷點下載,會出現什麼樣的問題?結果當然是文件與遠程文件不一致,會導致文件不可用。那麼我們有什麼辦法能夠在下載之前及時發現遠程文件已經變更,並及時進行調整下載方案呢?解決方法其實上面有給大家提到,遠程文件有沒有發生變化,有兩個標識:Etag和Last-Modified。二者任意一個屬性均可反應出來,相比而言,Etag會更精準些,原因如下:

  1. Last-Modified只能精確到秒級別,如果一秒內文件進行了多次修改,時間不會發生更新,但是文件的內容卻已經發生了變更,此時Etag會及時更新識別到變更;
  2. 在不同的時間節點(超過1秒),如果文件從A狀態改成B狀態,然後又重B狀態改回了A狀態,時間會發生更新,但是相較於A狀態文件內容,兩次變更後並沒又發生變化,此時Etag會變回最開始A狀態值,有點類似咱們併發編程裏面常說的ABA問題。

如果我們在進行範圍請求下載的時候,帶上了這兩個屬性中的一個或兩個,就能監控遠程文件發生了變化。如果發生了變化,那麼區間範圍請求的響應狀態就不是206而是200,說明它已經不支持該次請求的斷點下載了。接下來我們驗證一下Etag的驗證信息,我們的測試文件:ETag: "1FFD36BD1B06EB6C287AF8D788458808",然後我們將最後一個數值8改成9進行驗證,驗證如下:

文件未變更:

curl -I --header 'If-None-Match: "1FFD36BD1B06EB6C287AF8D788458808"' https://swsdl.vivo.com.cn/appstore/test-file-range-download.txt
HTTP/1.1 304 Not Modified
Date: Sun, 20 Dec 2020 03:53:03 GMT
Content-Type: text/plain
Connection: keep-alive
Last-Modified: Sun, 20 Dec 2020 03:04:33 GMT
ETag: "1FFD36BD1B06EB6C287AF8D788458808"
Age: 1
X-Via: 1.1 PS-FOC-01vM6221:15 (Cdn Cache Server V2.0)
X-Ws-Request-Id: 5fdeca9f_PS-FOC-01FMC220_2660-18267
Access-Control-Allow-Origin: *


文件已變更:

curl -I --header 'If-None-Match: "1FFD36BD1B06EB6C287AF8D788458809"' https://swsdl.vivo.com.cn/appstore/test-file-range-download.txt
HTTP/1.1 200 OK
Date: Sun, 20 Dec 2020 03:53:14 GMT
Content-Type: text/plain
Content-Length: 740
Connection: keep-alive
Server: AliyunOSS
x-oss-request-id: 5FDEC837E677A23037926897
Accept-Ranges: bytes
ETag: "1FFD36BD1B06EB6C287AF8D788458808"
Last-Modified: Sun, 20 Dec 2020 03:04:33 GMT
x-oss-object-type: Normal
x-oss-hash-crc64ecma: 5148872045942545519
x-oss-storage-class: Standard
Content-MD5: H/02vRsG62woevjXiEWICA==
x-oss-server-time: 17
X-Cache-Spec: Yes
Age: 1
X-Via: 1.1 xian23:7 (Cdn Cache Server V2.0), 1.1 PS-NTG-01KKN43:8 (Cdn Cache Server V2.0), 1.1 PS-FOC-01vM6221:15 (Cdn Cache Server V2.0)
X-Ws-Request-Id: 5fdecaaa_PS-FOC-01FMC220_4661-42392
Access-Control-Allow-Origin: *


結果顯示:當我們使用跟遠程文件一致的Etag時,狀態碼返回:HTTP/1.1 304 Not Modified,而使用篡改後的Etag後,返回狀態200,並且也攜帶了正確的Etag返回。所以我們在使用斷點下載過程中,對於這種資源變更的場景也是需要兼顧考慮的,不然就會出現下載後文件無法使用情況。

4.4 完整性驗證

文件在下載完成後,我們是不是就能直接使用呢?答案:NO。因爲我們無法確認文件是否跟遠程文件完全一致,所以在使用前,一定要做一次文件的完整性驗證。驗證方法很簡單,就是咱們前面提到過的屬性:Etag,資源版本的標識符,通常是消息摘要。帶雙引號的32位字符串,筆者驗證過,該屬性移除雙引號後,就是文件的MD5值,大家知道,文件MD5是可以用來驗證文件唯一性的標識。通過這個校驗,就能很好的識別解決本地文件被刪除、遠程資源文件變更的各類非常規的業務場景。

五、實踐部分

5.1 單線程斷點下載

假如我們需要下載1000個字節大小的文件,那麼我們在開始下載的時候,首先會獲取到文件的Content-Length,然後在第一次開始下載時,會使用參數:httpURLConnection.setRequestProperty("Range", "bytes=0-1000");

當下載到到150個字節大小的時候,因爲網絡問題或者客戶端服務重啓等情況,導致下載終止,那麼本地就存在一個大小爲150byte的不完整文件,當我們服務重啓後重新下載該文件時,我們不僅需要重新獲取遠程文件的大小,還需要獲取本地已經下載的文件大小,此時使用參數:httpURLConnection.setRequestProperty("Range", "bytes=150-1000");

來保證我們的下載是基於前一次的下載基礎之上的。圖示:

5.2 多線程斷點下載

多線程斷點下載的原理,與上面提到的單線程類似,唯一的區別在於:多個線程並行下載,單線程是串行下載。

5.3 代碼示例

5.3.1  獲取連接

在下載前,我們需要獲取遠程文件的HttpURLConnection 連接,如下:

/**
 * 獲取連接
 */
private static HttpURLConnection getHttpUrlConnection(String netUrl) throws Exception {
    URL url = new URL(netUrl);
    HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
    // 設置超時間爲3秒
    httpURLConnection.setConnectTimeout(3 * 1000);
    // 防止屏蔽程序抓取而返回403錯誤
    httpURLConnection.setRequestProperty("User-Agent", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt)");
    return httpURLConnection;
}


5.3.2 是否支持範圍請求

在進行斷點下載開始前,我們需要判斷該文件,是否支持範圍請求,支持的範圍請求,我們才能實現斷點下載,如下:

/**
 * 判斷連接是否支持斷點下載
 */
private static boolean isSupportRange(String netUrl) throws Exception {
    HttpURLConnection httpURLConnection = getHttpUrlConnection(netUrl);
    String acceptRanges = httpURLConnection.getHeaderField("Accept-Ranges");
    if (StringUtils.isEmpty(acceptRanges)) {
        return false;
    }
    if ("bytes".equalsIgnoreCase(acceptRanges)) {
        return true;
    }
    return false;
}


5.3.3 獲取遠程文件大小

當文件支持斷點下載,我們需要獲取遠程文件的大小,來設置Range參數的範圍區間,當然,如果是單線程斷線下載,不獲取遠程文件大小,使用 Range: start- 也是能完成斷點下載的,如下:

/**
 * 獲取遠程文件大小
 */
private static int getFileContentLength(String netUrl) throws Exception {
    HttpURLConnection httpUrlConnection = getHttpUrlConnection(netUrl);
    int contentLength = httpUrlConnection.getContentLength();
    closeHttpUrlConnection(httpUrlConnection);
    return contentLength;
}


5.3.4 單線程斷點下載

不管是單線程斷點下載還是多線程斷點下載,片段文件下載完成後,都無法繞開的一個問題,那就是文件合併。我們使用範圍請求,拿到了文件中的某個區間片段,最終還是要將各個片段合併成一個完整的文件,才能實現我們最初的下載目的。

相較而言,單線程的合併會比較簡單,因爲單線程斷點下載使用串行下載,在文件斷點寫入過程中,都是基於已有片段進行尾部追加,我們使用commons-io-2.4.jar裏面的一個工具方法,來實現文件的尾部追加:

5.3.4.1 文件分段

單線程-範圍分段

/**
 * 單線程串行下載
 *
 * @param totalFileSize 文件總大小
 * @param netUrl        文件地址
 * @param N             串行下載分段次數
 */
private static void segmentDownload(int totalFileSize, String netUrl, int N) throws Exception {
    // 本地文件目錄
    String localFilePath = "F:\\test_single_thread.txt";
    // 文件我們分N次來下載
    int eachFileSize = totalFileSize / N;
    for (int i = 1; i <= N; i++) {
        // 寫入本地文件
        File localFile = new File(localFilePath);
        // 獲取本地文件,如果爲空,則start=0,不爲空則爲該本地文件的大小作爲斷點下載開始位置
        long start = localFile.length();
        long end = 0;
        if (i == 1) {
            end = eachFileSize;
        } else if (i == N) {
            end = totalFileSize;
        } else {
            end = eachFileSize * i;
        }
        appendFile(netUrl, localFile, start, end);
        System.out.println(String.format("我是第%s次下載,下載片段範圍start=%s,end=%s", i, start, end));
    }
    File localFile = new File(localFilePath);
    System.out.println("本地文件大小:" + localFile.length());
}


5.3.4.2 文件追加

單線程-文件尾部追加

/**
 * 文件尾部追加
 * @param netUrl 地址
 * @param localFile 本地文件
 * @param start 分段開始位置
 * @param end 分段結束位置
 */
private static void appendFile(String netUrl, File localFile, long start, long end) throws Exception {
    HttpURLConnection httpURLConnection = getHttpUrlConnection(netUrl);
    httpURLConnection.setRequestProperty("Range", "bytes=" + start + "-" + end);
    // 獲取遠程文件流信息
    InputStream inputStream = httpURLConnection.getInputStream();
    // 本地文件寫入流,支持文件追加
    FileOutputStream fos = FileUtils.openOutputStream(localFile, true);
    IOUtils.copy(inputStream, fos);
    closeHttpUrlConnection(httpURLConnection);
}


單線程下載結果

遠程文件支持斷點下載
遠程文件大小:740
我是第1次下載,下載片段範圍start=0,end=246
我是第2次下載,下載片段範圍start=247,end=492
我是第3次下載,下載片段範圍start=493,end=740
本地文件和遠程文件一致,md5 = 1FFD36BD1B06EB6C287AF8D788458808, Etag = "1FFD36BD1B06EB6C287AF8D788458808"


5.3.5 多線程斷點下載

多線程的文件合併方式與單線程不一樣,因爲多線程是並行下載,每個子線程下載完成的時間是不確定的。這個時候,我們需要使用到java一個核心類:RandomAccessFile。這個類可以支持隨機的文件讀寫,其中有一個seek函數,可以將指針指向文件任意位置,然後進行讀寫。什麼意思呢,舉個栗子:假如我們開了10個線程,首先第一個下載完成的是線程X,它下載的數據範圍是300-400,那麼這時我們調用seek函數將指針動到300,然後調用它的write函數將byte寫出,這時候300之前都是NULL,300-400之後就是我們插入的數據。這樣就可以實現多線程下載和本地寫入了。話不多說,我們還是以代碼的方式來呈現:

5.3.5.1 資源分組

多線程-資源分組

/**
 * 多線程分組策略
 * @param netUrl 網絡地址
 * @param totalFileSize 文件總大小
 * @param N 線程池數量
 */
private static void groupDownload(String netUrl, int totalFileSize, int N) throws Exception {
    // 採用閉鎖特性來實現最後的文件校驗事件
    CountDownLatch countDownLatch = new CountDownLatch(N);
    // 本地文件目錄
    String localFilePath = "F:\\test_multiple_thread.txt";
    int groupSize = totalFileSize / N;
    int start = 0;
    int end = 0;
    for (int i = 1; i <= N; i++) {
        if (i <= 1) {
            start = groupSize * (i - 1);
            end = groupSize * i;
        } else if (i > 1 && i < N) {
            start = groupSize * (i - 1) + 1;
            end = groupSize * i;
        } else {
            start = groupSize * (i - 1) + 1;
            end = totalFileSize;
        }
        System.out.println(String.format("線程%s分配區間範圍start=%s, end=%s", i, start, end));
        downloadAndMerge(i, netUrl, localFilePath, start, end, countDownLatch);
    }
    // 校驗文件一致性
    countDownLatch.await();
    validateCompleteness(localFilePath, netUrl);
}


5.3.5.2 文件合併

多線程-文件合併


/**
 * 文件下載、合併
 *  @param threadNum     線程標識
 * @param netUrl        網絡文件地址
 * @param localFilePath 本地文件路徑
 * @param start         範圍請求開始位置
 * @param end           範圍請求結束位置
 * @param countDownLatch 閉鎖對象
 */
private static void downloadAndMerge(int threadNum, String netUrl, String localFilePath, int start, int end, CountDownLatch countDownLatch) {
    threadPoolExecutor.execute(() -> {
        try {
            HttpURLConnection httpURLConnection = getHttpUrlConnection(netUrl);
            httpURLConnection.setRequestProperty("Range", "bytes=" + start + "-" + end);
            // 獲取遠程文件流信息
            InputStream inputStream = httpURLConnection.getInputStream();
            RandomAccessFile randomAccessFile = new RandomAccessFile(localFilePath, "rw");
            // 文件寫入開始位置指針移動到已經下載位置
            randomAccessFile.seek(start);
            byte[] buffer = new byte[1024 * 10];
            int len = -1;
            while ((len = inputStream.read(buffer)) != -1) {
                randomAccessFile.write(buffer, 0, len);
            }
            closeHttpUrlConnection(httpURLConnection);
            System.out.println(String.format("下載完成時間%s, 線程:%s, 下載完成: start=%s, end = %s", System.currentTimeMillis(), threadNum, start, end));
        } catch (Exception e) {
            System.out.println(String.format("片段下載異常:線程:%s, start=%s, end = %s", threadNum, start, end));
            e.printStackTrace();
        }
        countDownLatch.countDown();
    });
}


多線程下載運行結果

遠程文件支持斷點下載
遠程文件大小:740
線程1分配區間範圍start=0, end=74
線程2分配區間範圍start=75, end=148
線程3分配區間範圍start=149, end=222
線程4分配區間範圍start=223, end=296
線程5分配區間範圍start=297, end=370
線程6分配區間範圍start=371, end=444
線程7分配區間範圍start=445, end=518
線程8分配區間範圍start=519, end=592
線程9分配區間範圍start=593, end=666
線程10分配區間範圍start=667, end=740
下載完成時間1608443874752, 線程:7, 下載完成: start=445, end = 518
下載完成時間1608443874757, 線程:2, 下載完成: start=75, end = 148
下載完成時間1608443874758, 線程:3, 下載完成: start=149, end = 222
下載完成時間1608443874759, 線程:5, 下載完成: start=297, end = 370
下載完成時間1608443874760, 線程:10, 下載完成: start=667, end = 740
下載完成時間1608443874760, 線程:1, 下載完成: start=0, end = 74
下載完成時間1608443874779, 線程:8, 下載完成: start=519, end = 592
下載完成時間1608443874781, 線程:6, 下載完成: start=371, end = 444
下載完成時間1608443874784, 線程:9, 下載完成: start=593, end = 666
下載完成時間1608443874788, 線程:4, 下載完成: start=223, end = 296
本地文件和遠程文件一致,md5 = 1FFD36BD1B06EB6C287AF8D788458808, Etag = "1FFD36BD1B06EB6C287AF8D788458808"


從運行結果可以出,子線程下載完成時間並沒有完全按着我們for循環指定的1-10線程標號順序完成,說明子線程之間是並行在寫入文件。其中還可以看到,子線程10和子線程1是在同一時間完成了文件的下載和寫入,這也很好的驗證了我們上面提到的RandomAccessFile類的效果。

5.3.6 完整性判斷

完整性校驗

/**
 * 校驗文件一致性,我們判斷Etag和本地文件的md5是否一致
 * 注:Etag攜帶了雙引號
 * @param localFilePath
 * @param netUrl
 */
private static void validateCompleteness(String localFilePath, String netUrl) throws Exception{
    File file = new File(localFilePath);
    InputStream data = new FileInputStream(file);
    String md5 = DigestUtils.md5Hex(data);
    HttpURLConnection httpURLConnection = getHttpUrlConnection(netUrl);
    String etag = httpURLConnection.getHeaderField("Etag");
    if (etag.toUpperCase().contains(md5.toUpperCase())) {
        System.out.println(String.format("本地文件和遠程文件一致,md5 = %s, Etag = %s", md5.toUpperCase(), etag));
    } else {
        System.out.println(String.format("本地文件和遠程文件不一致,md5 = %s, Etag = %s", md5.toUpperCase(), etag));
    }
}


六、寫在最後

文件斷點下載的優勢在於提升下載速度,但是也不是每種業務場景都適合,比如說業務網絡環境很好,下載的單個文件大小几十兆的情況下,使用斷點下載也沒有太大的優勢,反而增加了實現方案的複雜度。這就要求我們開發人員在使用時酌情考慮,而不是盲目使用。

作者:vivo-Tang Aibo

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章