java 实现断点续传服务
一:什么是断点续传
客户端软件断点续传指的是在下载或上传时,将下载或上传任务(一个文件或一个压缩包)人为的划分为几个部分,每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传下载未完成的部分,而没有必要从头开始上传下载
(将文件分片以及后续合并是一个不小的工作量,由于项目时间有限,我并没有做分片,只是实现了可断点下载)
二:实现原理
2.1 实现思路
需要前端和后端的配合,前端在请求头中 标明 下载开始的位置,后端重标记位置开始向前端输出文件剩余部分。
在简单模式下,前端不需要知道文件大小,也不许要知道文件是否已经下载完毕。当文件可以正常打开时即文件下载完毕。(若想知道文件是否下载完毕,可写个接口比较Range 值与文件大小)
一般服务请求头
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
响应头
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
如果要服务器支持断点续传功能的话,需要在请求头中表明文件开始下载的位置
请求头
GET /down.zip HTTP/1.0
User-Agent: NetFox
RANGE: bytes=2000070- #表示文件从2000070处开始下载
# Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
响应头
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
三:java代码实现
3.1 BreakPoinService类
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
@Service
public class BreakPoinService {
//断点续传
public void downLoadByBreakpoint(File file, long start, long end, HttpServletResponse response){
OutputStream stream = null;
RandomAccessFile fif = null;
try {
if (end <= 0) {
end = file.length() - 1;
}
stream = response.getOutputStream();
response.reset();
response.setStatus(206);
response.setContentType("application/octet-stream");
response.setHeader("Content-disposition", "attachment; filename=" + file.getName());
response.setHeader("Content-Length", String.valueOf(end - start + 1));
response.setHeader("file-size", String.valueOf(file.length()));
response.setHeader("Accept-Ranges", "bytes");
response.setHeader("Content-Range", String.format("bytes %s-%s/%s", start, end, file.length()));
fif = new RandomAccessFile(file, "r");
fif.seek(start);
long index = start;
int d;
byte[] buf = new byte[10240];
while (index <= end && (d = fif.read(buf)) != -1) {
if (index + d > end) {
d = (int)(end - index + 1);
}
index += d;
stream.write(buf, 0, d);
}
stream.flush();
} catch (Exception e) {
try {
if (stream != null)
stream.close();
if (fif != null)
fif.close();
} catch (Exception e11) {
}
}
}
//全量下载
public void downLoadAll(File file, HttpServletResponse response){
OutputStream stream = null;
BufferedInputStream fif = null;
try {
stream = response.getOutputStream();
response.reset();
response.setContentType("application/octet-stream");
response.setHeader("Content-disposition", "attachment; filename=" + file.getName());
response.setHeader("Content-Length", String.valueOf(file.length()));
fif = new BufferedInputStream(new FileInputStream(file));
int d;
byte[] buf = new byte[10240];
while ((d = fif.read(buf)) != -1) {
stream.write(buf, 0, d);
}
stream.flush();
} catch (Exception e) {
try {
if (stream != null)
stream.close();
if (fif != null)
fif.close();
} catch (Exception e11) {
}
}
}
}
3.2 断点续传控制类
import cn.ztuo.api.cos.QCloudStorageService;
import cn.ztuo.api.service.IBreakpointResumeService;
import cn.ztuo.api.service.impl.BreakPoinService;
import cn.ztuo.commons.annotation.PassToken;
import cn.ztuo.commons.response.CommonResult;
import cn.ztuo.mbg.entity.BreakpointResume;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.Date;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 断点续传控制类
*/
@RestController
@RequestMapping("/breakpoint")
public class BreakPointController {
@Autowired
private IBreakpointResumeService breakpointResumeService;
@Autowired
private BreakPoinService breakPoinService;
@Autowired
private QCloudStorageService storageService;
@PassToken
@GetMapping(value = "resource")
public CommonResult download(HttpServletRequest request, HttpServletResponse response, @RequestParam("key") String key) {
LambdaQueryWrapper<BreakpointResume> brWrapper=new LambdaQueryWrapper<>();
brWrapper.eq(BreakpointResume::getCodKey,key);
List<BreakpointResume> list = breakpointResumeService.list(brWrapper);
String str=null;
//如果本地存在取本地文件
if(list.size()>0){
BreakpointResume breakpointResume = list.get(0);
str=breakpointResume.getFilePath();
}else{//本地不存在
try{
String download = storageService.download(key);
BreakpointResume breakpointResume=new BreakpointResume();
breakpointResume.setCodKey(key);
breakpointResume.setFilePath(download);
breakpointResume.setCreateTime(new Date());
breakpointResume.setUpdateTime(new Date());
boolean save = breakpointResumeService.save(breakpointResume);
if(save){
str=download;
}else{
return CommonResult.error();
}
}catch (Exception e){
return CommonResult.error();
}
}
if(str==null){
return CommonResult.error();
}
File file=new File(str);
if (file.exists()) {
String range = request.getHeader("Range");
if (range != null && (range = range.trim()).length() > 0) {
Pattern rangePattern = Pattern.compile("^bytes=([0-9]+)-([0-9]+)?$");
Matcher matcher = rangePattern.matcher(range);
if (matcher.find()) {
Integer start = Integer.valueOf(matcher.group(1));
Integer end = 0;
String endStr = matcher.group(2);
if (endStr != null && (endStr = endStr.trim()).length() > 0)
end = Integer.valueOf(endStr);
breakPoinService.downLoadByBreakpoint(file, start, end, response);
return null;
}
}
breakPoinService.downLoadAll(file, response);
return null;
}
return CommonResult.error();
}
}
3.3 自定义全局响应类
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T> {
private String code;
private String msg;
private T data;
public CommonResult(String code,String msg){
this.code=code;
this.msg=msg;
}
public static CommonResult success(){
return create("200","成功");
}
public static <T> CommonResult success(T data){
CommonResult result = create("200", "成功");
result.setData(data);
return result;
}
public static CommonResult error(){
return create("500","服务器开小差了");
}
public static CommonResult create(String code,String msg){
return new CommonResult(code,msg);
}
}