<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          擼了個(gè)多線程斷點(diǎn)續(xù)傳下載器,我從中學(xué)習(xí)到了這些知識(shí)

          共 5826字,需瀏覽 12分鐘

           ·

          2020-07-28 16:39

          歡迎點(diǎn)擊?“未讀代碼” ,關(guān)注公眾號(hào),文章每周更新

          2020年6月20日?上海張江

          感謝看客老爺點(diǎn)進(jìn)來(lái)了,周末閑來(lái)無(wú)事,想起同事強(qiáng)哥的那句話:“你有沒(méi)有玩過(guò)斷點(diǎn)續(xù)傳?” 當(dāng)時(shí)轉(zhuǎn)念一想,斷點(diǎn)續(xù)傳下載用的確實(shí)不少,具體細(xì)節(jié)嘛,真的沒(méi)有去思考過(guò)啊。這不,思考過(guò)后有了這篇文章。感謝強(qiáng)哥,讓我有了一篇可以水的文章,下面會(huì)用純 Java 無(wú)依賴實(shí)現(xiàn)一個(gè)簡(jiǎn)單的多線程斷點(diǎn)續(xù)傳下載器。

          這篇文章到底有什么內(nèi)容呢?先簡(jiǎn)單列舉一下,順便思考幾個(gè)問(wèn)題。

          1. 斷點(diǎn)續(xù)傳的原理。
          2. 重啟續(xù)傳文件時(shí),怎么保證文件的一致性?
          3. 同一個(gè)文件多線程下載如何實(shí)現(xiàn)?
          4. 網(wǎng)速帶寬固定,為什么多線程下載可以提速?

          多線程斷點(diǎn)續(xù)傳會(huì)用到哪些知識(shí)呢?上面已經(jīng)拋出了幾個(gè)問(wèn)題,不妨思考一下。下面會(huì)針對(duì)上面的四個(gè)問(wèn)題一一進(jìn)行解釋,現(xiàn)在大多數(shù)的服務(wù)都可以在線提供,下載使用的場(chǎng)景越來(lái)越少,不過(guò)這不妨礙我們對(duì)原理的探求。

          斷點(diǎn)續(xù)傳的原理

          想要了解斷點(diǎn)續(xù)傳是如何實(shí)現(xiàn)的,那么肯定是要了解一下 HTTP 協(xié)議了。HTTP 協(xié)議是互聯(lián)網(wǎng)上應(yīng)用最廣泛網(wǎng)絡(luò)傳輸協(xié)議之一,它基于 TCP/IP 通信協(xié)議來(lái)傳遞數(shù)據(jù)。所以斷點(diǎn)續(xù)傳的奧秘也就隱藏在這 HTTP 協(xié)議中了。

          我們都知道 HTTP 請(qǐng)求會(huì)有一個(gè) Request headerResponse header ,就在這請(qǐng)求頭和響應(yīng)頭里,有一個(gè)和 Range 相關(guān)的參數(shù)。下面通過(guò)百度網(wǎng)盤(pán)的 pc 客戶端下載鏈接進(jìn)行測(cè)試。

          使用 cURL 查看 response header. 如果你想知道更多關(guān)于 cURL 的用法,可以看我之前的一篇文章 :進(jìn)來(lái)領(lǐng)略下cURL的獨(dú)門(mén)絕技。

          $ curl -I http://wppkg.baidupcs.com/issue/netdisk/yunguanjia/BaiduYunGuanjia_7.0.1.1.exeHTTP/1.1 200 OKServer: JSP3/2.0.14Date: Sat, 25 Jul 2020 13:41:55 GMTContent-Type: application/x-msdownloadContent-Length: 65804256Connection: keep-aliveETag: dcd0bfef7d90dbb3de50a26b875143fcLast-Modified: Tue, 07 Jul 2020 13:19:46 GMTExpires: Sat, 25 Jul 2020 14:05:19 GMTAge: 257796Accept-Ranges: bytesCache-Control: max-age=259200Content-Disposition: attachment;filename="BaiduYunGuanjia_7.0.1.1.exe"x-bs-client-ip: MTgwLjc2LjIyLjU0x-bs-file-size: 65804256x-bs-request-id: MTAuMTM0LjM0LjU2Ojg2NDM6NDM4MTUzMTE4NTU3ODc5MTIxNzoyMDIwLTA3LTA3IDIyOjAxOjE1x-bs-meta-crc32: 3545941535Content-MD5: dcd0bfef7d90dbb3de50a26b875143fcsuperfile: 2Ohc-Response-Time: 1 0 0 0 0 0Access-Control-Allow-Origin: *Access-Control-Allow-Methods: GET, PUT, POST, DELETE, OPTIONS, HEADOhc-Cache-HIT: bj2pbs54 [2], bjbgpcache54 [4]

          可以看到百度 pc 客戶端的 response header 信息有很多,我們只需要重點(diǎn)關(guān)注幾個(gè)。

          Content-Length: 65804256  // 請(qǐng)求的文件的大小,單位 byteAccept-Ranges: bytes      // 是否允許指定傳輸范圍,bytes:范圍請(qǐng)求的單位是 bytes (字節(jié)),none:不支持任何范圍請(qǐng)求單位,Last-Modified: Tue, 07 Jul 2020 13:19:46 GMT  // 服務(wù)端文件最后修改時(shí)間,可以用于校驗(yàn)文件是否更改過(guò)x-bs-meta-crc32: 3545941535	// crc32,可以用于校驗(yàn)文件是否更改過(guò)ETag: dcd0bfef7d90dbb3de50a26b875143fc //Etag 標(biāo)簽,可以用于校驗(yàn)文件是否更改過(guò)

          可見(jiàn)并不見(jiàn)得所有下載都支持?jǐn)帱c(diǎn)續(xù)傳,只有在 ?response header 中有 Accept-Ranges: bytes 字段時(shí)才可以斷點(diǎn)續(xù)傳。如果有這個(gè)信息,該怎么斷點(diǎn)續(xù)傳呢?其實(shí)只需要在 response header 中指定 Content-Range 值就可以了。

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

          Content-Range: =-/ // size 為文件總大小,如果不知道可以用 *Content-Range: =-/*  Content-Range: =-Content-Range: =*/

          舉例

          單位 bytes,從第 10 個(gè) bytes 開(kāi)始下載:Content-Range: bytes=10-.

          單位 bytes,從第 10 個(gè) bytes 開(kāi)始下載,下載到第100個(gè) bytes:Content-Range: bytes=10-100.

          這就是斷點(diǎn)續(xù)傳實(shí)現(xiàn)的原理了,你可以能已經(jīng)發(fā)現(xiàn)了,Content-Range 的 start 和 end 已經(jīng)讓分段下載有了可能。

          怎么保證文件的一致性?

          這里要說(shuō)的文件完整性有兩個(gè)方面,一個(gè)是下載階段的,一個(gè)是寫(xiě)入階段的。

          因?yàn)槲覀円獙?xiě)的下載器是支持?jǐn)帱c(diǎn)續(xù)傳的,那么在進(jìn)行續(xù)傳時(shí),怎么確定文件自從我們上次下載時(shí)沒(méi)有進(jìn)行過(guò)更新呢?其實(shí)可以通過(guò) response header 中的幾個(gè)屬性值進(jìn)行判斷。

          Last-Modified: Tue, 07 Jul 2020 13:19:46 GMT  // 服務(wù)端文件最后修改時(shí)間,可以用于校驗(yàn)文件是否更改過(guò)ETag: dcd0bfef7d90dbb3de50a26b875143fc //Etag 標(biāo)簽,可以用于校驗(yàn)文件是否更改過(guò)x-bs-meta-crc32: 3545941535	// crc32,可以用于校驗(yàn)文件是否更改過(guò)

          Last-ModifiedETag 都可以用來(lái)檢驗(yàn)文件是否更新過(guò),根據(jù) HTTP 協(xié)議的規(guī)定,當(dāng)文件更新時(shí),是會(huì)生成新的 ETag 值的,它類似于文件的指紋信息,而 Last-Modified 只是上次修改時(shí)間,有時(shí)可能并不能夠證明文件內(nèi)容被修改過(guò)。

          上面是下載階段的文件一致性校驗(yàn),那么在寫(xiě)入階段呢?不管單線程還是多線程,由于要斷點(diǎn)續(xù)傳,在寫(xiě)入時(shí)都要在指定位置進(jìn)行字符追加。在 Java 中有沒(méi)有好的實(shí)現(xiàn)方式?

          答案是一定的,使用 RandomAccessFile 類即可,RandomAccessFile 不同于其他的流操作。它可以在使用時(shí)指定讀寫(xiě)模式,使用 seek 方法隨意的移動(dòng)要操作的文件指針位置。很適合斷點(diǎn)續(xù)傳的寫(xiě)入場(chǎng)景。

          比如在 test.txt 的位置 0 開(kāi)始寫(xiě)入字符 abc,在位置 100 開(kāi)始寫(xiě)入字符 ddd.

          try (RandomAccessFile rw = new RandomAccessFile("test.txt", "rw")){ // rw 為讀寫(xiě)模式    rw.seek(0); // 移動(dòng)文件內(nèi)容指針位置    rw.writeChars("abc");    rw.seek(100);    rw.writeChars("ddd");}

          斷點(diǎn)續(xù)傳的寫(xiě)入就靠它了,在續(xù)傳時(shí)只需要移動(dòng)文件內(nèi)容指針到要續(xù)傳的位置即可。

          seek 方法還有很多妙用,比如使用它你可以快速定位到已知的位置,進(jìn)行快速檢索;也可以在同一個(gè)文件的不同位置進(jìn)行并發(fā)讀寫(xiě)。

          多線程下載如何實(shí)現(xiàn)?

          多線程下載必然要每個(gè)線程下載文件中的一部分,然后把每個(gè)線程下載到的文件內(nèi)容組裝成一個(gè)完整的文件,在這個(gè)過(guò)程中肯定是一個(gè) byte 都不能出錯(cuò)的,不然你組裝起來(lái)的文件是肯定運(yùn)行不起來(lái)的。那么怎么實(shí)現(xiàn)下載文件的一部分呢?其實(shí)在斷點(diǎn)續(xù)傳的部分已經(jīng)介紹過(guò)了,還是 Content-Range 參數(shù),只要計(jì)算好每個(gè)部分要下載的 bytes 范圍就可以了。

          比如:?jiǎn)挝?bytes,第二部分從第 10 個(gè) bytes 開(kāi)始下載,下載到第100個(gè) bytes:Content-Range: bytes=10-100.

          網(wǎng)速帶寬固定,為什么多線程下載可以提速?

          這是一個(gè)比較有意思的問(wèn)題了,最大網(wǎng)速是固定的,運(yùn)營(yíng)商給你 100Mbs 的網(wǎng)速,不管你怎么使用,速度最大也就是 100/8=12.5MB/S. 既然瓶頸在這里,為什么多線程下載可以提速呢?其實(shí)理論上來(lái)說(shuō),單線程下載就可以達(dá)到最大網(wǎng)速。但是往往事實(shí)是網(wǎng)絡(luò)不是那么通暢,十分擁堵,很難達(dá)到理想的最大速度。也就是說(shuō)只有在網(wǎng)絡(luò)不那么通暢的時(shí)候,多線程下載才能提速。否則,單線程即可。不過(guò)最大速度永遠(yuǎn)都是網(wǎng)絡(luò)帶寬。

          那為什么多線程下載可以提速呢?HTTP 協(xié)議在傳輸時(shí)候是基于 TCP 協(xié)議傳輸數(shù)據(jù)的,為了弄明白這個(gè)問(wèn)題需要了解一下 TCP 協(xié)議的擁塞控制機(jī)制。擁塞控制 是TCP 的一個(gè)避免網(wǎng)絡(luò)擁塞的算法,它是基于和性增長(zhǎng)/乘性降低這樣的控制方法來(lái)控制擁塞的。

          TCP 擁塞控制

          簡(jiǎn)單來(lái)說(shuō)就是在 TCP 開(kāi)始傳輸數(shù)據(jù)時(shí),服務(wù)端會(huì)不斷的探測(cè)可用帶寬。在一個(gè)傳輸內(nèi)容段被成功接收后,會(huì)加倍傳輸兩倍段內(nèi)容,如果再次被成功接收,就繼續(xù)加倍,直到發(fā)生了丟包,這是這也被叫做慢啟動(dòng)。當(dāng)達(dá)到**慢啟動(dòng)閥值(ssthresh)**時(shí),慢啟動(dòng)算法就會(huì)轉(zhuǎn)換為線性增長(zhǎng)的階段,每次只增加一個(gè)分段,放緩增加速度。我覺(jué)得其實(shí)慢啟動(dòng)的加倍增速過(guò)程并不慢,只是一種叫法。

          但是當(dāng)發(fā)生了丟包,也就是檢測(cè)到擁塞時(shí),發(fā)送方就會(huì)將發(fā)送段大小降低一個(gè)乘數(shù),比如二分之一,慢啟動(dòng)閾值降為超時(shí)前擁塞窗口的一半大小、擁塞窗口會(huì)降為1個(gè)MSS,并且重新回到慢啟動(dòng)階段。這時(shí)多線程的優(yōu)勢(shì)就體現(xiàn)出來(lái)了,因?yàn)槟愕亩嗑€程會(huì)讓這個(gè)速度減速?zèng)]有那么猛烈,畢竟這時(shí)可能有另一個(gè)線程正處在慢啟動(dòng)的在最終加速階段,這樣總體的下載速度就優(yōu)于單線程了。

          多線程斷點(diǎn)續(xù)傳代碼實(shí)現(xiàn)

          基于上面的原理介紹,心里應(yīng)該有了具體的實(shí)現(xiàn)思路了。我們只需要使用多線程,結(jié)合 Content-Range 參數(shù)分段請(qǐng)求文件內(nèi)容保存到臨時(shí)文件,下載完畢后使用 RandomAccessFile 把下載的文件合并成一個(gè)文件即可。而在需要斷點(diǎn)續(xù)傳時(shí),只需要讀取一下當(dāng)前臨時(shí)文件大小,然后調(diào)整 Content-Range ,就可以進(jìn)行續(xù)傳下載。

          代碼不多,下面是部分核心代碼,完整代碼可以直接點(diǎn)開(kāi)文章最后的 Github 倉(cāng)庫(kù)。

          1. Content-Range 請(qǐng)求指定文件的區(qū)間內(nèi)容。
          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> headerFields = httpConnection.getHeaderFields();List eTagList = headerFields.get("ETag");System.out.println(eTagList.get(0));
          1. 使用 RandomAccessFile 續(xù)傳寫(xiě)入文件。
          RandomAccessFile oSavedFile = new RandomAccessFile(httpFileName, "rw");oSavedFile.seek(localFileContentLength); // 文件寫(xiě)入開(kāi)始位置指針移動(dòng)到已經(jīng)下載位置byte[] buffer = new byte[1024 * 10];int len = -1;while ((len = inputStream.read(buffer)) != -1) {    oSavedFile.write(buffer, 0, len);}

          斷點(diǎn)續(xù)傳測(cè)試,下載一部分之后關(guān)閉程序再次啟動(dòng)。

          多線程下載測(cè)試

          完整代碼已經(jīng)上傳到 github.com/niumoo/down-bit.

          參考:

          [1] HTTP headers

          [2] Class RandomAccessFile

          [3] RandomAccessFile簡(jiǎn)介與使用

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

          [5] 維基百科 - 和性增長(zhǎng)/乘性降低


          最后的話

          文章有幫助可以點(diǎn)個(gè)「在看」或分享,都是支持,我都喜歡!
          文章每周持續(xù)更新,要實(shí)時(shí)關(guān)注我更新的文章以及分享的干貨,可以關(guān)注「?未讀代碼?」公眾號(hào)。

          ---- END ----

          "未讀代碼,一線技術(shù)工具人的學(xué)習(xí)、生活與見(jiàn)聞"

          一個(gè)「在看」,一段時(shí)光?


          瀏覽 119
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  一区二区三区三级18岁看的 | 亚洲欧美另类在线视频 | 操熟女网站 | 一本色道久久综合无码人妻四虎 | 色五月婷婷五月 |