客户端软件断点续传指的是在下载或上传时,将下载或上传任务(一个文件或一个压缩包)人为的划分为几个部分,每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传下载未完成的部分,而没有必要从头开始上传下载
(将文件分片以及后续合并是一个不小的工作量,由于项目时间有限,我并没有做分片,只是实现了可断点下载)
需要前端和后端的配合,前端在请求头中 标明 下载开始的位置,后端重标记位置开始向前端输出文件剩余部分。
在简单模式下,前端不需要知道文件大小,也不许要知道文件是否已经下载完毕。当文件可以正常打开时即文件下载完毕。(若想知道文件是否下载完毕,可写个接口比较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
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) { } } } }
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(); } }
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); } }