关于大文件传输,之前写过两篇大文件上传的:
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