实现一个视频播放的功能,以及对大文件的下载操作等等都避不开一个点:获取文件任意位置的数据,如果说我们单纯的通过 echo file-content
的方式只能用于文件下载,如果视频文件用于播放中,则难以处理,具体表现则为视频播放的时候无法调整进度条,而且如果是视频网站,对于视频只采用放在某个可以直接访问的目录上,那么这个视频也就相当于公开了,对于什么 VIP
什么的也就无从说起,本篇文章将 Range
,来提供视频播放、断点续传、多线程下载的技术依赖实现
Range
HTTP协议中,支持以 Range
的形式指定获取资源的特定偏移的数据,语法格式如下,具体参考 Range: MDN:
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>
-
<unit>
只能是bytes
(目前来说),指定单位 -
<range-start>
一个整数,表示在特定单位下,范围的起始值。 -
<range-end>
一个整数,表示在特定单位下,范围的结束值。这个值是可选的,如果不存在,表示此范围一直延伸到文档结束。
如: 获取 0-100
字节的数据和120到结尾的数据
Range: bytes=0-100,120-
Content-Range
该头部指定了响应的数据的内容范围,语法格式如下:
Content-Range: <unit> <range-start>-<range-end>/<size>
Content-Range: <unit> <range-start>-<range-end>/*
Content-Range: <unit> */<size>
说明:
-
<unit>
数据区间所采用的单位。通常是字节(bytes)。 -
<range-start>
一个整数,表示在给定单位下,区间的起始值。 -
<range-end>
一个整数,表示在给定单位下,区间的结束值。 -
<size>
整个文件的大小(如果大小未知则用"*"
表示)
例如:
Content-Range: bytes 200-1000/67589
多Range响应
目测在网络上面的都没有说到,但是HTTP协议支持多Range,具体返回内容信息格式如下:
GET http://suda.dev.dx/file HTTP/1.1
Host: suda.dev.dx
Connection: keep-alive
Accept-Encoding: identity;q=1, *;q=0
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3679.0 Safari/537.36
Accept: */*
Referer: http://test.dev.dx/video.html
Accept-Language: zh-CN,zh;q=0.9
Cookie: php_session=8eec314af63d994c2eeb1baca7487332
Range: bytes=0-1,2-3
HTTP/1.1 206 Partial Content
Date: Sun, 10 Mar 2019 09:36:59 GMT
Server: Apache/2.4.23 (Win32) OpenSSL/1.0.2j mod_fcgid/2.3.9
X-Powered-By: PHP/7.2.1
Accept-Ranges: bytes
Content-Length: 220
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: multipart/byteranges; boundary=multiple_range_ss6bBSB6IlLi0YPpP8rK3g==
--multiple_range_ss6bBSB6IlLi0YPpP8rK3g==
Content-Type: video/mp4
Content-Range: bytes 0-1/132006090
<...somedata...>
--multiple_range_ss6bBSB6IlLi0YPpP8rK3g==
Content-Type: video/mp4
Content-Range: bytes 2-3/132006090
<...somedata...>
Accept-Rangs
服务器响应,告诉浏览器是否支持 Range
,
语法:
Accept-Ranges: bytes
Accept-Ranges: none
- none
不支持任何范围请求单位,由于其等同于没有返回此头部,因此很少使用。不过一些浏览器,比如IE9,会依据该头部去禁用或者移除下载管理器的暂停按钮。 - bytes
范围请求的单位是 bytes (字节)
实现代码
本实现代码可以简单理解为伪代码,部分依赖没有给出,Swoole
环境下修改一下即可使用。
使用代码:
<?php
namespace suda\welcome\response;
use suda\framework\Request;
use suda\framework\Response;
use suda\application\processor\RequestProcessor;
use suda\application\processor\FileRangeProccessor;
class FileResponse implements RequestProcessor
{
public function onRequest(Request $request, Response $response)
{
$filename = 'G:\视频\刺客伍六七.2018\EP01.mp4';
$processor = new FileRangeProccessor($filename);
$processor->onRequest($request, $response);
}
}
依赖代码:
<?php
namespace suda\application\processor;
use SplFileObject;
use suda\framework\Request;
use suda\framework\Response;
use suda\framework\response\MimeType;
use suda\framework\http\stream\DataStream;
use suda\application\processor\RequestProcessor;
/**
* 响应
*/
class FileRangeProccessor implements RequestProcessor
{
/**
* 文件路径
*
* @var SplFileObject
*/
protected $file;
/**
* MIME
*
* @var string
*/
protected $mime;
public function __construct($file)
{
$this->file = $file instanceof SplFileObject? $file : new SplFileObject($file);
$this->mime = MimeType::getMimeType($this->file->getExtension());
}
/**
* 处理文件请求
*
* @param \suda\framework\Request $request
* @param \suda\framework\Response $response
* @return void
*/
public function onRequest(Request $request, Response $response)
{
$ranges = $this->getRanges($request);
$response->setHeader('accept-ranges', 'bytes');
if ($ranges === false || $request->getMethod() !== 'GET') {
$response->status(400);
} elseif ($ranges === null) {
$response->sendFile($this->file->getRealPath());
} elseif (count($ranges) === 1) {
$response->status(206);
$range = $ranges[0];
$response->setHeader('content-type', $this->mime);
$response->setHeader('content-range', $this->getRangeHeader($range));
$this->sendFileByRange($response, $range);
} else {
$response->status(206);
$this->sendMultipleFileByRange($response, $ranges);
}
}
/**
* 发送多Range
*
* @param \suda\framework\Response $response
* @param array $ranges
* @return void
*/
protected function sendMultipleFileByRange(Response $response, array $ranges)
{
$separates = 'multiple_range_'.base64_encode(\md5(\uniqid(), true));
$response->setHeader('content-type', 'multipart/byteranges; boundary='.$separates);
foreach ($ranges as $range) {
$response->write('--'.$separates."\r\n");
$this->sendMultipleRangePart($response, $range);
$this->sendFileByRange($response, $range);
$response->write("\r\n");
}
}
/**
* 发送范围数据
*
* @param \suda\framework\Response $response
* @param array $range
* @return void
*/
protected function sendFileByRange(Response $response, array $range)
{
$response->write(new DataStream($this->file->getRealPath(), $range['start'], $range['end'] - $range['start'] + 1));
}
/**
* 获取Range描述
*
* @param \suda\framework\Request $request
* @return array|bool|null
*/
protected function getRanges(Request $request)
{
$ranges = $this->parseRangeHeader($request);
if (\is_array($ranges)) {
return $this->parseRanges($ranges);
} elseif ($ranges === false) {
return false;
}
return null;
}
/**
* 写Range头
*
* @param \suda\framework\Response $response
* @param array $range
* @return void
*/
protected function sendMultipleRangePart(Response $response, array $range)
{
$response->write('Content-Type: '.$this->mime."\r\n");
$response->write('Content-Range: '.$this->getRangeHeader($range) ."\r\n\r\n");
}
/**
* 生成Range头
*
* @param array $range
* @return string
*/
protected function getRangeHeader(array $range):string
{
return sprintf('bytes %d-%d/%d', $range['start'], $range['end'], $this->file->getSize());
}
/**
* 获取Range描述
*
* @param \suda\framework\Request $request
* @return array|bool|null
*/
protected function parseRangeHeader(Request $request)
{
$range = $request->getHeader('range', null);
if (is_string($range)) {
$range = trim($range);
if (\strpos($range, 'bytes=') !== 0) {
return false;
}
$rangesFrom = \substr($range, strlen('bytes='));
return \explode(',', $rangesFrom);
}
return null;
}
/**
* 处理范围
*
* @param array $ranges
* @return array|bool
*/
protected function parseRanges(array $ranges)
{
$range = [];
foreach ($ranges as $value) {
if (($r = $this->parseRange($value)) !== null) {
$range[] = $r;
} else {
return false;
}
}
return $range;
}
/**
* 处理Range
*
* @param string $range
* @return array
*/
protected function parseRange(string $range):?array
{
$range = trim($range);
if (strrpos($range, '-') === strlen($range) - 1) {
return [
'start' => intval(\rtrim($range, '-')),
'end' => $this->file->getSize() - 1,
];
} elseif (\strpos($range, '-') !== false) {
list($start, $end) = \explode('-', $range, 2);
return ['start' => intval($start) , 'end' => intval($end) ];
}
return null;
}
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。