Spring MVC大文件的断点续传(File Transfer Resume)

Spring MVC大文件的断点续传(File Transfer Resume)

  • 博客分类: 
  • Java

 

根据 HTTP/1.1 协议,客户端可以获取 response 资源的一部分,以便由于在通信中断后还能够继续前一次的请求,常见的场景包括大文件下载视频播放的前进/后退等。 

以下是一个Byte-Range请求的具体HTTP信息: 

引用

【Status Code】 
206 Partial Content (出错时416) 

【Request Headers】 
Range:bytes=19448183- 

【Response Headers】 
Accept-Ranges:bytes 
Content-Length:58 
Content-Range:bytes 19448183-19448240/19448241 
Content-Type:video/mp4


详细说明可以参考HTTP/1.1(RFC 2616)的以下部分: 

  • 3.12 Range Units
  • 14.5 Accept-Ranges
  • 14.16 Content-Range
  • 14.27 If-Range
  • 14.35 Range

以下是一次请求的处理链: 
(0)Client Request -> (1)Web Server -> (2)Servlet Container -> (3)Web Framework -> (4) Your Code 

  • Web Server: 一般遵循HTTP/1.1 协议都支持Byte-Range请求,比如Apache、Ngnix
  • Servlet Container:大部分也支持,比如Tomcat的DefaultServlet.java
  • Web Framework: Spring4.2开始支持,具体可以查看ResourceHttpRequestHandler.java

截止到第三步都是针对static resources的,如果你想经过逻辑处理后再动态返回resource的话,就到了第四步,也就是在自己写的代码里相应Byte-Range请求。 

可以通过 Spring MVC 的 XmlViewResolver中注册一个自定义的View,在Controller中返回该View来实现。 

Java代码  收藏代码

  1. ModelAndView mv = new ModelAndView("byteRangeViewRender");  
  2. mv.addObject("file", new File("C:\\RenSanNing\\xxx.mp4"));  
  3. mv.addObject("contentType", "video/mp4");  
  4. return mv;  


这样具体的Byte-Range请求处理都将会在ByteRangeViewRender中进行。ByteRangeViewRender的具体实现可以参考Tomcat的DefaultServlet.java和Spring的ResourceHttpRequestHandler.java。 

以下是一个写好的View: 

Java代码  收藏代码

  1. public class ByteRangeViewRender extends AbstractView {  
  2.   
  3.     // Constants ----------------------------------------------------------------------------------  
  4.   
  5.     private static final int DEFAULT_BUFFER_SIZE = 20480; // ..bytes = 20KB.  
  6.     private static final long DEFAULT_EXPIRE_TIME = 604800000L; // ..ms = 1 week.  
  7.     private static final String MULTIPART_BOUNDARY = "MULTIPART_BYTERANGES";  
  8.   
  9.     @Override  
  10.     protected void renderMergedOutputModel(Map<String, Object> objectMap,  
  11.             HttpServletRequest request, HttpServletResponse response)  
  12.             throws Exception {  
  13.   
  14.         File file = (File) objectMap.get("file");  
  15.         if (file == null || !file.exists()) {  
  16.             response.sendError(HttpServletResponse.SC_NOT_FOUND);  
  17.             return;  
  18.         }  
  19.    
  20.         String contentType = (String) objectMap.get("contentType");  
  21.           
  22.         String fileName = file.getName();  
  23.         long length = file.length();  
  24.         long lastModified = file.lastModified();  
  25.         String eTag = fileName + "_" + length + "_" + lastModified;  
  26.         long expires = System.currentTimeMillis() + DEFAULT_EXPIRE_TIME;  
  27.   
  28.         // Validate request headers for caching ---------------------------------------------------  
  29.   
  30.         // If-None-Match header should contain "*" or ETag. If so, then return 304.  
  31.         String ifNoneMatch = request.getHeader("If-None-Match");  
  32.         if (ifNoneMatch != null && matches(ifNoneMatch, fileName)) {  
  33.             response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);  
  34.             response.setHeader("ETag", eTag); // Required in 304.  
  35.             response.setDateHeader("Expires", expires); // Postpone cache with 1 week.  
  36.             return;  
  37.         }  
  38.   
  39.         // If-Modified-Since header should be greater than LastModified. If so, then return 304.  
  40.         // This header is ignored if any If-None-Match header is specified.  
  41.         long ifModifiedSince = request.getDateHeader("If-Modified-Since");  
  42.         if (ifNoneMatch == null && ifModifiedSince != -1 && ifModifiedSince + 1000 > lastModified) {  
  43.             response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);  
  44.             response.setHeader("ETag", eTag); // Required in 304.  
  45.             response.setDateHeader("Expires", expires); // Postpone cache with 1 week.  
  46.             return;  
  47.         }  
  48.   
  49.         // Validate request headers for resume ----------------------------------------------------  
  50.   
  51.         // If-Match header should contain "*" or ETag. If not, then return 412.  
  52.         String ifMatch = request.getHeader("If-Match");  
  53.         if (ifMatch != null && !matches(ifMatch, fileName)) {  
  54.             response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);  
  55.             return;  
  56.         }  
  57.   
  58.         // If-Unmodified-Since header should be greater than LastModified. If not, then return 412.  
  59.         long ifUnmodifiedSince = request.getDateHeader("If-Unmodified-Since");  
  60.         if (ifUnmodifiedSince != -1 && ifUnmodifiedSince + 1000 <= lastModified) {  
  61.             response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);  
  62.             return;  
  63.         }  
  64.           
  65.         // Validate and process range -------------------------------------------------------------  
  66.   
  67.         // Prepare some variables. The full Range represents the complete file.  
  68.         Range full = new Range(0, length - 1, length);  
  69.         List<Range> ranges = new ArrayList<Range>();  
  70.   
  71.         // Validate and process Range and If-Range headers.  
  72.         String range = request.getHeader("Range");  
  73.         if (range != null) {  
  74.   
  75.             // Range header should match format "bytes=n-n,n-n,n-n...". If not, then return 416.  
  76.             if (!range.matches("^bytes=\\d*-\\d*(,\\d*-\\d*)*$")) {  
  77.                 response.setHeader("Content-Range", "bytes */" + length); // Required in 416.  
  78.                 response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);  
  79.                 return;  
  80.             }  
  81.   
  82.             String ifRange = request.getHeader("If-Range");  
  83.             if (ifRange != null && !ifRange.equals(eTag)) {  
  84.                 try {  
  85.                     long ifRangeTime = request.getDateHeader("If-Range"); // Throws IAE if invalid.  
  86.                     if (ifRangeTime != -1 && ifRangeTime + 1000 < lastModified) {  
  87.                         ranges.add(full);  
  88.                     }  
  89.                 } catch (IllegalArgumentException ignore) {  
  90.                     ranges.add(full);  
  91.                 }  
  92.             }  
  93.   
  94.             // If any valid If-Range header, then process each part of byte range.  
  95.             if (ranges.isEmpty()) {  
  96.                 for (String part : range.substring(6).split(",")) {  
  97.                     // Assuming a file with length of 100, the following examples returns bytes at:  
  98.                     // 50-80 (50 to 80), 40- (40 to length=100), -20 (length-20=80 to length=100).  
  99.                     long start = sublong(part, 0, part.indexOf("-"));  
  100.                     long end = sublong(part, part.indexOf("-") + 1, part.length());  
  101.   
  102.                     if (start == -1) {  
  103.                         start = length - end;  
  104.                         end = length - 1;  
  105.                     } else if (end == -1 || end > length - 1) {  
  106.                         end = length - 1;  
  107.                     }  
  108.   
  109.                     // Check if Range is syntactically valid. If not, then return 416.  
  110.                     if (start > end) {  
  111.                         response.setHeader("Content-Range", "bytes */" + length); // Required in 416.  
  112.                         response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);  
  113.                         return;  
  114.                     }  
  115.   
  116.                     // Add range.                      
  117.                     ranges.add(new Range(start, end, length));  
  118.                 }  
  119.             }  
  120.         }  
  121.   
  122.         // Prepare and initialize response --------------------------------------------------------  
  123.   
  124.         // Get content type by file name and set content disposition.  
  125.         String disposition = "inline";  
  126.   
  127.         // If content type is unknown, then set the default value.  
  128.         // For all content types, see: http://www.w3schools.com/media/media_mimeref.asp  
  129.         // To add new content types, add new mime-mapping entry in web.xml.  
  130.         if (contentType == null) {  
  131.             contentType = "application/octet-stream";  
  132.         } else if (!contentType.startsWith("image")) {  
  133.             // Else, expect for images, determine content disposition. If content type is supported by  
  134.             // the browser, then set to inline, else attachment which will pop a 'save as' dialogue.  
  135.             String accept = request.getHeader("Accept");  
  136.             disposition = accept != null && accepts(accept, contentType) ? "inline" : "attachment";  
  137.         }  
  138.   
  139.         // Initialize response.  
  140.         response.reset();  
  141.         response.setBufferSize(DEFAULT_BUFFER_SIZE);  
  142.         response.setHeader("Content-Disposition", disposition + ";filename=\"" + fileName + "\"");  
  143.         response.setHeader("Accept-Ranges", "bytes");  
  144.         response.setHeader("ETag", eTag);  
  145.         response.setDateHeader("Last-Modified", lastModified);  
  146.         response.setDateHeader("Expires", expires);  
  147.   
  148.         // Send requested file (part(s)) to client ------------------------------------------------  
  149.   
  150.         // Prepare streams.  
  151.         RandomAccessFile input = null;  
  152.         OutputStream output = null;  
  153.   
  154.         try {  
  155.             // Open streams.  
  156.             input = new RandomAccessFile(file, "r");  
  157.             output = response.getOutputStream();  
  158.   
  159.             if (ranges.isEmpty() || ranges.get(0) == full) {  
  160.   
  161.                 // Return full file.  
  162.                 Range r = full;  
  163.                 response.setContentType(contentType);  
  164.                 response.setHeader("Content-Range", "bytes " + r.start + "-" + r.end + "/" + r.total);  
  165.                 response.setHeader("Content-Length", String.valueOf(r.length));  
  166.                   
  167.                 copy(input, output, r.start, r.length);  
  168.                   
  169.             } else if (ranges.size() == 1) {  
  170.   
  171.                 // Return single part of file.  
  172.                 Range r = ranges.get(0);  
  173.                 response.setContentType(contentType);  
  174.                 response.setHeader("Content-Range", "bytes " + r.start + "-" + r.end + "/" + r.total);  
  175.                 response.setHeader("Content-Length", String.valueOf(r.length));  
  176.                 response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206.  
  177.   
  178.                 // Copy single part range.  
  179.                 copy(input, output, r.start, r.length);  
  180.   
  181.             } else {  
  182.   
  183.                 // Return multiple parts of file.  
  184.                 response.setContentType("multipart/byteranges; boundary=" + MULTIPART_BOUNDARY);  
  185.                 response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206.  
  186.   
  187.                 // Cast back to ServletOutputStream to get the easy println methods.  
  188.                 ServletOutputStream sos = (ServletOutputStream) output;  
  189.   
  190.                 // Copy multi part range.  
  191.                 for (Range r : ranges) {  
  192.                     // Add multipart boundary and header fields for every range.  
  193.                     sos.println();  
  194.                     sos.println("--" + MULTIPART_BOUNDARY);  
  195.                     sos.println("Content-Type: " + contentType);  
  196.                     sos.println("Content-Range: bytes " + r.start + "-" + r.end + "/" + r.total);  
  197.   
  198.                     // Copy single part range of multi part range.  
  199.                     copy(input, output, r.start, r.length);  
  200.                 }  
  201.   
  202.                 // End with multipart boundary.  
  203.                 sos.println();  
  204.                 sos.println("--" + MULTIPART_BOUNDARY + "--");  
  205.             }  
  206.         } finally {  
  207.             close(output);  
  208.             close(input);  
  209.         }  
  210.   
  211.     }  
  212.       
  213.     // Helpers (can be refactored to public utility class) ----------------------------------------  
  214.   
  215.     private void close(Closeable resource) {  
  216.         if (resource != null) {  
  217.             try {  
  218.                 resource.close();  
  219.             } catch (IOException ignore) {  
  220.             }  
  221.         }  
  222.     }  
  223.       
  224.     private void copy(RandomAccessFile input, OutputStream output, long start, long length) throws IOException {  
  225.         byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];  
  226.         int read;  
  227.   
  228.         try {  
  229.             if (input.length() == length) {  
  230.                 // Write full range.  
  231.                 while ((read = input.read(buffer)) > 0) {  
  232.                     output.write(buffer, 0, read);  
  233.                 }  
  234.             } else {  
  235.                 input.seek(start);  
  236.                 long toRead = length;  
  237.       
  238.                 while ((read = input.read(buffer)) > 0) {  
  239.                     if ((toRead -= read) > 0) {  
  240.                         output.write(buffer, 0, read);  
  241.                     } else {  
  242.                         output.write(buffer, 0, (int) toRead + read);  
  243.                         break;  
  244.                     }  
  245.                 }  
  246.             }  
  247.         } catch (IOException ignore) {  
  248.         }  
  249.     }  
  250.       
  251.     private long sublong(String value, int beginIndex, int endIndex) {  
  252.         String substring = value.substring(beginIndex, endIndex);  
  253.         return (substring.length() > 0) ? Long.parseLong(substring) : -1;  
  254.     }   
  255.   
  256.     private boolean accepts(String acceptHeader, String toAccept) {  
  257.         String[] acceptValues = acceptHeader.split("\\s*(,|;)\\s*");  
  258.         Arrays.sort(acceptValues);  
  259.         return Arrays.binarySearch(acceptValues, toAccept) > -1  
  260.             || Arrays.binarySearch(acceptValues, toAccept.replaceAll("/.*$", "/*")) > -1  
  261.             || Arrays.binarySearch(acceptValues, "*/*") > -1;  
  262.     }  
  263.   
  264.     private boolean matches(String matchHeader, String toMatch) {  
  265.         String[] matchValues = matchHeader.split("\\s*,\\s*");  
  266.         Arrays.sort(matchValues);  
  267.         return Arrays.binarySearch(matchValues, toMatch) > -1  
  268.             || Arrays.binarySearch(matchValues, "*") > -1;  
  269.     }  
  270.   
  271.     // Inner classes ------------------------------------------------------------------------------  
  272.   
  273.     protected class Range {  
  274.         long start;  
  275.         long end;  
  276.         long length;  
  277.         long total;  
  278.   
  279.         public Range(long start, long end, long total) {  
  280.             this.start = start;  
  281.             this.end = end;  
  282.             this.length = end - start + 1;  
  283.             this.total = total;  
  284.         }  
  285.     }  
  286.       
  287. }  



大文件上传时显示进度条 

1)application-context.xml 

Xml代码  收藏代码

  1. <bean id="multipartResolver" class="com.rensanning.test.core.fileupload.CustomMultipartResolver">  
  2.     <property name="defaultEncoding" value="UTF-8"/>  
  3.     <property name="fileSizeMax" value="20971520"/><!-- 20M : Maximum size of a single uploaded file-->  
  4.     <property name="maxUploadSize" value="52428800"/><!-- 50M : The maximum allowed size of a complete request-->  
  5. </bean>  



2)CustomMultipartResolver.java 

Java代码  收藏代码

  1. public class CustomMultipartResolver extends CommonsMultipartResolver {  
  2.       
  3.     @Autowired  
  4.     private CustomProgressListener progressListener;  
  5.        
  6.     public void setFileUploadProgressListener(CustomProgressListener progressListener){  
  7.         this.progressListener = progressListener;  
  8.     }  
  9.       
  10.     public void setFileSizeMax(long fileSizeMax) {  
  11.         getFileUpload().setFileSizeMax(fileSizeMax);  
  12.     }  
  13.     
  14.     @Override  
  15.     public MultipartParsingResult parseRequest(HttpServletRequest request) throws MultipartException {  
  16.         String encoding = determineEncoding(request);  
  17.         FileUpload fileUpload = prepareFileUpload(encoding);  
  18.           
  19.         progressListener.setSession(request.getSession());  
  20.         fileUpload.setProgressListener(progressListener);  
  21.           
  22.         try {  
  23.             List<FileItem> fileItems = ((ServletFileUpload) fileUpload).parseRequest(request);  
  24.             return parseFileItems(fileItems, encoding);  
  25.         }  
  26.         catch (FileUploadBase.SizeLimitExceededException ex) {  
  27.             throw new MaxUploadSizeExceededException(fileUpload.getSizeMax(), ex);  
  28.         }  
  29.         catch (FileUploadBase.FileSizeLimitExceededException ex) {  
  30.             throw new MaxUploadSizeExceededException(fileUpload.getFileSizeMax(), ex);  
  31.         }  
  32.         catch (FileUploadException ex) {  
  33.             throw new MultipartException("Could not parse multipart servlet request", ex);  
  34.         }  
  35.     }  
  36.   
  37. }  



3)CustomProgressListener.java 

Java代码  收藏代码

  1. @Component  
  2. public class CustomProgressListener implements ProgressListener {  
  3.       
  4.     private HttpSession session;    
  5.    
  6.     public void setSession(HttpSession session){  
  7.         this.session = session;  
  8.         ProgressInfo ps = new ProgressInfo();  
  9.         this.session.setAttribute(Constants.SESSION_KEY_UPLOAD_PROGRESS_INFO, ps);  
  10.     }  
  11.       
  12.     @Override  
  13.     public void update(long pBytesRead, long pContentLength, int pItems) {  
  14.         ProgressInfo ps = (ProgressInfo) session.getAttribute(Constants.SESSION_KEY_UPLOAD_PROGRESS_INFO);  
  15.         ps.setBytesRead(pBytesRead);  
  16.         ps.setContentLength(pContentLength);  
  17.         ps.setItemSeq(pItems);  
  18.     }  
  19.   
  20. }  



4)ProgressInfo.java 

Java代码  收藏代码

  1. public class ProgressInfo {  
  2.     private long bytesRead;  
  3.     private long contentLength;  
  4.     private int itemSeq;  
  5.       
  6.     public long getBytesRead() {  
  7.         return bytesRead;  
  8.     }  
  9.     public void setBytesRead(long bytesRead) {  
  10.         this.bytesRead = bytesRead;  
  11.     }  
  12.     public long getContentLength() {  
  13.         return contentLength;  
  14.     }  
  15.     public void setContentLength(long contentLength) {  
  16.         this.contentLength = contentLength;  
  17.     }  
  18.     public int getItemSeq() {  
  19.         return itemSeq;  
  20.     }  
  21.     public void setItemSeq(int itemSeq) {  
  22.         this.itemSeq = itemSeq;  
  23.     }  
  24. }  



5)Controller 

Java代码  收藏代码

  1. @ResponseBody  
  2. @RequestMapping(value = "admin/common/getProgress.do", method = RequestMethod.GET)  
  3. public String getProgress(HttpServletRequest request, HttpServletResponse response) {  
  4.     if (request.getSession().getAttribute(Constants.SESSION_KEY_UPLOAD_PROGRESS_INFO) == null) {  
  5.         return "";  
  6.     }  
  7.     ProgressInfo ps = (ProgressInfo) request.getSession().getAttribute(Constants.SESSION_KEY_UPLOAD_PROGRESS_INFO);  
  8.     Double percent = 0d;  
  9.     if (ps.getContentLength() != 0L) {  
  10.         percent = (double) ps.getBytesRead() / (double) ps.getContentLength() * 1.0d;  
  11.         if (percent != 0d) {  
  12.             DecimalFormat df = new DecimalFormat("0.00");  
  13.             percent = Double.parseDouble(df.format(percent));  
  14.         }  
  15.     }  
  16.     int pp = (int)(percent * 100);  
  17.     return String.valueOf(pp);  
  18. }  



6)JSP 

Html代码  收藏代码

  1. <div class="control-group" id="progressbar" style="display:none;">  
  2.   <div class="progress progress-striped active">  
  3.     <div class="bar" id="progressbardata" style="width: 0;"></div>  
  4.   </div>  
  5. </div>  
  6.   
  7. <script language="javascript">  
  8. function upload() {  
  9.     $('#uploadForm').submit();  
  10.     var interval = setInterval(function() {   
  11.         $.ajax({    
  12.              dataType : "json",  
  13.              url : "<%=request.getContextPath()%>/admin/common/getProgress.do",  
  14.              contentType : "application/json; charset=utf-8",  
  15.              type : "GET",  
  16.              success : function(data, stats) {    
  17.                 if(data) {  
  18.                     $('#progressbar').show();  
  19.                     console.log(data);  
  20.                     if (data == '100') {  
  21.                        clearInterval(interval);  
  22.                     } else {  
  23.                         $('#progressbardata').width(data+'%');  
  24.                     }  
  25.                 }  
  26.              }   
  27.         });  
  28.     }, 100);  
  29. }  
  30. </script>  
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值