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 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.csdn.net/sjiang2142/article/details/8125360

 [慎用 MappedByteBuffer!] http://www.iteye.com/topic/298153



0 0
原创粉丝点击