根据 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 {
-
-
-
- private static final int DEFAULT_BUFFER_SIZE = 20480;
- private static final long DEFAULT_EXPIRE_TIME = 604800000L;
- private static final String MULTIPART_BOUNDARY = "MULTIPART_BYTERANGES";
-
- @Override
- protected void renderMergedOutputModel(Map 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;
-
-
-
-
- String ifNoneMatch = request.getHeader("If-None-Match");
- if (ifNoneMatch != null && matches(ifNoneMatch, fileName)) {
- response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
- response.setHeader("ETag", eTag);
- response.setDateHeader("Expires", expires);
- return;
- }
-
-
-
- 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);
- response.setDateHeader("Expires", expires);
- return;
- }
-
-
-
-
- String ifMatch = request.getHeader("If-Match");
- if (ifMatch != null && !matches(ifMatch, fileName)) {
- response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
- return;
- }
-
-
- long ifUnmodifiedSince = request.getDateHeader("If-Unmodified-Since");
- if (ifUnmodifiedSince != -1 && ifUnmodifiedSince + 1000 <= lastModified) {
- response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
- return;
- }
-
-
-
-
- Range full = new Range(0, length - 1, length);
- List ranges = new ArrayList();
-
-
- String range = request.getHeader("Range");
- if (range != null) {
-
-
- if (!range.matches("^bytes=\\d*-\\d*(,\\d*-\\d*)*$")) {
- response.setHeader("Content-Range", "bytes */" + length);
- 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");
- if (ifRangeTime != -1 && ifRangeTime + 1000 < lastModified) {
- ranges.add(full);
- }
- } catch (IllegalArgumentException ignore) {
- ranges.add(full);
- }
- }
-
-
- if (ranges.isEmpty()) {
- for (String part : range.substring(6).split(",")) {
-
-
- 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;
- }
-
-
- if (start > end) {
- response.setHeader("Content-Range", "bytes */" + length);
- response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
- return;
- }
-
-
- ranges.add(new Range(start, end, length));
- }
- }
- }
-
-
-
-
- String disposition = "inline";
-
-
-
-
- if (contentType == null) {
- contentType = "application/octet-stream";
- } else if (!contentType.startsWith("image")) {
-
-
- String accept = request.getHeader("Accept");
- disposition = accept != null && accepts(accept, contentType) ? "inline" : "attachment";
- }
-
-
- 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);
-
-
-
-
- RandomAccessFile input = null;
- OutputStream output = null;
-
- try {
-
- input = new RandomAccessFile(file, "r");
- output = response.getOutputStream();
-
- if (ranges.isEmpty() || ranges.get(0) == full) {
-
-
- 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) {
-
-
- 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);
-
-
- copy(input, output, r.start, r.length);
-
- } else {
-
-
- response.setContentType("multipart/byteranges; boundary=" + MULTIPART_BOUNDARY);
- response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
-
-
- ServletOutputStream sos = (ServletOutputStream) output;
-
-
- for (Range r : ranges) {
-
- sos.println();
- sos.println("--" + MULTIPART_BOUNDARY);
- sos.println("Content-Type: " + contentType);
- sos.println("Content-Range: bytes " + r.start + "-" + r.end + "/" + r.total);
-
-
- copy(input, output, r.start, r.length);
- }
-
-
- sos.println();
- sos.println("--" + MULTIPART_BOUNDARY + "--");
- }
- } finally {
- close(output);
- close(input);
- }
-
- }
-
-
-
- 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) {
-
- 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;
- }
-
-
-
- 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;
- }
- }
-
- }
大文件上传时显示进度条
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"/>
- <property name="maxUploadSize" value="52428800"/>
- 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 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>