擼了個多線程斷點續傳下載器,我從中學習到了這些知識(附開源地址)

2021-02-21 Java知音

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

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

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

斷點續傳的原理

想要了解斷點續傳是如何實現的,那麼肯定是要了解一下 HTTP 協議了。HTTP 協議是網際網路上應用最廣泛網絡傳輸協議之一,它基於 TCP/IP 通信協議來傳遞數據。所以斷點續傳的奧秘也就隱藏在這 HTTP 協議中了。

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

$ 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 值就可以了。

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

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-Modified 和 ETag 都可以用來檢驗文件是否更新過,根據 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();

Map<String, List<String>> headerFields = httpConnection.getHeaderFields();
List<String> eTagList = headerFields.get("ETag");
System.out.println(eTagList.get(0));

3.使用 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] 維基百科 - 和性增長/乘性降低

相關焦點

  • JAVA實現大文件多線程下載,提速30倍!想學?我教你啊
    前言兄弟們看到這個標題可能會覺得是個標題黨,為了解決疑慮,我們先來看下最終的測試結果:測試雲盤下載的文件 46M,自己本地最大下載速度 2M1. 單線程下載,總耗時: 603s2. 多線程下載,50個線程,總耗時:13s
  • 新增在線備份多線程傳輸功能 百度雲PC客戶端3.2版試用
    同時,還實現了對文件夾、批量文件的拖拽上傳功能,支持多任務、斷點續傳、秒傳功能,以及web端獨立文件夾,可實現只上傳不同步。至於百度雲PC客戶端3.2版功能如何,小編下文便對新版本進行了一番體驗。下載體驗地址:http://pan.baidu.com/download提起百度雲,小編從最初的百度網盤使用至今,一直以來對其快速的上傳下載速度所折服,作為編輯,小編經常在外度奔波,隨時隨地的將各種如現場圖片、錄音、視頻文件及寫好的稿件上傳至網盤,百度雲所體現出的穩定性及無論在3G或是電信、聯通網絡下,總能快速的將文件同步至雲端並迅速通過郵件或簡訊分享給同事。
  • 如何調試多線程程序
    在上一篇文章《使用 gdb 調試多進程程序 —— 以調試 nginx 為例》我們介紹了如何使用 gdb 調試多進程程序,這篇文章我們來介紹下如何使用 gdb 調試多線程程序,同時這個方法也是我閱讀和分析一個新的 C/C++ 項目常用的方法。當然,多線程調試的前提是你需要熟悉多線程的基礎知識,包括線程的創建和退出、線程之間的各種同步原語等。
  • 我教你啊!
    前言兄弟們看到這個標題可能會覺得是個標題黨,為了解決疑慮,我們先來看下最終的測試結果:測試雲盤下載的文件 46M,自己本地最大下載速度 2M1. 單線程下載,總耗時: 603simg2. 多線程下載,50個線程,總耗時:13s
  • Python 10min 面試題解析丨Python實現多連接下載器
    今天群裡看到有人問關於 Python 多線程寫文件的問題,聯想到這是 Reboot 的架構師班的入學題.我想了一下,感覺坑和考察的點還挺多的,可以當成一個面試題來問,簡單說一下我的想法和思路吧,涉及的代碼和注釋在 GitHub
  • 五道口學院小分隊隊長帶你橫掃:多線程+微服務架構+Docker+k8s
    多線程在本書寫作的過程中,我儘量減少「噦嗦」的文字語言,全部用案例來講解技術點的實現,使讀者看到代碼及運行結果後就可以知道此項目要解決的是什麼問題,類似於網絡中的博客風格,可讓讀者用最短的時間學完相關知識點,明白這些知識點是如何應用的,以及在使用時要避免什麼
  • IDM 下載 (多線程高速下載器)
    免責聲明:以下軟體來自網際網路,僅供學習交流使用
  • Windows線程保護之調試逃逸源碼實現及內核逆向分析
    ,其好處是顯而易見的;今天就來講一下另一種系統原生的線程調試逃逸技術,筆者第一次接觸這個技術是當時做一個項目,需要逆向某軟體,找到它的某些關鍵數據的來源時,用Windbg Attach上去之後,發現對某個線程下的斷點,斷不下來,而其他線程的斷點都是沒有問題的,後來研究了下,發現了這麼個技術,撰寫此文,與君分享。
  • 線程、進程、多線程、多進程、 多任務!懵逼了吧?
    可能學習嵌入式開發的讀者都聽說過這些專業名詞,但又多少人理解了?
  • 多線程程序中操作的原子性
    在多線程程序中原子操作是一個非常重要的概念,它常常用來實現一些同步機制,同時也是一些常見的多線程Bug的源頭。本文主要討論了三個問題:1. 多線程程序中對變量的讀寫操作是否是原子的?2. 多線程程序中對Bit field(位域)的讀寫操作是否是線程安全的?3. 程式設計師該如何使用原子操作?
  • 一文探討 RPC 框架中的服務線程隔離
    果然學習之路不能停!微服務如今應當是一個優秀的程式設計師必須學習的一種架構思想,而RPC框架作為微服務的核心,不說讀一遍源碼吧,起碼它的實現原理還是應該知道的。然而目前的RPC服務框架,大多存在一個問題,就是當服務提供端Provider應用中,有的服務流量大,耗時長,導致線程池資源被這些服務佔盡,從而影響同一應用中的其他服務正常提供。
  • 線程、進程、多線程、多進程和多任務有啥關係?
    進程又被細化為線程,也就是一個進程下有多個能獨立運行的更小的單位。在同一個時間裡,同一個計算機系統中如果允許兩個或兩個以上的進程處於運行狀態,這便是多任務。現代的作業系統幾乎都是多任務作業系統,能夠同時管理多個進程的運行。多任務帶來的好處是明顯的,比如你可以邊聽網易雲音樂,一邊上網,與此同時甚至可以將下載的文檔列印出來,而這些任務之間絲毫不會相互幹擾。
  • Linux 多線程詳解 —— 什麼是線程
    線程是怎樣描述的?線程實際上也是一個task_struct,工作線程拷貝主線程的task_struct,然後共用主線程的mm_struct。線程ID是在用task_struct中pid描述的,而task_struct中tgid是線程組ID,表示線程屬於該線程組,對於主線程而言,其pid和tgid是相同的,我們一般看到的進程ID就是tgid。
  • ...2.1 發布,租戶模式支持動態新增數據源 - OSCHINA - 中文開源...
    將分散在各個服務的枚舉接口(/enums)合併到 Oauth 服務,前端在登錄之後,一次性拉取存放在前端緩存中。9. 日誌統一存儲路徑: logging.file.path 移動到 src/main/filters/config-dev.properties 統一配置10.
  • 別再問我多線程的這些問題了
    很多同學面對多線程的問題都很頭大,因為自己做項目很難用到,但是但凡高薪的職位面試都會問到。。畢竟現在大廠裡用的都是多線程高並發,所以這塊內容不吃透肯定是不行的。比如在任務管理器裡的就是一個個進程,就是「動起來」的應用程式。Q:這些進程是並行執行的嗎?單核 CPU 一個時間片裡只能執行一個進程。
  • 玩大數據一定用得到的19款 Java 開源 Web 爬蟲
    Heritrix是按多線程方式抓取的爬蟲,主線程把任務分配給Teo線程(處理線程),每個Teo線程每次處理一個URL。Teo線程對每個URL執行一遍URL處理器鏈。URL處理器鏈包括如下5個處理步驟。(1)預取鏈:主要是做一些準備工作,例如,對處理進行延遲和重新處理,否決隨後的操作。(2)提取鏈:主要是下載網頁,進行DNS轉換,填寫請求和響應表單。