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

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

[b]以下是一个Byte-Range请求的具体HTTP信息:[/b]
[quote]【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[/quote]
详细说明可以参考HTTP/1.1([url=http://www.w3.org/Protocols/rfc2616/rfc2616.html]RFC 2616[/url])的以下部分:
[list][*]3.12 Range Units
[*]14.5 Accept-Ranges
[*]14.16 Content-Range
[*]14.27 If-Range
[*]14.35 Range[/list]
以下是一次请求的处理链:
[b][color=blue](0)Client Request -> (1)Web Server -> (2)Servlet Container -> (3)Web Framework -> (4) Your Code[/color][/b]
[list][*]Web Server: 一般遵循HTTP/1.1 协议都支持Byte-Range请求,比如Apache、Ngnix
[*]Servlet Container:大部分也支持,比如Tomcat的DefaultServlet.java
[*]Web Framework: Spring4.2开始支持,具体可以查看ResourceHttpRequestHandler.java[/list]
截止到第三步都是针对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;
}
}

}


[color=blue][b]通过ProgressListener实现大文件上传时进度条的显示[/b][/color]

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>


参考:
[url=http://stackoverflow.com/questions/28427339/how-to-implement-http-byte-range-requests-in-spring-mvc]How to Implement HTTP byte-range requests in Spring MVC[/url]
[url=http://datum-bits.blogspot.jp/2013/01/implementing-http-byte-range-requests_30.html]Implementing HTTP byte-range requests in Spring MVC[/url]
[url=http://balusc.blogspot.in/2009/02/fileservlet-supporting-resume-and.html]FileServlet supporting resume and caching and GZIP[/url]
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值