实现一个视频播放的功能,以及对大文件的下载操做等等都避不开一个点:获取文件任意位置的数据,若是说咱们单纯的经过 echo file-content
的方式只能用于文件下载,若是视频文件用于播放中,则难以处理,具体表现则为视频播放的时候没法调整进度条,并且若是是视频网站,对于视频只采用放在某个能够直接访问的目录上,那么这个视频也就至关于公开了,对于什么 VIP
什么的也就无从提及,本篇文章将 Range
,来提供视频播放、断点续传、多线程下载的技术依赖实现php
HTTP协议中,支持以 Range
的形式指定获取资源的特定偏移的数据,语法格式以下,具体参考 Range: MDN:html
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到结尾的数据git
Range: bytes=0-100,120-
该头部指定了响应的数据的内容范围,语法格式以下:github
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
目测在网络上面的都没有说到,可是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...>
服务器响应,告诉浏览器是否支持 Range
,session
语法:多线程
Accept-Ranges: bytes Accept-Ranges: none
本实现代码能够简单理解为伪代码,部分依赖没有给出,Swoole
环境下修改一下便可使用。app
<?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; } }