`
rensanning
  • 浏览: 3552629 次
  • 性别: Icon_minigender_1
  • 来自: 大连
博客专栏
Efef1dba-f7dd-3931-8a61-8e1c76c3e39f
使用Titanium Mo...
浏览量:38217
Bbab2146-6e1d-3c50-acd6-c8bae29e307d
Cordova 3.x入门...
浏览量:607603
C08766e7-8a33-3f9b-9155-654af05c3484
常用Java开源Libra...
浏览量:682783
77063fb3-0ee7-3bfa-9c72-2a0234ebf83e
搭建 CentOS 6 服...
浏览量:89534
E40e5e76-1f3b-398e-b6a6-dc9cfbb38156
Spring Boot 入...
浏览量:402149
Abe39461-b089-344f-99fa-cdfbddea0e18
基于Spring Secu...
浏览量:69760
66a41a70-fdf0-3dc9-aa31-19b7e8b24672
MQTT入门
浏览量:91835
社区版块
存档分类
最新评论

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

 
阅读更多
根据 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来实现。
ModelAndView mv = new ModelAndView("byteRangeViewRender");
mv.addObject("file", new File("C:\\RenSanNing\\xxx.mp4"));
mv.addObject("contentType", "video/mp4");
return mv;

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

以下是一个写好的View:
public class ByteRangeViewRender extends AbstractView {

    // Constants ----------------------------------------------------------------------------------

    private static final int DEFAULT_BUFFER_SIZE = 20480; // ..bytes = 20KB.
    private static final long DEFAULT_EXPIRE_TIME = 604800000L; // ..ms = 1 week.
    private static final String MULTIPART_BOUNDARY = "MULTIPART_BYTERANGES";

    @Override
    protected void renderMergedOutputModel(Map<String, Object> objectMap,
            HttpServletRequest request, HttpServletResponse response)
            throws Exception {

    	File file = (File) objectMap.get("file");
        if (file == null || !file.exists()) {
            response.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
 
        String contentType = (String) objectMap.get("contentType");
        
        String fileName = file.getName();
        long length = file.length();
        long lastModified = file.lastModified();
        String eTag = fileName + "_" + length + "_" + lastModified;
        long expires = System.currentTimeMillis() + DEFAULT_EXPIRE_TIME;

        // Validate request headers for caching ---------------------------------------------------

        // If-None-Match header should contain "*" or ETag. If so, then return 304.
        String ifNoneMatch = request.getHeader("If-None-Match");
        if (ifNoneMatch != null && matches(ifNoneMatch, fileName)) {
            response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
            response.setHeader("ETag", eTag); // Required in 304.
            response.setDateHeader("Expires", expires); // Postpone cache with 1 week.
            return;
        }

        // If-Modified-Since header should be greater than LastModified. If so, then return 304.
        // This header is ignored if any If-None-Match header is specified.
        long ifModifiedSince = request.getDateHeader("If-Modified-Since");
        if (ifNoneMatch == null && ifModifiedSince != -1 && ifModifiedSince + 1000 > lastModified) {
        	response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
            response.setHeader("ETag", eTag); // Required in 304.
            response.setDateHeader("Expires", expires); // Postpone cache with 1 week.
            return;
        }

        // Validate request headers for resume ----------------------------------------------------

        // If-Match header should contain "*" or ETag. If not, then return 412.
        String ifMatch = request.getHeader("If-Match");
        if (ifMatch != null && !matches(ifMatch, fileName)) {
            response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
            return;
        }

        // If-Unmodified-Since header should be greater than LastModified. If not, then return 412.
        long ifUnmodifiedSince = request.getDateHeader("If-Unmodified-Since");
        if (ifUnmodifiedSince != -1 && ifUnmodifiedSince + 1000 <= lastModified) {
            response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
            return;
        }
        
        // Validate and process range -------------------------------------------------------------

        // Prepare some variables. The full Range represents the complete file.
        Range full = new Range(0, length - 1, length);
        List<Range> ranges = new ArrayList<Range>();

        // Validate and process Range and If-Range headers.
        String range = request.getHeader("Range");
        if (range != null) {

            // Range header should match format "bytes=n-n,n-n,n-n...". If not, then return 416.
            if (!range.matches("^bytes=\\d*-\\d*(,\\d*-\\d*)*$")) {
                response.setHeader("Content-Range", "bytes */" + length); // Required in 416.
                response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
                return;
            }

            String ifRange = request.getHeader("If-Range");
            if (ifRange != null && !ifRange.equals(eTag)) {
                try {
                    long ifRangeTime = request.getDateHeader("If-Range"); // Throws IAE if invalid.
                    if (ifRangeTime != -1 && ifRangeTime + 1000 < lastModified) {
                        ranges.add(full);
                    }
                } catch (IllegalArgumentException ignore) {
                    ranges.add(full);
                }
            }

            // If any valid If-Range header, then process each part of byte range.
            if (ranges.isEmpty()) {
                for (String part : range.substring(6).split(",")) {
                    // Assuming a file with length of 100, the following examples returns bytes at:
                    // 50-80 (50 to 80), 40- (40 to length=100), -20 (length-20=80 to length=100).
                    long start = sublong(part, 0, part.indexOf("-"));
                    long end = sublong(part, part.indexOf("-") + 1, part.length());

                    if (start == -1) {
                        start = length - end;
                        end = length - 1;
                    } else if (end == -1 || end > length - 1) {
                        end = length - 1;
                    }

                    // Check if Range is syntactically valid. If not, then return 416.
                    if (start > end) {
                        response.setHeader("Content-Range", "bytes */" + length); // Required in 416.
                        response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
                        return;
                    }

                    // Add range.                    
                    ranges.add(new Range(start, end, length));
                }
            }
        }

        // Prepare and initialize response --------------------------------------------------------

        // Get content type by file name and set content disposition.
        String disposition = "inline";

        // If content type is unknown, then set the default value.
        // For all content types, see: http://www.w3schools.com/media/media_mimeref.asp
        // To add new content types, add new mime-mapping entry in web.xml.
        if (contentType == null) {
            contentType = "application/octet-stream";
        } else if (!contentType.startsWith("image")) {
            // Else, expect for images, determine content disposition. If content type is supported by
            // the browser, then set to inline, else attachment which will pop a 'save as' dialogue.
            String accept = request.getHeader("Accept");
            disposition = accept != null && accepts(accept, contentType) ? "inline" : "attachment";
        }

        // Initialize response.
        response.reset();
        response.setBufferSize(DEFAULT_BUFFER_SIZE);
        response.setHeader("Content-Disposition", disposition + ";filename=\"" + fileName + "\"");
        response.setHeader("Accept-Ranges", "bytes");
        response.setHeader("ETag", eTag);
        response.setDateHeader("Last-Modified", lastModified);
        response.setDateHeader("Expires", expires);

        // Send requested file (part(s)) to client ------------------------------------------------

        // Prepare streams.
        RandomAccessFile input = null;
        OutputStream output = null;

        try {
            // Open streams.
            input = new RandomAccessFile(file, "r");
            output = response.getOutputStream();

            if (ranges.isEmpty() || ranges.get(0) == full) {

                // Return full file.
                Range r = full;
                response.setContentType(contentType);
                response.setHeader("Content-Range", "bytes " + r.start + "-" + r.end + "/" + r.total);
                response.setHeader("Content-Length", String.valueOf(r.length));
                
                copy(input, output, r.start, r.length);
                
            } else if (ranges.size() == 1) {

                // Return single part of file.
                Range r = ranges.get(0);
                response.setContentType(contentType);
                response.setHeader("Content-Range", "bytes " + r.start + "-" + r.end + "/" + r.total);
                response.setHeader("Content-Length", String.valueOf(r.length));
                response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206.

                // Copy single part range.
                copy(input, output, r.start, r.length);

            } else {

                // Return multiple parts of file.
                response.setContentType("multipart/byteranges; boundary=" + MULTIPART_BOUNDARY);
                response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206.

                // Cast back to ServletOutputStream to get the easy println methods.
                ServletOutputStream sos = (ServletOutputStream) output;

                // Copy multi part range.
                for (Range r : ranges) {
                    // Add multipart boundary and header fields for every range.
                    sos.println();
                    sos.println("--" + MULTIPART_BOUNDARY);
                    sos.println("Content-Type: " + contentType);
                    sos.println("Content-Range: bytes " + r.start + "-" + r.end + "/" + r.total);

                    // Copy single part range of multi part range.
                    copy(input, output, r.start, r.length);
                }

                // End with multipart boundary.
                sos.println();
                sos.println("--" + MULTIPART_BOUNDARY + "--");
            }
        } finally {
            close(output);
            close(input);
        }

    }
    
    // Helpers (can be refactored to public utility class) ----------------------------------------

    private void close(Closeable resource) {
        if (resource != null) {
            try {
                resource.close();
            } catch (IOException ignore) {
            }
        }
    }
    
    private void copy(RandomAccessFile input, OutputStream output, long start, long length) throws IOException {
        byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
        int read;

        try {
	        if (input.length() == length) {
	            // Write full range.
	            while ((read = input.read(buffer)) > 0) {
	                output.write(buffer, 0, read);
	            }
	        } else {
	            input.seek(start);
	            long toRead = length;
	
	            while ((read = input.read(buffer)) > 0) {
	                if ((toRead -= read) > 0) {
	                    output.write(buffer, 0, read);
	                } else {
	                    output.write(buffer, 0, (int) toRead + read);
	                    break;
	                }
	            }
	        }
        } catch (IOException ignore) {
        }
    }
    
    private long sublong(String value, int beginIndex, int endIndex) {
        String substring = value.substring(beginIndex, endIndex);
        return (substring.length() > 0) ? Long.parseLong(substring) : -1;
    } 

    private boolean accepts(String acceptHeader, String toAccept) {
        String[] acceptValues = acceptHeader.split("\\s*(,|;)\\s*");
        Arrays.sort(acceptValues);
        return Arrays.binarySearch(acceptValues, toAccept) > -1
            || Arrays.binarySearch(acceptValues, toAccept.replaceAll("/.*$", "/*")) > -1
            || Arrays.binarySearch(acceptValues, "*/*") > -1;
    }

    private boolean matches(String matchHeader, String toMatch) {
        String[] matchValues = matchHeader.split("\\s*,\\s*");
        Arrays.sort(matchValues);
        return Arrays.binarySearch(matchValues, toMatch) > -1
            || Arrays.binarySearch(matchValues, "*") > -1;
    }

    // Inner classes ------------------------------------------------------------------------------

    protected class Range {
        long start;
        long end;
        long length;
        long total;

        public Range(long start, long end, long total) {
            this.start = start;
            this.end = end;
            this.length = end - start + 1;
            this.total = total;
        }
    }
    
}


通过ProgressListener实现大文件上传时进度条的显示

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


2)CustomMultipartResolver.java
public class CustomMultipartResolver extends CommonsMultipartResolver {
	
	@Autowired
    private CustomProgressListener progressListener;
     
    public void setFileUploadProgressListener(CustomProgressListener progressListener){
        this.progressListener = progressListener;
    }
    
	public void setFileSizeMax(long fileSizeMax) {
	    getFileUpload().setFileSizeMax(fileSizeMax);
	}
  
    @Override
    public MultipartParsingResult parseRequest(HttpServletRequest request) throws MultipartException {
        String encoding = determineEncoding(request);
        FileUpload fileUpload = prepareFileUpload(encoding);
        
        progressListener.setSession(request.getSession());
        fileUpload.setProgressListener(progressListener);
        
        try {
            List<FileItem> fileItems = ((ServletFileUpload) fileUpload).parseRequest(request);
            return parseFileItems(fileItems, encoding);
        }
        catch (FileUploadBase.SizeLimitExceededException ex) {
            throw new MaxUploadSizeExceededException(fileUpload.getSizeMax(), ex);
        }
	    catch (FileUploadBase.FileSizeLimitExceededException ex) {
	        throw new MaxUploadSizeExceededException(fileUpload.getFileSizeMax(), ex);
	    }
        catch (FileUploadException ex) {
            throw new MultipartException("Could not parse multipart servlet request", ex);
        }
    }

}


3)CustomProgressListener.java
@Component
public class CustomProgressListener implements ProgressListener {
	
	private HttpSession session;  
 
    public void setSession(HttpSession session){
        this.session = session;
        ProgressInfo ps = new ProgressInfo();
        this.session.setAttribute(Constants.SESSION_KEY_UPLOAD_PROGRESS_INFO, ps);
    }
    
    @Override
    public void update(long pBytesRead, long pContentLength, int pItems) {
        ProgressInfo ps = (ProgressInfo) session.getAttribute(Constants.SESSION_KEY_UPLOAD_PROGRESS_INFO);
        ps.setBytesRead(pBytesRead);
        ps.setContentLength(pContentLength);
        ps.setItemSeq(pItems);
    }

}


4)ProgressInfo.java
public class ProgressInfo {
	private long bytesRead;
    private long contentLength;
    private int itemSeq;
    
	public long getBytesRead() {
		return bytesRead;
	}
	public void setBytesRead(long bytesRead) {
		this.bytesRead = bytesRead;
	}
	public long getContentLength() {
		return contentLength;
	}
	public void setContentLength(long contentLength) {
		this.contentLength = contentLength;
	}
	public int getItemSeq() {
		return itemSeq;
	}
	public void setItemSeq(int itemSeq) {
		this.itemSeq = itemSeq;
	}
}


5)Controller
@ResponseBody
@RequestMapping(value = "admin/common/getProgress.do", method = RequestMethod.GET)
public String getProgress(HttpServletRequest request, HttpServletResponse response) {
    if (request.getSession().getAttribute(Constants.SESSION_KEY_UPLOAD_PROGRESS_INFO) == null) {
        return "";
    }
    ProgressInfo ps = (ProgressInfo) request.getSession().getAttribute(Constants.SESSION_KEY_UPLOAD_PROGRESS_INFO);
    Double percent = 0d;
    if (ps.getContentLength() != 0L) {
        percent = (double) ps.getBytesRead() / (double) ps.getContentLength() * 1.0d;
        if (percent != 0d) {
            DecimalFormat df = new DecimalFormat("0.00");
            percent = Double.parseDouble(df.format(percent));
        }
    }
    int pp = (int)(percent * 100);
    return String.valueOf(pp);
}


6)JSP
<div class="control-group" id="progressbar" style="display:none;">
  <div class="progress progress-striped active">
    <div class="bar" id="progressbardata" style="width: 0;"></div>
  </div>
</div>

<script language="javascript">
function upload() {
    $('#uploadForm').submit();
    var interval = setInterval(function() { 
        $.ajax({  
             dataType : "json",
             url : "<%=request.getContextPath()%>/admin/common/getProgress.do",
             contentType : "application/json; charset=utf-8",
             type : "GET",
             success : function(data, stats) {  
                if(data) {
                    $('#progressbar').show();
                    console.log(data);
                    if (data == '100') {
                       clearInterval(interval);
                    } else {
                        $('#progressbardata').width(data+'%');
                    }
                }
             } 
        });
    }, 100);
}
</script>


参考:
How to Implement HTTP byte-range requests in Spring MVC
Implementing HTTP byte-range requests in Spring MVC
FileServlet supporting resume and caching and GZIP
1
2
分享到:
评论
2 楼 ausit 2015-12-13  
请问,函数在getFileUpload().setFileSizeMax(fileSizeMax);
在哪里 ?能分享个能跑的demo么,谢谢
1 楼 lis1314 2015-10-28  
mark!

相关推荐

    spring boot实现大文件断点续传

    通过以上步骤,我们就能在Spring Boot应用中实现大文件的断点续传功能。这种功能的实现不仅可以提高用户满意度,还能减少服务器带宽和存储资源的浪费。在实际项目中,还需要考虑安全性、性能优化等方面的问题,例如...

    spring boot + plupload实现大文件断点续传

    综上所述,通过Spring Boot和plupload的结合,我们可以实现大文件的断点续传功能,提高文件上传的稳定性和效率。在实际项目中,还需要考虑错误处理、安全性(如防止重复上传、文件覆盖)以及性能优化等问题。这个名...

    webuploader MVC 切片上传-断点续传-秒传Demo

    以上就是“webuploader MVC 切片上传-断点续传-秒传Demo”的核心知识点,这种技术不仅提高了大文件上传的效率,还增强了系统的稳定性和用户体验。在实际项目中,结合WebUploader与其他MVC框架,如ASP.NET MVC、...

    支持断点续传的下载进度对话框

    断点续传技术是现代网络下载中非常关键的一项功能,尤其在大文件传输时更为重要。这个技术允许用户中断下载后,在同一位置继续下载,而无需重新开始,从而节省了大量的时间和带宽资源。在安卓系统中,实现断点续传...

    Springboot 断点续传大文件

    曾经没有上传过:后台返回前端,表示从未上传过此文件,前端通过技术,将大文件分隔成无数个小文件,一一上传 曾经上传一部分:前端在上传每个模块之前,先请求后台,判断此模块是否已经上传过,如果已经上传过,...

    Java实现断点续传

    Java实现断点续传是一项在文件传输中非常实用的技术,特别是在大文件传输或者网络不稳定的情况下。断点续传允许用户在文件传输中断后从上次中断的位置继续,而不是重新开始整个传输过程,极大地提高了效率和用户体验...

    springboot+vue 大文件上传 包括断点续传 秒传 分片上传.zip

    本项目"springboot+vue 大文件上传 包括断点续传 秒传 分片上传.zip"提供了一套完整的解决方案,针对大文件上传进行了优化,确保了上传的高效性和可靠性。 首先,我们来看SpringBoot的部分。SpringBoot是基于Spring...

    springboot整合plupload,实现文件批量上传、断点续传、秒传 源码

    在本文中,我们将深入探讨如何使用SpringBoot框架与Plupload工具进行集成,以实现文件的批量上传、断点续传和秒传功能。这个项目基于SpringBoot 2和Plupload 2.3.6,提供了直观的上传进度条,并且是在IntelliJ IDEA...

    基于SpringBoot的文件上传系统,前后端分离,单文件上传,多文件上传,大文件上传,断点续传,文件秒传,图片上传

    基于SpringBoot的文件上传系统,前后端分离,单文件上传,多文件上传,大文件上传,断点续传,文件秒传,图片上传 项目经过严格测试,确保可以运行! 采用前后端分离的方式进行开发,实现了几种常用的文件上传功能...

    大文件上传支持断点续传springboot版

    在IT行业中,大文件上传和断点续传是常见的需求,尤其在云存储、文件分享以及数据备份等场景。SpringBoot作为一个轻量级的Java框架,因其简洁的配置和强大的功能,被广泛应用在各种项目中。本项目"大文件上传支持...

    MinIO文件切片(断点续传)

    Spring Boot结合MinIO实现的大文件分片上传技术,通过将大文件分割成小片段并并行上传,有效提升了文件上传速度和稳定性。该技术利用了Spring Boot框架的强大功能和MinIO对象存储的高效性,使得大文件的上传过程更加...

    SpringBoot分片上传Excel大文件,支持断点续传,EasyExcel处理百万级数据

    前端Excel大文件file slice分片,md5校验文件完整性并作文件标识记录写入数据库,支持断点续传。文件上传完毕后,使用EasyExcel读取文件流,处理Excel数据写入数据库中,可处理百万级数据。项目完整,连接数据库即可...

    fileUploadDemo-master_大文件上传_springboot_断点续传_

    在IT行业中,大文件上传和断点续传是常见的需求,尤其是在处理用户生成内容或大数据交换的场景。本文将深入探讨如何使用Webupload与SpringBoot框架实现大文件上传及断点续传功能。 首先,SpringBoot是Java领域的一...

    java实现大文件上传分片上传断点续传.zip

    本项目"java实现大文件上传分片上传断点续传.zip"提供了一个基于SpringBoot框架的解决方案,它实现了大文件的分片上传和断点续传功能。以下是关于这个项目的关键知识点的详细说明: 1. **大文件上传**: - 大文件...

    spring mvc上传文件

    在本文中,我们将深入探讨如何使用Spring MVC框架与Ajax技术结合来实现文件上传的功能。Spring MVC是Spring框架的一部分,提供了一种模型-视图-控制器(MVC)架构模式,用于构建Web应用程序。Ajax(Asynchronous ...

    java 文件上传下载 断点续传 断点上传

    断点续传功能则进一步提高了文件传输的效率和用户体验,尤其是在处理大文件时。本项目提供了两种实现方式,一种是基于Web端的上传下载,另一种是通过Java接口实现的断点续传上传。 1. **Web端上传下载**: - 这种...

    精通Spring MVC 4

    本书共计10章,分别介绍了快速搭建Spring Web应用、精通MVC结构、URL映射、文件上传与错误处理、创建Restful应用、保护应用、单元测试与验收测试、优化请求、将Web应用部署到云等内容,循序渐进地讲解了Spring MVC4...

    spring mvc文件上传实现进度条

    这个场景通常涉及到前端的JavaScript或jQuery库(如jQuery File Upload)与后端的Spring MVC控制器之间的交互,以及可能的多线程处理来跟踪文件上传的进度。接下来,我们将深入探讨如何在Spring MVC中实现这一功能。...

    springMVC框架下plUpload断点续传

    在Spring MVC框架中,plUpload是一个非常实用的前端文件上传插件,尤其适用于处理大文件,如视频和图片的断点续传。该插件支持多浏览器兼容,提供了丰富的功能和自定义选项,使得文件上传体验更加友好。下面将详细...

Global site tag (gtag.js) - Google Analytics