文章曾经收录在 Github.com/niumoo/JavaNotes,更有 Java 程序员所须要把握的外围常识,欢送 Star 和指教。
欢送关注我的公众号,文章每周更新。
感激看客老爷点进来了,周末闲来无事,想起 共事强哥 的那句话:“你有没有玩过 断点续传 ?”过后转念一想, 断点续传 下载用的的确不少,具体细节嘛,真的没有去思考过啊。这不,思考过后有了这篇文章。感激强哥,让我有了一篇能够水的文章,上面会用纯 Java 无依赖实现一个简略的 多线程断点续传下载器。
这篇水文章到底有什么内容呢?先简略列举一下,顺便思考几个问题。
- 断点续传的原理。
- 重启续传文件时,怎么保障文件的一致性?
- 同一个文件多线程下载如何实现?
- 网速带宽固定,为什么多线程下载能够提速?
多线程断点续传会用到哪些常识呢?下面曾经抛出了几个问题,不放思考一下。上面会针对下面的四个问题一一进行解释,当初大多数的服务都能够在线提供,下载应用的场景越来越少,不过这不障碍咱们对原理的探究。
断点续传的原理
想要理解断点续传是如何实现的,那么必定是要理解一下 HTTP 协定了。HTTP 协定是互联网上利用最宽泛网络传输协定之一,它基于 TCP/IP 通信协议来传递数据。所以断点续传的神秘也就暗藏在这 HTTP 协定中了。
咱们都晓得 HTTP 申请会有一个 Request header 和 Response header,就在这申请头和响应头里,有一个和 Range 相干的参数。上面通过百度网盘的 pc 客户端下载链接进行测试。
应用 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 值就能够了。
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);
}
断点续传测试,下载一部分之后关闭程序再次启动。
残缺代码曾经上传到 github.com/niumoo/down-bit.
参考:
[1] HTTP headers
[2] Class RandomAccessFile
[3] RandomAccessFile 简介与应用
[4] 维基百科 – TCP 拥塞管制)
[5] 维基百科 – 和性增长 / 乘性升高)
最初的话
文章曾经收录在 Github.com/niumoo/JavaNotes,欢送 Star 和指教。更有一线大厂面试点,Java 程序员须要把握的外围常识等文章,也整顿了很多我的文字,欢送 Star 和欠缺,心愿咱们一起变得优良。
文章有帮忙能够点个「赞 」或「 分享 」,都是反对,我都喜爱!
文章每周继续更新,要实时关注我更新的文章以及分享的干货,能够关注「未读代码」公众号或者我的博客。