一:什么是断点续传
客户端软件断点续传指的是在下载或上传时,将下载或上传任务(一个文件或一个压缩包)人为的划分为几个部分,每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传下载未完成的部分,而没有必要从头开始上传下载
(将文件分片以及后续合并是一个不小的工作量,由于项目时间有限,我并没有做分片,只是实现了可断点下载)
二:实现原理
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);
}
}
另附一段
另外一文件抄过来的内容
//实现文件下载功能
public String downloadFile(){
File dir = new File(filepath);//获取文件路劲
if(!dir.exists()) {
System.out.println("文件路径错误");
log.debug("文件路径错误");
return "failed";// 判断文件或文件夹是否存在
}
File downloadFile = new File(dir, filename);//在指定目录下查找文件
if(!downloadFile.isFile()){
System.out.println("文件不存在");
log.debug("文件不存在");
return "failed";// 判断文件或文件夹是否存在
}
try {
downloadFileRanges(downloadFile);
} catch(ClientAbortException e){
System.out.println("连接被终止");
log.debug("连接被终止");
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
private void downloadFileRanges(File downloadFile) throws IOException {
// 要下载的文件大小
long fileLength = downloadFile.length();
// 已下载的文件大小
long pastLength = 0;
// 是否快车下载,否则为迅雷或其他
boolean isFlashGet = true;
// 用于记录需要下载的结束字节数(迅雷或其他下载)
long lenEnd = 0;
// 用于记录客户端要求下载的数据范围字串
String rangeBytes = request.getHeader("Range");
//用于随机读取写入文件
RandomAccessFile raf = null;
OutputStream os = null;
OutputStream outPut = null;
byte b[] = new byte[1024];
// 如果客户端下载请求中包含了范围
if (null != rangeBytes)
{
// 返回码 206
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
rangeBytes = request.getHeader("Range").replaceAll("bytes=", "");
// 判断 Range 字串模式
if (rangeBytes.indexOf('-') == rangeBytes.length() - 1)
{
// 无结束字节数,为快车
isFlashGet = true;
rangeBytes = rangeBytes.substring(0, rangeBytes.indexOf('-'));
pastLength = Long.parseLong(rangeBytes.trim());
}
else
{
// 迅雷下载
isFlashGet = false;
String startBytes = rangeBytes.substring(0,
rangeBytes.indexOf('-'));
String endBytes = rangeBytes.substring(
rangeBytes.indexOf('-') + 1, rangeBytes.length());
// 已下载文件段
pastLength = Long.parseLong(startBytes.trim());
// 还需下载的文件字节数(从已下载文件段开始)
lenEnd = Long.parseLong(endBytes);
}
}
// 通知客户端允许断点续传,响应格式为:Accept-Ranges: bytes
response.setHeader("Accept-Ranges", "bytes");
// response.reset();
// 如果为第一次下载,则状态默认为 200,响应格式为: HTTP/1.1 200 ok
if (0 != pastLength)
{
// 内容范围字串
String contentRange = "";
// 响应格式
// Content-Range: bytes [文件块的开始字节]-[文件的总大小 - 1]||[文件的总大小]
if (isFlashGet)
{
contentRange = new StringBuffer("bytes")
.append(new Long(pastLength).toString()).append("-")
.append(new Long(fileLength - 1).toString())
.append("/").append(new Long(fileLength).toString())
.toString();
}
else
{
contentRange = new StringBuffer(rangeBytes).append("/")
.append(new Long(fileLength).toString()).toString();
}
response.setHeader("Content-Range", contentRange);
}
String fileName = getDownloadChineseFileName(filename);
response.setHeader("Content-Disposition",
"attachment;filename=" + fileName + "");
// 响应的格式是:
response.setContentType("application/octet-stream");
response.addHeader("Content-Length", String.valueOf(fileLength));
try
{
os = response.getOutputStream();
outPut = new BufferedOutputStream(os);
raf = new RandomAccessFile(downloadFile, "r");
// 跳过已下载字节
raf.seek(pastLength);
if (isFlashGet)
{
// 快车等
int n = 0;
while ((n = raf.read(b, 0, 1024)) != -1)
{
outPut.write(b, 0, n);
}
}
else
{
// 迅雷等
while (raf.getFilePointer() < lenEnd)
{
outPut.write(raf.read());
}
}
outPut.flush();
}
catch (IOException e)
{
/**
* 在写数据的时候 对于 ClientAbortException 之类的异常
* 是因为客户端取消了下载,而服务器端继续向浏览器写入数据时, 抛出这个异常,这个是正常的。 尤其是对于迅雷这种吸血的客户端软件。
* 明明已经有一个线程在读取 bytes=1275856879-1275877358,
* 如果短时间内没有读取完毕,迅雷会再启第二个、第三个。。。线程来读取相同的字节段, 直到有一个线程读取完毕,迅雷会 KILL
* 掉其他正在下载同一字节段的线程, 强行中止字节读出,造成服务器抛 ClientAbortException。
* 所以,我们忽略这种异常
*/
}
finally
{
if(outPut != null)
{
outPut.close();
}
if(raf != null)
{
raf.close();
}
}
}
private String getDownloadChineseFileName(String paramName)
{
String downloadChineseFileName = "";
try
{
downloadChineseFileName = new String(paramName.getBytes("GBK"),
"ISO8859-1");
}
catch (UnsupportedEncodingException e)
{
e.printStackTrace();
}
return downloadChineseFileName;
}
public String getFilepath() {
return filepath;
}
public void setFilepath(String filepath) {
this.filepath = filepath;
}
public String getFilename() {
return filename;
}
public void setFilename(String filename) {
this.filename = filename;
}
public HttpServletRequest getRequest() {
return request;
}
public HttpServletResponse getResponse() {
return response;
}
2. struts部分
<action name="downloadFile" class="downloadFileAction" method="downloadFile">
<result name="failed" type="redirectAction">showDownloadFileNameList</result>
</action>
3. jsp部分
<td><a href="downloadFile?filename=${fileMap.key }&&filepath=${fileMap.value }">文件下载</a></td>