客户端的分片上传和断点续传是指将一个文件拆分为多个分片,在网络不稳定或上传中断后,可以从中断处继续上传而不需要重新上传整个文件。 当需要上传大型文件时,客户端的分片上传和断点续传是一种常用的技术。它可以提高上传效率并减少网络中断造成的影响。
大文件下载流程
- 发起下载请求:客户端向服务器发送下载请求,请求包括要下载的文件名和希望的分片大小。
- 获取文件信息:客户端接收到服务器的响应,从响应头中获取文件的总大小(fSize),文件名(fName)以及服务器是否支持分片下载(Accept-Range字段)等信息。
- 计算分片数目:根据希望的分片大小和文件的总大小,计算出所需的分片数目。例如,如果希望每个分片大小为1MB,文件总大小为10MB,则需要分为10个分片。
- 请求下载分片:客户端根据计算得到的分片数目,循环发送多个请求去下载每个分片。每个请求需要设置Range头字段,指定要下载的分片范围。
- 接收分片数据:客户端接收到服务器返回的每个分片数据,并将其保存在本地。
- 合并分片:当所有分片都下载完成后,客户端将所有分片按顺序合并成完整的文件。
- 下载完成:客户端完成文件的下载,并进行必要的处理(例如保存到指定位置、通知用户下载完成等)。
引入依赖
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.1</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.4</version>
</dependency>
<!-- 发起服务器请求-->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
</dependency>
<!-- 发起服务器请求-->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
服务端的代码
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
@Controller
public class DownLoadController {
private final static String utf8 = "utf-8";
/**
* 客户端获取要下载文件信息
* @param request
* @param response
* @throws IOException
*/
@GetMapping("/getFileInfo")
public void getFileInfo(HttpServletRequest request, HttpServletResponse response) throws IOException {
String targetFileName = request.getHeader("FileName");
response.setCharacterEncoding(utf8);
//定义文件路径
File file = new File("C:\\Users\\zhang\\Downloads\\"+targetFileName);
long fSize = file.length();//获取长度
// URLEncoder.encode方法会将文件名中的空格、中文等特殊字符转换为对应的十六进制编码
String fileName = URLEncoder.encode(file.getName(),utf8);
//根据前端传来的Range 判断支不支持分片下载
response.setHeader("Accept-Range","bytes");
//获取文件大小
response.setHeader("fSize",String.valueOf(fSize));
response.setHeader("fName",fileName);
}
/**
* 分片下载大文件
* @param request
* @param response
* @throws IOException
*/
@RequestMapping("/down")
public void downLoadFile(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.setCharacterEncoding(utf8);
//定义文件路径
File file = new File("C:\\Users\\zhang\\Downloads\\CentOS-7-x86_64-DVD-2009.iso");
InputStream is = null;
OutputStream os = null;
try {
//分片下载
long fSize = file.length();//获取长度
response.setContentType("application/x-download");
// URLEncoder.encode方法会将文件名中的空格、中文等特殊字符转换为对应的十六进制编码
String fileName = URLEncoder.encode(file.getName(),utf8);
response.addHeader("Content-Disposition","attachment;filename="+fileName);
//根据前端传来的Range 判断支不支持分片下载
response.setHeader("Accept-Range","bytes");
//获取文件大小
response.setHeader("fSize",String.valueOf(fSize));
response.setHeader("fName",fileName);
//定义断点
long pos = 0,last = fSize-1,sum = 0;
//判断前端需不需要分片下载
if (null != request.getHeader("Range")){
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
// 使用replaceAll方法将"Range"字段中的"bytes="替换为空字符串,得到真实的字节范围。
String numRange = request.getHeader("Range").replaceAll("bytes=","");
// 使用split方法将字节范围字符串按照"-"进行分割,得到开始位置和结束位置的字符串数组strRange
String[] strRange = numRange.split("-");
// 如果strRange的长度为2,说明客户端请求了指定范围的文件内容
if (strRange.length == 2){
pos = Long.parseLong(strRange[0].trim());
last = Long.parseLong(strRange[1].trim());
//若结束字节超出文件大小 取文件大小
if (last>fSize-1){
last = fSize-1;
}
// 如果strRange的长度不为2,则说明客户端请求的是从某个位置开始一直到文件结束的字节范围。
}else {
//若只给一个长度 开始位置一直到结束
pos = Long.parseLong(numRange.replaceAll("-","").trim());
}
}
long rangeLenght = last-pos+1;
// 举例:bytes 500-1000/2000
String contentRange = new StringBuffer("bytes").append(pos).append("-").append(last).append("/").append(fSize).toString();
// 服务器返回的文件内容的字节范围
response.setHeader("Content-Range",contentRange);
// 服务器将返回的文件内容的字节范围长度。
response.setHeader("Content-Lenght",String.valueOf(rangeLenght));
os = new BufferedOutputStream(response.getOutputStream());
is = new BufferedInputStream(new FileInputStream(file));
is.skip(pos);//跳过已读的文件
byte[] buffer = new byte[1024];
int lenght = 0;
//相等证明读完
while (sum < rangeLenght){
// 从输入流is中读取文件内容,并将实际读取的字节数赋值给length
lenght = is.read(buffer,0, (rangeLenght-sum)<=buffer.length? (int) (rangeLenght - sum) :buffer.length);
// 用于更新已经读取的字节总数
sum = sum+lenght;
os.write(buffer,0,lenght);
}
System.out.println("下载完成");
}finally {
if (is!= null){
is.close();
}
if (os!=null){
os.close();
}
}
}
}
客户端的代码
import org.apache.commons.io.FileUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.HttpClients;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.*;
import java.net.URLDecoder;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@RestController
public class DownloadClient {
private final static long per_page = 1024l*1024l*50l;
// 分片存储临时目录 当分片下载完后在目录中找到文件合并
// 先确保有此目录
private final static String down_path="D:\\tmp\\file";
//多线程下载
ExecutorService pool = Executors.newFixedThreadPool(10);
//文件大小 分片数量 文件名称
//使用探测 获取变量
//使用多线程分片下载
//最后一个分片下载完 开始合并
@RequestMapping("/downloadFile")
public String downloadFile() throws IOException {
FileInfo fileInfo = getFileInfo(targetFileName);
if (fileInfo!= null){
long pages = fileInfo.fSize/per_page;
for (int i = 0; i <= pages; i++) {
// i*per_page表示当前页的起始位置,(i+1)*per_page-1表示当前页的结束位置(因为是左闭右开区间,需要减去1)
pool.submit(new Download(i*per_page,(i+1)*per_page-1,i,fileInfo.fName));
}
}
return "成功";
}
class Download implements Runnable{
long start;
long end;
long page;
String fName;
public Download(long start, long end, long page, String fName) {
this.start = start;
this.end = end;
this.page = page;
this.fName = fName;
}
@Override
public void run() {
try {
download(start,end,page,fName);
} catch (IOException e) {
e.printStackTrace();
}
}
}
private String targetFileName="CentOS-7-x86_64-DVD-2009.iso";
//返回文件名 跟大小
private FileInfo getFileInfo(String targetFileName) throws IOException {
//需要知道 开始-结束 = 分片大小
HttpClient client = HttpClients.createDefault();
//httpclient进行请求,服务端接口
HttpGet httpGet = new HttpGet("http://127.0.0.1:8080/getFileInfo");
//告诉服务端获取的文件信息
httpGet.setHeader("FileName",targetFileName);
HttpResponse response = client.execute(httpGet);
String fSize = response.getFirstHeader("fSize").getValue();
String fName= URLDecoder.decode(response.getFirstHeader("fName").getValue(),"utf-8");
return new FileInfo(Long.valueOf(fSize),fName);
}
// 下载分片
private void download(long start,long end,long page,String fName) throws IOException {
//断点下载 文件存在不需要下载
File file = new File(down_path, page + "-" + fName);
//探测必须放行 若下载分片只下载一半就锻炼需要重新下载所以需要判断文件是否完整
if (file.exists()&&page != -1&&file.length()==per_page){
return ;
}
//需要知道 开始-结束 = 分片大小
HttpClient client = HttpClients.createDefault();
//httpclient进行请求,服务端接口
HttpGet httpGet = new HttpGet("http://127.0.0.1:8080/down");
//告诉服务端做分片下载
httpGet.setHeader("Range","bytes="+start+"-"+end);
HttpResponse response = client.execute(httpGet);
String fSize = response.getFirstHeader("fSize").getValue();
fName= URLDecoder.decode(response.getFirstHeader("fName").getValue(),"utf-8");
HttpEntity entity = response.getEntity();//获取文件流对象
InputStream is = entity.getContent();
//临时存储分片文件
FileOutputStream fos = new FileOutputStream(file);
byte[] buffer = new byte[1024];//定义缓冲区
int ch;
while ((ch = is.read(buffer)) != -1){
fos.write(buffer,0,ch);
}
is.close();
fos.flush();
fos.close();
//判断是不是最后一个分片,end - Long.valueOf(fSize)来判断当前分片的结束位置是否超过了文件的总大小。如果结果大于0,说明当前分片是最后一个分片。
if (end-Long.valueOf(fSize)>0){
//合并
try {
mergeFile(fName,page);
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 合并文件
private void mergeFile(String fName, long page) throws Exception {
//归并文件位置
File file = new File(down_path, fName);
BufferedOutputStream os = new BufferedOutputStream(new FileOutputStream(file));
for (int i = 0; i <= page; i++) {
File tempFile = new File(down_path, i + "-" + fName);
//分片没下载或者没下载完需要等待
while (!file.exists()||(i!=page&&tempFile.length()<per_page)){
Thread.sleep(100);
}
byte[] bytes = FileUtils.readFileToByteArray(tempFile);
os.write(bytes);
os.flush();
tempFile.delete();
}
os.flush();
os.close();
}
//使用内部类实现
class FileInfo{
long fSize;
String fName;
public FileInfo(long fSize, String fName) {
this.fSize = fSize;
this.fName = fName;
}
}
}
测试
访问/localhost:8080/downloadFile
生成分片
合并文件完成