案例分析,Mac与iOS客户端无法*放播**视频,服务端的问题?

案例分析,Mac与iOS客户端无法*放播**视频,服务端的问题?

0 - 本案例所涉及的知识点

HTTP协议范围请求允许服务器只返回一部分资源到客户端,范围请求在传送大的媒体文件,或者与文件*载下**的断点续传功能搭配使用时非常有用。

涉及知识点:nginx http协议

1 - 案例概要

前端用户反馈,在Mac或iOS下Safari浏览器无法*放播**视频,但Chrome等浏览器又可以,视频由HTML5的Video标签实现,简化示例代码如下:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>demo</title>
<body>
 <video width="30%" controls="true" x5-playsinline="true" src="http://example.com/demo.mp4"></video>
</body>
</html>

2 - 故障重现

分别通过Safari与Chrome浏览器打开,并同时开启开发者模式:

safari访问情况,视频无法打开:

案例分析,Mac与iOS客户端无法*放播**视频,服务端的问题?

chrome访问情况,视频打开正常:

案例分析,Mac与iOS客户端无法*放播**视频,服务端的问题?

对比之下容易发现发现请求头略有不通之处,主要差别在于Range的值。

safari为:

Range: bytes=0-1

chrome为:

Range: bytes=0-

3 - 初步假设

大概率问题围绕在Range请求头相关。

4 - 理论知识

在HTTP协议请求中,可通过设置请求头:Range,实现范围数据获取,告知服务器返回资源的哪一部分。请求某范围内的资源可以更有效地对大型对象发出请求(分段对其发出请求),或者更有效地从传输错误中恢复(允许客户端请求没有完成的那部分资源)。

在一个Range请求头中,支持三种请求方式:单一范围查询、多重范围查询、条件范围查询。

单一范围查询:

Range: <unit>=<range-start>-
Range: <unit>=<range-start>-<range-end>

多重范围查询:

Range: <unit>=<range-start>-<range-end>, <range-start>-<range-end>
Range: <unit>=<range-start>-<range-end>, <range-start>-<range-end>, <range-start>-<range-end>

条件范围查询:

If-Range: entity-tag
If-Range: HTTP-date
Range: <unit>=<range-start>-<range-end>

条件范围查询需配合If-Range实现,当字段值中的条件得到满足时,Range头字段才会起作用,HTTP-date为资源Last-Modified时间,entity-tag一般设置为资源的md5值(不管Last-Modified或etag,首先需确保资源存在该响应头)。

案例分析,Mac与iOS客户端无法*放播**视频,服务端的问题?

并不是所有服务端都接受范围请求,但很多都可以。服务端可以通过在响应中包含Accept-Ranges请求头向客户端说明可以接受范围请求,这个值在HTTP1规范中只定义了bytes,表示范围的单位是以字节计算的。

Accept-Ranges: bytes 范围请求的单位是 bytes (字节)
Accept-Ranges: none 不支持范围请求,其等同于没有返回此头部,因此很少使用

5 - 疑点提出

  1. 缓存在服务器中的资源请求头是否包含Accept-Ranges,能否支持Range请求?
  2. 如果主动在缓存服务器设置返回Accept-Ranges请求头呢?
  3. 如果主动在真实服务器设置返回Accept-Ranges请求头呢?
  4. 客户端通过范围请求资源,如果缓存服务器没有数据,那缓存服务器的行为是什么?
  5. 如果缓存服务器不存在资源而客户端又是范围请求,缓存的数据会是部分吗?

6 - 实践检验

6.1 - 实验前准备

  • 实验均可通过curl进行模拟,以下关键参数说明
-r, --range RANGE 获取资源数据的范围,从字节哪里到哪里
-I, --head 只获取响应头信息,不捕获响应体,也就是Head请求
  • 缓存在服务器具体路径的计算方式

登陆缓存服务器,查看nginx.conf中的proxy_cache设置

nginx.conf设置示例:

proxy_cache_key $host$uri;
proxy_cache_path /data/nginx/cache levels=1:2 keys_zone=one:10m;

如上所示,假设$host=example.com,uri=/hello/world.jpg,则缓存在本地磁盘可通过如下算法获得:

echo -n 'example.com/hello/world.jpg' | md5sum
d34eacb50650153d77e82aded8de91a0

即存在路径,路径0与1a根据levels设置得出:

/data/nginx/cache/0/1a/d34eacb50650153d77e82aded8de91a0

获取缓存前20行数据(主要是查看后端真实服务器的响应头)

strings /data/nginx/cache/0/1a/d34eacb50650153d77e82aded8de91a0 | head -n 20

手工删除缓存:

rm /data/nginx/cache/0/1a/d34eacb50650153d77e82aded8de91a0 | head -n 20

以上步骤将会在以下实验中不断使用到。

6.1 - 验证疑点1

通过curl命令进行模拟测试:

案例分析,Mac与iOS客户端无法*放播**视频,服务端的问题?

请求的资源字节范围为:1000-2000。结果响应状态码为200,通过Content-Length可以看出,实际返回了整个资源,根据理论知识可知,实际是缓存服务器忽略了来自客户端的Range请求头。

可知缓存服务器并不支持Range请求范围查询。

6.2 - 验证疑点2

通过设置nginx的add_header指令对缓存服务器添加请求头

add_header "Accept-Ranges" bytes;

案例分析,Mac与iOS客户端无法*放播**视频,服务端的问题?

返回了Accept-Ranges请求头,但实际还是不支持Range请求范围查询。

通过缓存服务器,获取缓存文件前20行,如下:

案例分析,Mac与iOS客户端无法*放播**视频,服务端的问题?

实际缓存在服务器中的数据也不包含Accept-Ranges请求头。

6.3 - 验证疑点3

在真实服务器上主动设置返回Accpet-Ranges请求头,同时清除缓存服务器上的缓存数据。

Accept-Ranges: bytes

案例分析,Mac与iOS客户端无法*放播**视频,服务端的问题?

查看缓存服务器上的数据,来自源服务器的响应头:

案例分析,Mac与iOS客户端无法*放播**视频,服务端的问题?

缓存在服务器上的数据包含Accept-Ranges,也就是来自源服务端的响应包含此请求头,此时缓存服务器对客户端是支持Ranges的范围请求了。

6.4 - 验证疑点4、5

由实验3可知,当缓存不存在时,即时客户端为范围请求数据,缓存在本地的数据均为整体,而非部分。

通过nginx官方的ngx_http_proxy_module文档,存在如下关键描述:

If caching is enabled, the header fields “If-Modified-Since”, “If-Unmodified-Since”, “If-None-Match”, “If-Match”, “Range”, and “If-Range” from the original request are not passed to the proxied server.

大意如下:如果设置了缓存,则不会对客户端请求中的“If-Modified-Since”, “If-Unmodified-Since”, “If-None-Match”,“If-Match”, “Range”, “If-Range”这几个请求头传递到后端真实服务器。

所以客户端通过范围请求资源,如果缓存服务器没有数据,那缓存服务器将会去源站获取整个数据并缓存,而不管客户端是否为范围请求。

7 - 问题定位

此时缓存服务器支持Range范围请求之后,在通过浏览器查看效果:

由于担心涉及版权,*放播**效果的图片取消了。

案例分析,Mac与iOS客户端无法*放播**视频,服务端的问题?

此时不在是原先的黑屏,可正常*放播**视频了。

8 - 解决方案

8.1 - 方案1

由源服务器程序实现范围请求功能,返回给缓存服务器时需明确包含Accept-Ranges头,如果值非bytes,为其他自定义类型,则两端均需对该类型的实现。

8.2 - 方案2

如果缓存服务器是基于nginx1.7.7及以上版本,可通过设置proxy_force_ranges参数值为on来实现,默认为off,而不管后端服务是否设置Accept-Ranges请求头。

Syntax: proxy_force_ranges on | off;

Default: proxy_force_ranges off;

Context: http, server, location

This directive appeared in version 1.7.7.

9 - 总结讨论

结合当前业务情况,最终评估通过方案2实现。