java http Range分段下载

1.原理

1).HTTP 请求头 Range

Range: bytes=start-end
Range: bytes=10- :第10个字节及最后个字节的数据
Range: bytes=40-100 :第40个字节到第100个字节之间的数据.

示例:
#表示从第0个字节开始下载
conn.addRequestProperty(“range”, “bytes=” + 0 + “-“);

注意:若支持range分段下载,服务端需返回206状态码。
nginx默认支持range分段下载。
tomcat默认也支持range分段下载

示例响应头:

{
“1”: [“HTTP/1.1 206 Partial Content”],
“ETag”: [“W/\”174093750-1525092037000\”“],
“Date”: [“Mon, 30 Apr 2018 12:42:06 GMT”],
“Content-Length”: [“174093750”],
“Last-Modified”: [“Mon, 30 Apr 2018 12:40:37 GMT”],
“Set-Cookie”: [“JSESSIONID=8A5AF04A71028DD2F8742CACA8830995; Path=/; HttpOnly”],
“Accept-Ranges”: [“bytes”],
“Server”: [“Apache-Coyote/1.1”],
“Content-Range”: [“bytes 0-174093749/174093750”]
}

2).响应头

Content-Range

Content-Range: bytes 0-10/3103
这个表示,服务器响应了前(0-10)个字节的数据,该资源一共有(3103)个字节大小。

Content-Type

Content-Type: image/png
表示这个资源的类型

Content-Length

Content-Length: 11
表示这次服务器响应了11个字节的数据(0-10)

Last-Modified

Last-Modified: Tue, 30 Jun 2015 03:12:48 GMT
表示资源最近修改的时间(分段下载时要注意这个东西,因为如果修改了,分段下载可能就要重新下载了)

ETag
ETag: W/"3103-1435633968000"
这个响应头表示资源版本的标识符,通常是消息摘要(类似MD5一样)(分段下载时要注意这个东西,或者缓存控制也要注意这个东西)

注意,每种服务器对生成ETag的算法不同,这个要特别注意 如果使用分布式缓存,要特别要保证每台服务器生成的ETag算法是一致的.

缓存的过期,要同时结合(ETag + Last-Modified)这两个响应头来判断.
强ETag
只要实体发生任何改变,都会改变ETag值.如:
ETag: "1234234234"
弱ETag
它在前面会有个 W/ ,如:
ETag: W/"12342423"

注意:根据HTTP规范,HTTP的消息头部的字段名,是不区分大小写的.

2.服务器端代码

/**
 * 下载
 * @param request
 * @param response
 * @param model
 * @return
 * @throws IOException
 */
@RequestMapping(value = "/api/update", method = RequestMethod.GET)
public ResponseEntity<String> update(HttpServletRequest request, HttpServletResponse response, ModelMap model)
        throws IOException {

    InputStream inputStream = null;
    ServletOutputStream out = null;
    String odexName = "file.apk";
    try {
        File file = new File("/data/apk/" + odexName);
        int fSize = Integer.parseInt(String.valueOf(file.length()));
        response.setCharacterEncoding("utf-8");
        response.setContentType("application/x-download");
        response.setHeader("Accept-Ranges", "bytes");
        response.setHeader("Content-Length", String.valueOf(fSize));
        response.setHeader("Content-Disposition", "attachment;fileName=" + odexName);
        inputStream = new FileInputStream("/data/apk/" + odexName);
        long pos = 0;
        if (null != request.getHeader("Range")) {
            // 断点续传
            response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
            try {
                pos = Long.parseLong(request.getHeader("Range").replaceAll("bytes=", "").replaceAll("-", ""));
            } catch (NumberFormatException e) {
                pos = 0;
            }
        }
        out = response.getOutputStream();
        response.setHeader("Content-Range", new StringBuffer("bytes ").append(pos + "").append("-")
                .append((fSize - 1) + "").append("/").append(fSize + "").toString());
        inputStream.skip(pos);
        byte[] buffer = new byte[1024 * 10];
        int length = 0;
        while ((length = inputStream.read(buffer, 0, buffer.length)) != -1) {
            out.write(buffer, 0, length);
            Thread.sleep(100);
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        try {
            if (null != out)
                out.flush();
            if (null != out)
                out.close();
            if (null != inputStream)
                inputStream.close();
        } catch (IOException e) {
        }
    }
    return new ResponseEntity(null, HttpStatus.OK);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59

3.客户端代码

/**
 * 断点下载
 * 
 * @throws InterruptedException
 */
@Test
public void testDebugDownload() throws IOException, InterruptedException {
    // 创建URL对象
    URL url = new URL("http://localhost:8080/file.apk");
    // 使用url获取HttpURLConnection对象
    HttpURLConnection conn = (HttpURLConnection) url.openConnection();
    // 客户端的请求方式
    conn.setRequestMethod("GET");
    // 已经下载的字节数
    long alreadySize = 0;
    // 将文件写到download/file.apk中
    File file = new File("/download/file.apk");
    // 如果存在,说明原来下载过,不过可能没有下载完
    if (file.exists()) {
        // 如果文件存在,就获取当前文件的大小
        alreadySize = file.length();
    }
    /**
     * Range头域可以请求实体的一个或者多个子范围。 例如: 表示头500个字节:bytes=0-499
     * 表示第二个500字节:bytes=500-999 表示最后500个字节:bytes=-500
     * 表示500字节以后的范围:bytes=500- 第一个和最后一个字节:bytes=0-0,-1
     * 同时指定几个范围:bytes=500-600,601-999
     * 但是服务器可以忽略此请求头,如果无条件GET包含Range请求头,响应会以状态码206(PartialContent)返回而不是以200
     * (OK)。
     */
    conn.addRequestProperty("range", "bytes=" + alreadySize + "-");
    conn.connect();

    // 206,一般表示断点续传
    // 获取服务器回馈的状态码
    int code = conn.getResponseCode();
    // 如果响应成功,因为使用了range请求头,那么响应成功的状态码为206,而不是200
    if (code == 206) {
        // 获取未下载的文件的大小
        // 本方法用来获取响应正文的大小,但因为设置了range请求头,那么这个方法返回的就是剩余的大小
        long unfinishedSize = conn.getContentLength();
        // 文件的大小
        long size = alreadySize + unfinishedSize;

        // 获取输入流
        InputStream in = conn.getInputStream();
        // 获取输出对象,参数一:目标文件,参数2表示在原来的文件中追加
        OutputStream out = new BufferedOutputStream(new FileOutputStream(file, true));

        // 开始下载
        byte[] buff = new byte[2048];
        int len;
        StringBuilder sb = new StringBuilder();
        while ((len = in.read(buff)) != -1) {
            out.write(buff, 0, len);
            // 将下载的累加到alreadSize中
            alreadySize += len;
            // 下载进度
            System.out.printf("%.2f%%\n", alreadySize * 1.0 / size * 100);
            // 由于文件大小可以看得到,那么我们这里使用阻塞
            //Thread.sleep(2);
            //break;
        }
        out.close();
        System.out.println("下载完成!!!");
    } else {
        System.out.println("下载失败!!!");
    }

    // 断开连接
    conn.disconnect();
}
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值