Play Framework 服务器端支持断点续传记录

本文探讨了在PlayFramework环境下实现文件断点续传的方法。通过解析HTTP请求与响应头,介绍了如何利用RandomAccessFile类处理不同类型的下载请求,并讨论了在实际应用中遇到的内存溢出问题及其解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

需求: 公司网站提供程序文件下载,  需要支持断点续传, 

 

环境: 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

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值