多线程断点下载
来源:互联网 发布:顾比均线指标源码 编辑:程序博客网 时间:2024/06/07 18:07
多线程断点续传可以应用在上传和下载两个方面,好的上传或者下载案例,都是要求能够 “多线程 + 断点续传 + 进度条显示”。本篇是下载篇,主要是面向服务端程序员阐述原理。
做服务端开发或者web开发的同学,相比于安卓、IOS开发的同学,对于这个概念会比较陌生,因为web只能使用浏览器上传和下载,可编程性不高,客户端具体的上传和下载过程是个黑盒,比如下载过程到底是不是多线程,是不是支持断点续传,程序员都不用怎么关心。而安卓或者IOS的开发,客户端上传下载都是自己去面向服务端接口,自己读取本地IO流输出到服务端(上传),自己读取服务端IO流存储到本地(下载),比如关于下载的多线程、断点续传、进度条显示就需要程序员自己去考虑。
概述
1.缩短整个下载时间;
2.可以任意暂停开始,并且网络不稳定断开或者服务端超时断开,用户下一次不用重头开始下载;
3.下载进度显示。
问题1,可以从很多方面来考虑,除了提升网络带宽,一个有效的方案就是客户端多线程了,试想这样一个场景:客户端下载某一个服务端的1000MB的资源文件,这个服务端对外流量的输出有速率限制,限速策略是针对每个请求设置最大速率128KB/S。这时客户端的多线程就特别有效,如果客户端带宽充分,n个线程分段请求的速率就是n*128kb/s。
问题2,第一次客户端下载了500MB的临时文件,第二次请求500-999即可,这就是所谓的断点续传;
问题3,就更简单了,用当前下载大小/文件总大小就得到了进度。
如上针对问题1、2的方案就是多线程断点下载
http范围请求
多线程断点下载的核心原理就是依赖http协议的范围请求,从上述的方案分析,可以明显看出无论是多线程还是断点续传,都要求能够通过http协议访问某一段资源,这种范围请求是依靠一系列相对陌生的请求、响应消息头支持的。
1.Range:范围请求主要是通过Range消息头指定请求资源的字节范围,比如,上图表示请求[5000, 9999]字节。如果服务端不支持Range,则返回200 OK和全部资源;如果支持Range,则返回206 Partial Content和区间范围[5000, 9999]的资源,以及Accept-Range、Content-Range、Content-Length字段
2.Accept-Ranges:用来告诉客户端是否能处理范围请求,可指定的字段值有两种,可处理范围请求时指定其为bytes,反之指定其为none
3.Content-Range:5000-9999表示返回的实体资源的字节区间,10000表示资源的总大小。
4.Content-Length:实际返回的片段的大小
常见的Range格式还有:
Range: bytes=0-499 下载第0-499字节范围的内容
Range: bytes=500-999 下载第500-999字节范围的内容
Range: bytes=-500 下载最后500字节的内容
Range: bytes=500- 下载从第500字节开始到文件结束部分的内容
服务端
多线程断点续传的核心就是服务端的下载链接支持范围请求,那么我们平时给出的下载链接支不支持范围请求呢,我们又该如何测试呢?
回忆一下,我们平时对外提供下载链接主要有三种方案:
1.资源上传到第三方云存储,第三方云存储会返回资源的下载链接
2.资源上传至tomcat的静态资源目录,直接contextpath+资源相对路径,给出下载链接,即利用tomcat的DefaultServlet
3.自定义servlet读取对应的文件,输出给调用的客户端
测试服务端是否支持范围请求,我们可以使用360浏览器下载管理器、迅雷下载和curl程序:
360浏览器的下载使用的是迅雷下载技术,它就是一个典型的值得模仿的多线程断点续传下载客户端,原理大致是
1.发送一个非范围请求获取文件信息,包括文件名和文件大小。文件名称通过Content-Disposition的filename,没有Content-Disposition就通过下载链接,文件大小通过Content-Length获得。
2.尝试多线程,判断服务器端是否支持范围请求
3.不支持范围请求,就使用单线程下载,此时因为不支持范围请求,暂停继续功能是假的,后续测试案例会提到。
4.如果支持范围请求,使用多线程下载
curl可以指定Range消息头,curl --header "Range: bytes=0-20000" xxx.com/xxx.mp4,或者通过使用-C选项可对文件使用断点续传功能
经过测试,一般的云存储和DefaultServlet是支持范围请求的,下面主要分析自定义Servlet
public class DownloadServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { /** * 打印出请求头 */ Enumeration<String> headerNames = req.getHeaderNames(); while (headerNames.hasMoreElements()) { String headerName = headerNames.nextElement(); System.out.println(headerName + ": " + req.getHeader(headerName)); } System.out.println("-------------------------------------------"); File file = new File("D:/cache/1.mp4"); InputStream in = new FileInputStream(file); OutputStream out = resp.getOutputStream(); resp.setHeader("Content-Disposition", "attachment;filename=123.mp4"); resp.setContentLength((int) file.length()); byte[] buffer = new byte[1024]; int len; while ((len = in.read(buffer)) != -1) { out.write(buffer, 0, len); } in.close(); out.close(); }}
360 case 1:注释掉resp.setContentLength((int) file.length());即不返回Content-Length
360 case 2:返回Content-Length
360 case 3:暂停再继续
curl case 1:curl --header "Range: bytes=0-200" -O http://localhost/download.do
下载的文件大小会超过201字节,curl没有判断服务端是否支持Ranges,照单全收
-O:服务端的消息体保存成文件
curl case 2:curl -C - -O http://localhost/download.do
执行一段时间后,中断,再次执行,第二次执行会报错
-C -:断点续传
两次请求对应的消息头
我们再来测试一个支持范围请求的链接,这里选择DefaultServlet,主要是得到返回的消息头
curl -C - -O -v http://localhost/1.mp4
-v:显示详细的通信过程
这里给出第二次请求,即范围请求的头信息:
经过上面的测试case,我们得出结论:
DefaultServlet是支持范围请求的,通过查看tomcat/catalina.jar/org.apache.catalina.servlets.DefaultServlet源代码是能够看到对于Range的处理的
我们的自定义Servlet要想兼容各个客户端,获取好的下载体验,需要:
1.能够返回Content-Length
2.如果请求没有带有Range头,需要返回200和全部的资源
3.如果请求带有Range头,需要遵循范围请求相关的约定
改进后的代码:
public class StandardDownLoadServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { /** * 打印出请求头 */ Enumeration<String> headerNames = req.getHeaderNames(); while (headerNames.hasMoreElements()) { String headerName = headerNames.nextElement(); System.out.println(headerName + ": " + req.getHeader(headerName)); } System.out.println("-------------------------------------------"); resp.setHeader("Content-Disposition", "attachment;filename=123.mp4"); resp.setHeader("Accept-Ranges", "bytes"); File file = new File("D:/cache/1.mp4"); int fileLength = (int) file.length(); String rangeHeader = req.getHeader("Range"); /** * 解析出Range信息封装到Range对象,根据请求有没有Range信息做不同的响应处理 */ Range range = this.parseRange(rangeHeader, fileLength); if (range != null) { resp.setHeader("Content-Range", "bytes " + range.start + "-" + range.end + "/" + range.length); resp.setContentLength(range.end - range.start + 1); resp.setStatus(206); } else { resp.setContentLength(fileLength); resp.setStatus(200); range = new Range(); range.start = 0; range.end = fileLength - 1; } InputStream is = new FileInputStream(file); OutputStream out = resp.getOutputStream(); is.skip(range.start); // 分段读取文件的关键API byte[] buffer = new byte[1024]; int length; int total = range.end - range.start + 1; while(total > 0 && (length = is.read(buffer)) != -1) { if (total >= length) { out.write(buffer, 0, length); total -= length; } else { out.write(buffer, 0, total); total = 0; } } is.close(); out.close(); } private Range parseRange(String rangeHeader, int fileLength) { /** * Range: bytes=0-499 下载第0-499字节范围的内容 * Range: bytes=500-999 下载第500-999字节范围的内容 * Range: bytes=-500 下载最后500字节的内容 * Range: bytes=500- 下载从第500字节开始到文件结束部分的内容 */ if (rangeHeader == null) return null; Range range = new Range(); String rangeStr = rangeHeader.substring(6); String startStr = rangeStr.split("-")[0]; String endStr = rangeStr.endsWith("-")?"":rangeStr.split("-")[1]; if (startStr != "") { range.start = Integer.parseInt(startStr); if (endStr != "") { range.end = Integer.parseInt(endStr); } else { range.end = fileLength - 1; } } else { range.end = fileLength - 1; range.start = range.end + Integer.parseInt(rangeStr); } range.length = fileLength; return range; } private static class Range { public int start; public int end; public int length; }}我们继续使用curl -C - -O -v http://localhost/download.do测试,发现第二次不报错了,也就是说支持范围请求,支持curl的断点续传了,这里给出第二次请求的相关头信息
客户端
多线程断点下载的客户端根据上面的原理分析,就很容易理解了,当然要想写出健全的代码还是很不容易的,要考虑的因素很多,对于支持范围请求与不支持范围请求的服务端都要能够兼容,像360浏览器的下载工具就做到了兼容,我这里就不给出完整的客户端代码了,我也没有认真写过健硕的客户端下载的代码,这里仅仅就关键API做一下介绍。
1.如果仅仅实现像curl的单线程的断点续传,那么要求我们能够对文件进行追加,可以通过public FileOutputStream(File file, boolean append),true表示在file的末尾追加内容。
2.如果要实现多线程的断点续传,我们让每个线程下载部分文件片断,之后要能够把这些片段合并,可以参考RandomAccessFile相关API,这个类能够满足多线程断点续传的需求,使用这个类的思路不是合并,而是一上来创建一个和服务端完整文件同样大小的随机读写的文件,然后每个线程向这个文件的特定片段填充从服务端下载到的内容。
- 多线程下载断点下载
- 多线程下载,断点下载
- java 多线程断点下载
- 网络多线程断点下载
- 网络多线程断点下载
- 网络多线程断点下载
- android多线程断点下载
- 多线程断点下载原理
- 多线程断点下载文件
- java 多线程断点下载
- android 多线程断点下载
- android多线程断点下载
- 多线程断点下载器
- Java---多线程断点下载
- Java--多线程断点下载
- 断点多线程下载
- android多线程断点下载
- android,多线程断点下载
- java常用类及接口
- 判断一个数是否为完全平方数
- 【笔记】仿支付宝密码输入框
- Linux中线程与CPU核的绑定
- 实例说明<<PostMessage和SendMessage函数的区别>>
- 多线程断点下载
- 从零开始学python网络爬虫
- UVa1583Digit Generator 以及一些自我看法
- VMware 9.0+redhat 6.5(rhel-server-6.5-x86_x64)+oracle11gr2 rac 各种坑
- 【UVA】 UVA 1589
- c语言实现n的阶乘
- 安装ubuntu-desktop16以及firefox使用flash问题
- 快乐树莓派视频监控系列(一)motion监控方案
- java创建线程的三种方式以及比较