擼了個多線程斷點續傳下載器,我從中學習到了這些知識

文章已經收錄在 Github.com/niumoo/JavaNotes ,更有 Java 程序員所須要掌握的核心知識,歡迎Star和指教。
歡迎關注個人 公衆號,文章每週更新。

感謝看客老爺點進來了,週末閒來無事,想起同事強哥的那句話:「你有沒有玩過斷點續傳?」 當時轉念一想,斷點續傳下載用的確實很多,具體細節嘛,真的沒有去思考過啊。這不,思考事後有了這篇文章。感謝強哥,讓我有了一篇能夠水的文章,下面會用純 Java 無依賴實現一個簡單的多線程斷點續傳下載器html

這篇水文章到底有什麼內容呢?先簡單列舉一下,順便思考幾個問題。java

  1. 斷點續傳的原理。
  2. 重啓續傳文件時,怎麼保證文件的一致性?
  3. 同一個文件多線程下載如何實現?
  4. 網速帶寬固定,爲何多線程下載能夠提速?

多線程斷點續傳會用到哪些知識呢?上面已經拋出了幾個問題,不放思考一下。下面會針對上面的四個問題一一進行解釋,如今大多數的服務均可以在線提供,下載使用的場景愈來愈少,不過這不妨礙咱們對原理的探求。git

斷點續傳的原理

想要了解斷點續傳是如何實現的,那麼確定是要了解一下 HTTP 協議了。HTTP 協議是互聯網上應用最普遍網絡傳輸協議之一,它基於 TCP/IP 通訊協議來傳遞數據。因此斷點續傳的奧祕也就隱藏在這 HTTP 協議中了。 程序員

咱們都知道 HTTP 請求會有一個 Request headerResponse header ,就在這請求頭和響應頭裏,有一個和 Range 相關的參數。下面經過百度網盤的 pc 客戶端下載連接進行測試。github

使用 cURL 查看 response header. 若是你想知道更多關於 cURL 的用法,能夠看我以前的一篇文章 :進來領略下cURL的獨門絕技面試

$ curl -I http://wppkg.baidupcs.com/issue/netdisk/yunguanjia/BaiduYunGuanjia_7.0.1.1.exe
HTTP/1.1 200 OK
Server: JSP3/2.0.14
Date: Sat, 25 Jul 2020 13:41:55 GMT
Content-Type: application/x-msdownload
Content-Length: 65804256
Connection: keep-alive
ETag: dcd0bfef7d90dbb3de50a26b875143fc
Last-Modified: Tue, 07 Jul 2020 13:19:46 GMT
Expires: Sat, 25 Jul 2020 14:05:19 GMT
Age: 257796
Accept-Ranges: bytes
Cache-Control: max-age=259200
Content-Disposition: attachment;filename="BaiduYunGuanjia_7.0.1.1.exe"
x-bs-client-ip: MTgwLjc2LjIyLjU0
x-bs-file-size: 65804256
x-bs-request-id: MTAuMTM0LjM0LjU2Ojg2NDM6NDM4MTUzMTE4NTU3ODc5MTIxNzoyMDIwLTA3LTA3IDIyOjAxOjE1
x-bs-meta-crc32: 3545941535
Content-MD5: dcd0bfef7d90dbb3de50a26b875143fc
superfile: 2
Ohc-Response-Time: 1 0 0 0 0 0
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, PUT, POST, DELETE, OPTIONS, HEAD
Ohc-Cache-HIT: bj2pbs54 [2], bjbgpcache54 [4]

能夠看到百度 pc 客戶端的 response header 信息有不少,咱們只須要重點關注幾個。算法

Content-Length: 65804256  // 請求的文件的大小,單位 byte
Accept-Ranges: bytes      // 是否容許指定傳輸範圍,bytes:範圍請求的單位是 bytes (字節),none:不支持任何範圍請求單位,
Last-Modified: Tue, 07 Jul 2020 13:19:46 GMT  // 服務端文件最後修改時間,能夠用於校驗文件是否更改過
x-bs-meta-crc32: 3545941535    // crc32,能夠用於校驗文件是否更改過
ETag: dcd0bfef7d90dbb3de50a26b875143fc //Etag 標籤,能夠用於校驗文件是否更改過

可見並不見得全部下載都支持斷點續傳,只有在 response header 中有 Accept-Ranges: bytes 字段時才能夠斷點續傳。若是有這個信息,該怎麼斷點續傳呢?其實只須要在 response header 中指定 Content-Range 值就能夠了。shell

Content-Range 使用格式有下面幾種。api

Content-Range: <unit>=<range-start>-<range-end>/<size> // size 爲文件總大小,若是不知道能夠用 *
Content-Range: <unit>=<range-start>-<range-end>/*  
Content-Range: <unit>=<range-start>-
Content-Range: <unit>=*/<size>

舉例網絡

單位 bytes,從第 10 個 bytes 開始下載:Content-Range: bytes=10-.

單位 bytes,從第 10 個 bytes 開始下載,下載到第100個 bytes:Content-Range: bytes=10-100.

這就是斷點續傳實現的原理了,你能夠能已經發現了,Content-Range 的 start 和 end 已經讓分段下載有了可能。

怎麼保證文件的一致性?

這裏要說的文件完整性有兩個方面,一個是下載階段的,一個是寫入階段的。

由於咱們要寫的下載器是支持斷點續傳的,那麼在進行續傳時,怎麼肯定文件自從咱們上次下載時沒有進行過更新呢?其實能夠經過 response header 中的幾個屬性值進行判斷。

Last-Modified: Tue, 07 Jul 2020 13:19:46 GMT  // 服務端文件最後修改時間,能夠用於校驗文件是否更改過
ETag: dcd0bfef7d90dbb3de50a26b875143fc //Etag 標籤,能夠用於校驗文件是否更改過
x-bs-meta-crc32: 3545941535    // crc32,能夠用於校驗文件是否更改過

Last-ModifiedETag 均可以用來檢驗文件是否更新過,根據 HTTP 協議的規定,當文件更新時,是會生成新的 ETag 值的,它相似於文件的指紋信息,而 Last-Modified 只是上次修改時間,有時可能並不可以證實文件內容被修改過。

上面是下載階段的文件一致性校驗,那麼在寫入階段呢?無論單線程仍是多線程,因爲要斷點續傳,在寫入時都要在指定位置進行字符追加。在 Java 中有沒有好的實現方式?

答案是必定的,使用 RandomAccessFile 類便可,RandomAccessFile 不一樣於其餘的流操做。它能夠在使用時指定讀寫模式,使用 seek 方法隨意的移動要操做的文件指針位置。很適合斷點續傳的寫入場景。

好比在 test.txt 的位置 0 開始寫入字符 abc,在位置 100 開始寫入字符 ddd.

try (RandomAccessFile rw = new RandomAccessFile("test.txt", "rw")){ // rw 爲讀寫模式
    rw.seek(0); // 移動文件內容指針位置
    rw.writeChars("abc");
    rw.seek(100);
    rw.writeChars("ddd");
}

斷點續傳的寫入就靠它了,在續傳時只須要移動文件內容指針到要續傳的位置便可。

seek 方法還有不少妙用,好比使用它你能夠快速定位到已知的位置,進行快速檢索;也能夠在同一個文件的不一樣位置進行併發讀寫

多線程下載如何實現?

多線程下載必然要每一個線程下載文件中的一部分,而後把每一個線程下載到的文件內容組裝成一個完整的文件,在這個過程當中確定是一個 byte 都不能出錯的,否則你組裝起來的文件是確定運行不起來的。那麼怎麼實現下載文件的一部分呢?其實在斷點續傳的部分已經介紹過了,仍是 Content-Range 參數,只要計算好每一個部分要下載的 bytes 範圍就能夠了。

好比:單位 bytes,第二部分從第 10 個 bytes 開始下載,下載到第100個 bytes:Content-Range: bytes=10-100.

網速帶寬固定,爲何多線程下載能夠提速?

這是一個比較有意思的問題了,最大網速是固定的,運營商給你 100Mbs 的網速,無論你怎麼使用,速度最大也就是 100/8=12.5MB/S. 既然瓶頸在這裏,爲何多線程下載能夠提速呢?其實理論上來講,單線程下載就能夠達到最大網速。可是每每事實是網絡不是那麼通暢,十分擁堵,很難達到理想的最大速度。也就是說只有在網絡不那麼通暢的時候,多線程下載才能提速。不然,單線程便可。不過最大速度永遠都是網絡帶寬。

那爲何多線程下載能夠提速呢?HTTP 協議在傳輸時候是基於 TCP 協議傳輸數據的,爲了弄明白這個問題須要瞭解一下 TCP 協議的擁塞控制機制。擁塞控制 是TCP 的一個避免網絡擁塞的算法,它是基於和性增加/乘性下降這樣的控制方法來控制擁塞的。

TCP 擁塞控制

簡單來講就是在 TCP 開始傳輸數據時,服務端會不斷的探測可用帶寬。在一個傳輸內容段被成功接收後,會加倍傳輸兩倍段內容,若是再次被成功接收,就繼續加倍,直到發生了丟包,這是這也被叫作慢啓動。當達到慢啓動閥值(ssthresh)時,滿啓動算法就會轉換爲線性增加的階段,每次只增長一個分段,放緩增長速度。我以爲其實慢啓動的加倍增速過程並不慢,只是一種叫法。

可是當發生了丟包,也就是檢測到擁塞時,發送方就會將發送段大小下降一個乘數,好比二分之一,慢啓動閾值降爲超時前擁塞窗口的一半大小、擁塞窗口會降爲1個MSS,而且從新回到慢啓動階段。這時多線程的優點就體現出來了,由於你的多線程會讓這個速度減速沒有那麼猛烈,畢竟這時可能有另外一個線程正處在慢啓動的在最終加速階段,這樣整體的下載速度就優於單線程了。

多線程斷點續傳代碼實現

基於上面的原理介紹,內心應該有了具體的實現思路了。咱們只須要使用多線程,結合 Content-Range 參數分段請求文件內容保存到臨時文件,下載完畢後使用 RandomAccessFile 把下載的文件合併成一個文件便可。而在須要斷點續傳時,只須要讀取一下當前臨時文件大小,而後調整 Content-Range ,就能夠進行續傳下載。

代碼很少,下面是部分核心代碼,完整代碼能夠直接點開文章最後的 Github 倉庫。

  1. Content-Range 請求指定文件的區間內容。
URL httpUrl = new URL(url);
HttpURLConnection httpConnection = (HttpURLConnection)httpUrl.openConnection();
httpConnection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36");
httpConnection.setRequestProperty("RANGE", "bytes=" + start + "-" + end + "/*");
InputStream inputStream = httpConnection.getInputStream();
  1. 獲取文件的 ETag.
Map<String, List<String>> headerFields = httpConnection.getHeaderFields();
List<String> eTagList = headerFields.get("ETag");
System.out.println(eTagList.get(0));
  1. 使用 RandomAccessFile 續傳寫入文件。
RandomAccessFile oSavedFile = new RandomAccessFile(httpFileName, "rw");
oSavedFile.seek(localFileContentLength); // 文件寫入開始位置指針移動到已經下載位置
byte[] buffer = new byte[1024 * 10];
int len = -1;
while ((len = inputStream.read(buffer)) != -1) {
    oSavedFile.write(buffer, 0, len);
}

斷點續傳測試,下載一部分以後關閉程序再次啓動。

多線程下載測試

完整代碼已經上傳到 github.com/niumoo/down-bit.

參考:

[1] HTTP headers

[2] Class RandomAccessFile

[3] RandomAccessFile簡介與使用

[4] 維基百科 - TCP擁塞控制)

[5] 維基百科 - 和性增加/乘性下降)

最後的話

文章已經收錄在 Github.com/niumoo/JavaNotes ,歡迎Star和指教。更有一線大廠面試點,Java程序員須要掌握的核心知識等文章,也整理了不少個人文字,歡迎 Star 和完善,但願咱們一塊兒變得優秀。

文章有幫助能夠點個「」或「分享」,都是支持,我都喜歡!
文章每週持續更新,要實時關注我更新的文章以及分享的乾貨,能夠關注「 未讀代碼 」公衆號或者個人博客

公衆號

相關文章
相關標籤/搜索