Java服务器端支持断点续传编码实战
来源:互联网 发布:游奇网络礼包 编辑:程序博客网 时间:2024/05/17 09:26
需求: 公司网站上提供一个程序文件供下载, 但不支持断点续传, 解决这个问题
环境: Play Framework
待下载文件大小: 4.5M, 随着版本升级, 会缓慢增加.
升级场景: 软件可能会要求强制升级, 所以下载并发某个时刻会大一些
了解到以上信息, 大脑灵光闪现, 立刻百度...
1. 先了解下载是怎么回事
(1). 用户下载时, 会发送http请求
GET /down.zip HTTP/1.1 Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/vnd.ms- excel, application/msword, application/vnd.ms-powerpoint, */* Accept-Language: zh-cn Accept-Encoding: gzip, deflate User-Agent: Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0) Connection: Keep-Alive
(2). 服务器回复:
200 Content-Length=106786028 Accept-Ranges=bytes Date=Mon, 30 Apr 2001 12:56:11 GMT ETag=W/"02ca57e173c11:95b" Content-Type=application/octet-stream Server=Microsoft-IIS/5.0 Last-Modified=Mon, 30 Apr 2001 12:56:11 GMT
Accept-Ranges=bytes : 回复这条头信息, 意味着服务器支持断点续传
Content-Length=106786028 : 只有回复了这条头信息, 客户端才好断点续传
(3). 客户端发起分块请求
从某处开始下载
GET /down.zip HTTP/1.0 User-Agent: Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)RANGE: bytes=2000070- Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2 3.
RANGE: bytes=2000070- : 客户端告诉服务器文件从2000070字节开始传下来
下载中间的一段
206 Content-Length=106786028 Content-Range=bytes 2000070-106786027/106786028 Date=Mon, 30 Apr 2001 12:55:20 GMT ETag=W/"02ca57e173c11:95b" Content-Type=application/octet-stream Server=Microsoft-IIS/5.0 Last-Modified=Mon, 30 Apr 2001 12:55:20 GMTContent-Range=bytes 2000070-106786027/106786028 : 客户端告诉服务器文件从2000070字节开始, 到106786027截止传下来
返回的代码也改为206了,而不再是200了
2. 了解相关技术
疯狂百度,...
(1) RandomAccessFile
(2) MappedByteBuffer
各种编码测试:
重写play.mvc.results.RenderBinary 类
public static void android() { //... throw new MyRenderBinary(f, saveName); //...}主要是重写里面一个apply方法:
@Override public void apply(Http.Request request, Http.Response response) { System.out.println("-----------apply"); for (String s : request.headers.keySet()) { Logger.info("---header : " + s + " = " + request.headers.get(s)); } try { if (name != null) { setContentTypeIfNotSet(response, MimeTypes.getContentType(name)); } if (contentType != null) { response.contentType = contentType; } String dispositionType; if (inline) { dispositionType = INLINE_DISPOSITION_TYPE; } else { dispositionType = ATTACHMENT_DISPOSITION_TYPE; } if (!response.headers.containsKey("Content-Disposition")) { if (name == null) { response.setHeader("Content-Disposition", dispositionType); } else { if (canAsciiEncode(name)) { String contentDisposition = "%s; filename=\"%s\""; response.setHeader("Content-Disposition", String.format(contentDisposition, dispositionType, name)); } else { final String encoding = getEncoding(); String contentDisposition = "%1$s; filename*=" + encoding + "''%2$s; filename=\"%2$s\""; response.setHeader("Content-Disposition", String.format(contentDisposition, dispositionType, encoder.encode(name, encoding))); } } } if (file != null) { if (!file.exists()) { throw new UnexpectedException("Your file does not exists (" + file + ")"); } if (!file.canRead()) { throw new UnexpectedException("Can't read your file (" + file + ")"); } if (!file.isFile()) { throw new UnexpectedException("Your file is not a real file (" + file + ")"); } //解析下载范围 long fileLength = file.length();//记录文件大小 long pastLength = 0;//记录已下载文件大小 int rangeSwitch = 0;//0:从头开始的全文下载;1:从某字节开始的下载(bytes=27000-);2:从某字节开始到某字节结束的下载(bytes=27000-39000) long toLength = 0;//记录客户端需要下载的字节段的最后一个字节偏移量(比如bytes=27000-39000,则这个值是为39000) long contentLength = 0;//客户端请求的字节总量 String rangeBytes = "";//记录客户端传来的形如“bytes=27000-”或者“bytes=27000-39000”的内容 RandomAccessFile raf = null;//负责读取数据 OutputStream os = null;//写出数据 OutputStream out = null;//缓冲 FileChannel fc = null; byte b[] = new byte[1024 * 5];//暂存容器 if (request.headers.containsKey("range")) {// 客户端请求的下载的文件块的开始字节 String range = request.headers.get("range").value(); response.status = javax.servlet.http.HttpServletResponse.SC_PARTIAL_CONTENT; Logger.info("request.getHeader(\"range\")=" + range); rangeBytes = range.replaceAll("bytes=", ""); if (rangeBytes.indexOf('-') == rangeBytes.length() - 1) {//bytes=969998336- rangeSwitch = 1; rangeBytes = rangeBytes.substring(0, rangeBytes.indexOf('-')); pastLength = Long.parseLong(rangeBytes.trim()); contentLength = fileLength - pastLength + 1;//客户端请求的是 969998336 之后的字节 } else {//bytes=1275856879-1275877358 rangeSwitch = 2; String temp0 = rangeBytes.substring(0, rangeBytes.indexOf('-')); String temp2 = rangeBytes.substring(rangeBytes.indexOf('-') + 1, rangeBytes.length()); pastLength = Long.parseLong(temp0.trim());//bytes=1275856879-1275877358,从第 1275856879 个字节开始下载 toLength = Long.parseLong(temp2);//bytes=1275856879-1275877358,到第 1275877358 个字节结束 contentLength = toLength - pastLength + 1;//客户端请求的是 1275856879-1275877358 之间的字节 } } else {//从开始进行下载 contentLength = fileLength;//客户端要求全文下载 } response.setHeader("Accept-ranges", "bytes");//如果是第一次下,还没有断点续传,状态是默认的 200,无需显式设置;响应的格式是:HTTP/1.1 200 OK //0:从头开始的全文下载 if (rangeSwitch == 0) { Logger.info("----------------------------从头开始的全文下载..."); //response.direct = file; raf = new RandomAccessFile(file, "r"); int count = 0; while ((count = raf.read(b)) > 0) { response.out.write(b, 0, count); } } else { /** * 如果设设置了Content-Length,则客户端会自动进行多线程下载。如果不希望支持多线程,则不要设置这个参数。 * 响应的格式是: Content-Length: [文件的总大小] - [客户端请求的下载的文件块的开始字节] * ServletActionContext.getResponse().setHeader("Content-Length", * new Long(file.length() - p).toString()); */ //response.reset();//告诉客户端允许断点续传多线程连接下载,响应的格式是:Accept-ranges: bytes //响应的格式是: //Content-range: bytes [文件块的开始字节]-[文件的总大小 - 1]/[文件的总大小] switch (rangeSwitch) { case 1: {//针对 bytes=27000- 的请求 String contentrange = new StringBuffer("bytes ").append(new Long(pastLength).toString()).append("-").append(new Long(fileLength - 1).toString()).append("/").append(new Long(fileLength).toString()).toString(); response.setHeader("Content-range", contentrange); break; } case 2: {//针对 bytes=27000-39000 的请求 String contentrange = rangeBytes + "/" + new Long(fileLength).toString(); response.setHeader("Content-range", contentrange); break; } default: { break; } } //开始下载 os = response.out; raf = new RandomAccessFile(file, "rw"); try { switch (rangeSwitch) { case 1: {//针对 bytes=27000- 的请求 raf.seek(pastLength);//形如 bytes=969998336- 的客户端请求,跳过 969998336 个字节 int n = 0; while ((n = raf.read(b)) != -1) { response.out.write(b, 0, n); } break; } case 2: {//针对 bytes=27000-39000 的请求 raf.seek(pastLength - 1);//形如 bytes=1275856879-1275877358 的客户端请求,找到第 1275856879 个字节 int n = 0; long readLength = 0;//记录已读字节数 while (readLength <= contentLength - 1024) {//大部分字节在这里读取 n = raf.read(b, 0, 128); readLength += 1024; response.out.write(b, 0, n); } if (readLength <= contentLength) {//余下的不足 1024 个字节在这里读取 n = raf.read(b, 0, (int) (contentLength - readLength)); response.out.write(b, 0, n); } break; } default: { break; } } out.flush(); } catch (IOException ie) { ie.printStackTrace(); /** * 在写数据的时候, 对于 ClientAbortException 之类的异常, * 是因为客户端取消了下载,而服务器端继续向浏览器写入数据时, 抛出这个异常,这个是正常的。 * 尤其是对于迅雷这种吸血的客户端软件, 明明已经有一个线程在读取 * bytes=1275856879-1275877358, * 如果短时间内没有读取完毕,迅雷会再启第二个、第三个。。。线程来读取相同的字节段, * 直到有一个线程读取完毕,迅雷会 KILL 掉其他正在下载同一字节段的线程, 强行中止字节读出,造成服务器抛 * ClientAbortException。 所以,我们忽略这种异常 */ //ignore } finally { if (out != null) { try { out.close(); } catch (IOException e) { Logger.error(e.getMessage(), e); } } if (raf != null) { try { raf.close(); } catch (IOException e) { Logger.error(e.getMessage(), e); } } if (fc != null) { try { fc.close(); } catch (IOException e) { Logger.error(e.getMessage(), e); } } } } } } catch (Exception e) { //throw new UnexpectedException(e); e.printStackTrace(); }
结果他大爷的, 报OutOfMemery错误
原因在此:
while ((count = raf.read(b)) > 0) { response.out.write(b, 0, count);}
play 里面的这个response 是包装完成后再写入客户端channel, 下载的文件一大点就报OutOfMemery错误
只好加大JVM运行内存空间, 但是指标不治本哪, 并发一大还是死
继续百度....
...
一天过去了
...
3. 跟同事聊聊
同事说现在的http服务器都支持断点续传哪, 直接url转发到一个静态下载地址就好啦! 尼玛....
问题就这样解决了
总结: 聊聊的威力非常大
参考文章:
JAVA读写大容量数据:
http://hi.baidu.com/victorlin23/item/c98293eca95711a9c10d759a
http://blog.csdn.net/sjiang2142/article/details/8125360
[慎用 MappedByteBuffer!] http://www.iteye.com/topic/298153
- Java服务器端支持断点续传编码实战
- 使java服务器端支持断点续传
- Java服务器端支持断点续传代码
- Java 服务器端支持断点续传的源代码【支持快车、迅雷】
- Java 服务器端支持断点续传的源代码【支持快车、迅雷】
- Java 服务器端支持断点续传的源代码【支持快车、迅雷】
- Java 服务器端支持断点续传的源代码【支持快车、迅雷】
- Java 服务器端支持断点续传的源代码【支持快车、迅雷】
- Java 服务器端支持断点续传的源代码【支持快车、迅雷】
- Java实现服务器端动态流断点续传下载支持
- Java Spring Boot 服务器端断点续传功能支持 实现代码
- Java 服务器端支持断点续传的源代码【支持快车、迅雷】(仅支持 HTTP 协议)
- java 文件上传支持断点续传
- Java 下载支持断点续传服务端
- java下载工具类,支持断点续传
- java多线程分块上传并支持断点续传
- java多线程下载网络资源(支持断点续传)
- java服务器端配置支持跨域请求
- window.returnValue使用方法
- dos调试工具之debug
- opencv canny边缘检测
- mx平台显示闪红边框
- ANSI-C
- Java服务器端支持断点续传编码实战
- 如何聚集高水平研发人员
- 解决错误:ASP.NET Error: Failed to access IIS metabase
- mx临时修改遥控器码值
- PHP中常用设置
- Jenkins邮件设置
- 苹果与Unicode协会合作推进emoji多样化
- Makefile 的函数-1
- PgSql函数——赋值问题