https://www.cnblogs.com/niumoo/p/13395041.html
最近闲来无事,想起同事的那句话:“你有没有玩过断点续传?” 当时转念一想,断点续传下载用的确实不少,详细细节嘛,真的没有去思考过啊。这不,思考过后有了这篇文章。感谢同事,让我有了一篇可以水的文章,下面会用纯 Java 无依赖实现一个大略的多线程断点续传下载器。
这篇水文章到底有什么内容呢?先大略列举一下,顺便思考几个问题。
断点续传的事理。重启续传文件时,怎么担保文件的同等性?同一个文件多线程下载如何实现?网速带宽固定,为什么多线程下载可以提速?
多线程断点续传会用到哪些知识呢?上面已经抛出了几个问题,不妨思考一下。下面会针对上面的四个问题逐一进行阐明,现在大多数的做事都可以在线供应,下载利用的场景越来越少,不过这不妨碍我们对事理的探求。
断点续传的事理想要理解断点续传是如何实现的,那么肯定是要理解一下 HTTP 协议了。HTTP 协议是互联网上运用最广泛网络传输协议之一,它基于 TCP/IP 通信协议来通报数据。以是断点续传的奥秘也就隐蔽在这 HTTP 协议中了。
我们都知道 HTTP 要求会有一个 Request header 和 Response header ,就在这要求头和相应头里,有一个和 Range 干系的参数。下面通过百度网盘的 pc 客户端下载链接进行测试。
利用 cURL 查看 response header.
$ 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 信息有很多,我们只须要重点关注几个。
Content-Length: 65804256 // 要求的文件的大小,单位 byteAccept-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 开始传输数据时,做事端会不断的探测可用带宽。在一个传输内容段被成功吸收后,会更加传输两倍段内容,如果再次被成功吸收,就连续更加,直到发生了丢包,这时这也被叫做慢启动。当达到慢启动阀值(ssthresh)时,慢启动算法就会转换为线性增长的阶段,每次只增加一个分段,放缓增加速率。我以为实在慢启动的更加增速过程并不慢,只是一种叫法。
但是当发生了丢包,也便是检测到拥塞时,发送方就会将发送段大小降落一个乘数,比如二分之一,慢启动阈值降为超时前拥塞窗口的一半大小、拥塞窗口会降为1个MSS,并且重新回到慢启动阶段。这时多线程的上风就表示出来了,由于你的多线程会让这个速率减速没有那么剧烈,毕竟这时可能有另一个线程正处在慢启动的在终极加速阶段,这样总体的下载速率就优于单线程了。
多线程断点续传代码实现基于上面的事理先容,心里该当有了详细的实现思路了。我们只须要利用多线程,结合 Content-Range 参数分段要求文件内容保存到临时文件,下载完毕后利用 RandomAccessFile 把下载的文件合并成一个文件即可。而在须要断点续传时,只须要读取一下当前临时文件大小,然后调度 Content-Range ,就可以进行续传下载。
代码不多,下面是部分核心代码,完全代码可以直接点开文章末了的 Github 仓库。
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();
获取文件的 ETag.
Map<String, List<String>> headerFields = httpConnection.getHeaderFields();List<String> eTagList = headerFields.get("ETag");System.out.println(eTagList.get(0));
利用 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);}
断点续传测试,下载一部分之后关闭程序再次启动。