需求: 公司网站提供程序文件下载, 需要支持断点续传,
环境: 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 GMT
Content-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.youkuaiyun.com/sjiang2142/article/details/8125360
[慎用 MappedByteBuffer] http://www.iteye.com/topic/298153