http 206断点续传

最近客户提出一个在app中播放视频的需求,我们的方案是在app中调用webview实现。结果发现在pc上都没有问题,但在移动平台上视频怎么也播放不了。错误信息如下:

Connection reset. Stacktrace follows:
java.net.SocketException: Connection reset
at java.net.SocketOutputStream.socketWrite(SocketOutputStream.java:118)
at java.net.SocketOutputStream.write(SocketOutputStream.java:159)
at back.tool.DownloadController.processRangeRequest(DownloadController.groovy:84)
at back.tool.DownloadController.index(DownloadController.groovy:48)
at grails.plugin.cache.web.filter.PageFragmentCachingFilter.doFilter(PageFragmentCachingFilter.java:198)
at grails.plugin.cache.web.filter.AbstractFilter.doFilter(AbstractFilter.java:63)
at common.MainController.attachment(MainController.groovy:28)
at grails.plugin.cache.web.filter.PageFragmentCachingFilter.doFilter(PageFragmentCachingFilter.java:198)
at grails.plugin.cache.web.filter.AbstractFilter.doFilter(AbstractFilter.java:63)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
at java.lang.Thread.run(Thread.java:744)
257119 [http-bio-8081-exec-5] ERROR org.codehaus.groovy.grails.web.errors.GrailsExceptionResolver - IllegalStateException occurred when processing request: [GET] /guoanjia/common/main/attachment/test.mp4
getOutputStream() has already been called for this response. Stacktrace follows:
org.codehaus.groovy.grails.web.pages.exceptions.GroovyPagesException: Error processing GroovyPageView: getOutputStream() has already been called for this response
at grails.plugin.cache.web.filter.PageFragmentCachingFilter.doFilter(PageFragmentCachingFilter.java:198)
at grails.plugin.cache.web.filter.AbstractFilter.doFilter(AbstractFilter.java:63)
at common.MainController.attachment(MainController.groovy:28)
at grails.plugin.cache.web.filter.PageFragmentCachingFilter.doFilter(PageFragmentCachingFilter.java:198)
at grails.plugin.cache.web.filter.AbstractFilter.doFilter(AbstractFilter.java:63)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
at java.lang.Thread.run(Thread.java:744)
Caused by: java.lang.IllegalStateException: getOutputStream() has already been called for this response
at java.io.FilterWriter.flush(FilterWriter.java:100)
... 8 more

初步分析报错可能是由于客户端主动断开导致的,这样大概有2种可能:

1、webview版本不支持视频播放

2、服务器不支持相关协议,客户端无法解析服务器返回的数据

对于第一点,我们尝试了许多不同的移动端浏览器,发现播放都有问题,版本的原因基本可以排除。剩下的应该就是服务器返回给客户端的协议格式有问题了。我们之前对于资源(文本、图片等)的下载都是通过程序将其读入内存(安全),写入特定的Content-Type头部,再以流的形式返回给客户端,很有可能这里处理的不到位。

接着当然是去找一个应用服务器,看看他加载视频有没有问题。我们将视频直接扔到tomcat/webapp下面,发现视频正常播放,而且发现了一个特殊的http状态码:206.

查了一些资料,发现对于流媒体,有些平台必须实现Range Requests,下面讲讲http 206 Partial Content。

 

A brief of partial content

The HTTP 206 Partial Content status code and its related headers provide a mechanism which allows browser and other user agents to receive partial content instead of entire one from server. This mechanism is widely used in streaming a video file and supported by most of browsers and players such as Windows Media Player and VLC Player.

The basic workflow could be explained by these following steps:

  1. Browser requests the content.
  2. Server tells browser that the content can be requested partially with Accept-Ranges header.
  3. Browser resends the request, tells server the expecting range with Range header.
  4. Server responses browser in one of following siturations:
    • If range is available, server returns the partial content with status 206 Partial Content. Range of current content will be indicated in Content-Range header.
    • If range is unavailable (for example, greater than total bytes of content), server returns status 416Requested Range Not Satisfiable. The available range will be indicated in Content-Range header too.

Let's take a look at each key header of these steps.

Accept-Ranges: bytes

This is the header which is sent by server, represents the content that can be partially returned to browser. The value indicates the acceptable unit of each range request, usually is bytes in most of situations.

Range: bytes=(start)-(end)

This is the header for browser telling server the expecting range of content. Note that start and end positions are both inclusive and zero-based. This header could be sent without one of them in following meanings:

  • If end position is omitted, server returns the content from indicated start position to the position of last available byte.
  • If start position is omitted, the end position will be described as how many bytes shall server returns counting from the last available byte.
Content-Range: bytes (start)-(end)/(total)

This is the header which shall appear following HTTP status 206. Values start and end represent the range of current content. Like Range header, both values are inclusive and zero-based. Value total indicates the total avaliable bytes.

Content-Range: */(total)

This is same header but in another format and will only be sent following HTTP status 416. Value total also indicates the total avaliable bytes of content.

Here is couple examples of a file with 2048 bytes long. Note the different meaning of end when start is omitted.

 

Request first 1024 bytes

What browser sends:

What server returns:

 

Request without end position

What browser sends:

What server returns:

Note that server does not have to return all remaining bytes in single response especially when content is too long or there are other performance considerations. So following two examples are also acceptable in this case:

Server only returns half of remaining content. The range of next request will start at 1536th byte.

Server only returns 256 bytes of remaining content. The range of next request will start at 1280th byte.

Request last 512 bytes

What browser sends:

What server returns:

 

Request with unavailable range

What browser sends:

What server returns:

 

 source code:

 

资料:

http://www.codeproject.com/Articles/813480/HTTP-Partial-Content-In-Node-js

https://jira.grails.org/browse/GRAILS-11325

https://tools.ietf.org/html/rfc7233#page-12

发表评论