java大文件断点续传下载分段下载nginx206问题

关于大文件传输,之前写过两篇大文件上传的:
java利用websocket实现分段上传大文件并显示进度信息
java大文件传输方案总结以及切分与合并代码记录

这次再记一篇大文件分段下载的。

普通的java下载实现方式一般是如下代码:

public static Boolean downloadNet(String urlPath, String filePath) throws Exception {
        Boolean flag = true;
        int byteread = 0;

        URL url;
        try {
            url = new URL(urlPath);
        } catch (MalformedURLException e1) {
            flag = false;
            throw e1;
        }
        InputStream inStream = null;
        FileOutputStream fs = null;
        try {
            URLConnection conn = url.openConnection();
            inStream = conn.getInputStream();
            fs = new FileOutputStream(filePath);

            byte[] buffer = new byte[1024];
            while ((byteread = inStream.read(buffer)) != -1) {
                fs.write(buffer, 0, byteread);
            }
        } catch (Exception e) {
            flag = false;
            throw e;
        } finally {
            fs.close();
            inStream.close();
        }
        return flag;
    }

此种方式下载小文件是没有问题的 几百M的文件也没有问题,但是,这种方式是先把数据下载到内存里,当下载大于1.2g的文件时,就遇到下载不全的问题,这时候就属于大文件下载的范畴,可以利用http的断点续传分段下载。

原理

HTTP 请求头 设置 Range
Range: bytes=start-end
Range: bytes=10- :第10个字节及最后个字节的数据
Range: bytes=40-100 :第40个字节到第100个字节之间的数据。

当不设置end的时候,会根据服务器端最大的传输大小自动分段。
只要设置了Range头,服务器会自动返回206而不是200,206是分段下载的标志,一般的下载服务器比如nginx,tomcat都是支持断点续传分段下载的。

示例响应头:

{
“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”]
}

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

下面是client端的下载demo:

新建DownloadClient.java


package ly.mp.project.common.otautils;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;

import com.google.common.base.Objects;

import ly.mp.project.common.ota.HashAlgorithm;
import ly.mp.project.common.util.LogUtils;

public class DownloadClient {

    /**
     * 断点下载
     * @param num 
     * 
     * @throws InterruptedException
     */
    private static int execDownload(int num, String fileUrl, String targetPath) throws Exception {
//      String fileUrl = "https://xx.com/dfv-fota-server/dp/202307/e633af4ef30c4e8eb233f2fc013515aa/dp_ce6a91dc9908d6308d3c57cd40e29b929ffd4d2a4a6cda6ffe1453e50611451d_1689419655424_AES.iso";
//      String fileUrl = "http://127.0.0.1:8088/test/downloadTest/cn_windows_10_consumer_editions_version_20h2_x64_dvd_d4f7a83e.iso/download.do";
        // 创建URL对象
        URL url = new URL(fileUrl);
        // 使用url获取HttpURLConnection对象
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setConnectTimeout(6000);
        conn.setReadTimeout(1000*60);
        // 客户端的请求方式
        conn.setRequestMethod("GET");
        // 已经下载的字节数
        long alreadySize = 0;
        // 将文件写到download/file.apk中
        File file = new File(targetPath);
        // 如果存在,说明原来下载过,不过可能没有下载完
        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.getContentLengthLong();
            long totalSize = alreadySize + unfinishedSize;
            LogUtils.info("totalSize:{}", totalSize);

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

            // 开始下载
            byte[] buff = new byte[1024*1024*2];
            int len;
            while ((len = in.read(buff)) != -1) {
                out.write(buff, 0, len);
                // 将下载的累加到alreadSize中
                alreadySize += len;
                // 下载进度
                int process =  (int)(alreadySize * 1.0 / totalSize * 100);
                int lastProcess = (int)((alreadySize-len) * 1.0 / totalSize * 100);
                if(process % 10 == 0 && lastProcess % 10 != 0) {
                    LogUtils.info("下载进度:" + process + " alreadySize:" + alreadySize + " totalSize:" + totalSize);
                }
            }
            out.close();
            LogUtils.info("第" + (num+1) + "次下载完成!!!");
            if(alreadySize != 0 && !Objects.equal(alreadySize, totalSize) && alreadySize < totalSize) {
                num = num + 1;
            } else if(alreadySize != 0 && Objects.equal(alreadySize, totalSize)) {
                num = 0;
            }
        } else {
            LogUtils.info("下载失败!!!");
            num = 0;
        }

        // 断开连接
        conn.disconnect();
        return num;
    }
    
    public static void main(String[] args) {
        String fileUrl = "https://xx.com/dfv-fota-server/dp/202307/4bd639bf2e164a82b8953215b17cab67/dp_ce6a91dc9908d6308d3c57cd40e29b929ffd4d2a4a6cda6ffe1453e50611451d_1689579274473_AES.iso";
        String targetPath = "D://tmp/dp_ce6a91dc9908d6308d3c57cd40e29b929ffd4d2a4a6cda6ffe1453e50611451d_1689579274473_AES.iso";
        try {
            DownloadClient.beginDowanload(0, fileUrl, targetPath);
        } catch (Exception e) {
            e.printStackTrace();
        }
        String hash = "";
        try {
            hash = HashUtils.calculateHash(targetPath, HashAlgorithm.SHA256);
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        System.out.println(hash);
    }

    public static void beginDowanload(int num, String fileUrl, String targetPath) throws Exception {
            num = DownloadClient.execDownload(num, fileUrl, targetPath);
            if(num == 0) return;
            if(num > 0) DownloadClient.beginDowanload(num, fileUrl, targetPath);
    }
}

如上代码里递归下载文件,直至下载完成,一个2g多的文件,实际运行结果如下:

totalSize:2121672720
下载进度:10 alreadySize:212168409 totalSize:2121672720
下载进度:20 alreadySize:424335065 totalSize:2121672720
下载进度:30 alreadySize:636503769 totalSize:2121672720
下载进度:40 alreadySize:848670425 totalSize:2121672720
下载进度:50 alreadySize:1060837081 totalSize:2121672720
第1次下载完成!!!
totalSize:2121672720
下载进度:60 alreadySize:1273004054 totalSize:2121672720
下载进度:70 alreadySize:1485172758 totalSize:2121672720
下载进度:80 alreadySize:1697339414 totalSize:2121672720
下载进度:90 alreadySize:1909506070 totalSize:2121672720
下载进度:100 alreadySize:2121672720 totalSize:2121672720
第2次下载完成!!!

可以看到nginx服务器端自动把文件分成两次下载,第一次下载完成后,客户端要自己写程序递归触发第N次下载,直至下载完成即可。

注意以上代码里要用:

long unfinishedSize = conn.getContentLengthLong();

而不是:

long unfinishedSize = conn.getContentLength();

getContentLength()方法返回的是int 超2.5g文件大小就变成-1了 超过int的范围,所以用getContentLengthLong(),这个坑卡了我一天才发现是这个问题。

server端代码

server端一般不需要手动代码实现,毕竟nginx或tomcat都支持文件下载的,但java手写controller也是可以支持:

package com.zhaohy.app.test;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
public class DownloadTestServer {
    @RequestMapping(value = "/test/downloadTest/{odexName}/download.do", method = RequestMethod.GET)
    public void download(HttpServletRequest request, HttpServletResponse response, @PathVariable("odexName") String odexName)
            throws IOException {

        InputStream inputStream = null;
        ServletOutputStream out = null;
        try {
            File file = new File("D:/Download/" + odexName);
            long fSize = file.length();
            System.out.println(fSize);
            response.setCharacterEncoding("utf-8");
            response.setContentType("application/x-download");
            response.setHeader("Accept-Ranges", "bytes");
            response.setHeader("Content-Disposition", "attachment;fileName=" + odexName);
            inputStream = new FileInputStream("D:/Download/" + 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;
                }
            } else {
                response.setStatus(HttpServletResponse.SC_OK);
                
            }
            response.setHeader("Content-Range", new StringBuffer("bytes ").append(pos + "").append("-")
                    .append((fSize - 1) + "").append("/").append(fSize + "").toString());
            response.setHeader("Content-Length", (fSize - pos) + "");
            out = response.getOutputStream();
            
            inputStream.skip(pos);
            long bufferSize = 1024*1024*25;
            byte[] buffer = new byte[(int)bufferSize];
            int length = 0;
            while ((length = inputStream.read(buffer, 0, buffer.length)) != -1) {
                out.write(buffer, 0, length);
            }
        } 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) {
            }
        }
    }
}

参考:https://blog.csdn.net/hechaojie_com/article/details/81989951
https://blog.csdn.net/yy4545/article/details/107787463
https://www.cnblogs.com/nxlhero/p/11670942.html

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容