實現一個視頻播放的功能,以及對大文件的下載操做等等都避不開一個點:獲取文件任意位置的數據,若是說咱們單純的經過 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; } }