资讯专栏INFORMATION COLUMN

视频播放、断点续传、多线程下载实现基础:Range

MockingBird / 3776人阅读

摘要:如获取字节的数据和到结尾的数据该头部指定了响应的数据的内容范围,语法格式如下说明数据区间所采用的单位。

实现一个视频播放的功能,以及对大文件的下载操作等等都避不开一个点:获取文件任意位置的数据,如果说我们单纯的通过 echo file-content 的方式只能用于文件下载,如果视频文件用于播放中,则难以处理,具体表现则为视频播放的时候无法调整进度条,而且如果是视频网站,对于视频只采用放在某个可以直接访问的目录上,那么这个视频也就相当于公开了,对于什么 VIP 什么的也就无从说起,本篇文章将 Range,来提供视频播放、断点续传、多线程下载的技术依赖实现

Range

HTTP协议中,支持以 Range 的形式指定获取资源的特定偏移的数据,语法格式如下,具体参考 Range: MDN:

Range: =-
Range: =-
Range: =-, -
Range: =-, -, -

只能是 bytes (目前来说),指定单位

一个整数,表示在特定单位下,范围的起始值。

一个整数,表示在特定单位下,范围的结束值。这个值是可选的,如果不存在,表示此范围一直延伸到文档结束。

如: 获取 0-100 字节的数据和120到结尾的数据

Range: bytes=0-100,120-
Content-Range

该头部指定了响应的数据的内容范围,语法格式如下:

Content-Range:  -/
Content-Range:  -/*
Content-Range:  */

说明:

数据区间所采用的单位。通常是字节(bytes)。

一个整数,表示在给定单位下,区间的起始值。

一个整数,表示在给定单位下,区间的结束值。

整个文件的大小(如果大小未知则用 "*" 表示)

例如:

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 环境下修改一下即可使用。

使用代码:
onRequest($request, $response);
    }
}
依赖代码:
file = $file instanceof SplFileObject? $file : new SplFileObject($file);
        $this->mime = MimeType::getMimeType($this->file->getExtension());
    }

    /**
     * 处理文件请求
     *
     * @param sudaframeworkRequest $request
     * @param sudaframeworkResponse $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 sudaframeworkResponse $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."
");
            $this->sendMultipleRangePart($response, $range);
            $this->sendFileByRange($response, $range);
            $response->write("
");
        }
    }


    /**
     * 发送范围数据
     *
     * @param sudaframeworkResponse $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 sudaframeworkRequest $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 sudaframeworkResponse $response
     * @param array $range
     * @return void
     */
    protected function sendMultipleRangePart(Response $response, array $range)
    {
        $response->write("Content-Type: ".$this->mime."
");
        $response->write("Content-Range: ".$this->getRangeHeader($range) ."

");
    }

    /**
     * 生成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 sudaframeworkRequest $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(
trim($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;
    }
}
参考文献

https://tools.ietf.org/html/r...

https://tools.ietf.org/html/r...

https://developer.mozilla.org...

https://developer.mozilla.org...

完整代码

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/30972.html

相关文章

  • 练习项目备选清单

    摘要:练习项目备选清单文件下载器功能概要设计实现新建下载功能以为基础给出下载链接可以启动下载任务实现局域网内下载传输文件以单线程下载方式实现附加功能支持断点续传实现多线程下载实现下载参考技术套接字编程多线程编程音视频播放器功能概要设计实现播放常见 练习项目备选清单 Utilities 1. 文件下载器 功能概要设计: 实现新建下载功能(以ftp为基础) 给出下载链接可以启动下载任务 实现局...

    guyan0319 评论0 收藏0
  • 练习项目备选清单

    摘要:练习项目备选清单文件下载器功能概要设计实现新建下载功能以为基础给出下载链接可以启动下载任务实现局域网内下载传输文件以单线程下载方式实现附加功能支持断点续传实现多线程下载实现下载参考技术套接字编程多线程编程音视频播放器功能概要设计实现播放常见 练习项目备选清单 Utilities 1. 文件下载器 功能概要设计: 实现新建下载功能(以ftp为基础) 给出下载链接可以启动下载任务 实现局...

    peixn 评论0 收藏0
  • Android Okhttp 断点续传面试解析

    摘要:怎么支持断点续传的协议中默认支持获取文件的部分内容,这其中主要是通过头部的两个参数和来实现的。我们在刷一下面试题的时候,有时候会看到一些大厂会问关于断点续传的原理,那么今天在这里从 HTTP 断点续传知识和 Android 中如何实现断点续传的思路来做一个关于 Android 断点续传原理的总结。 Http 断点续传知识点 什么是断点续传 指的是在上传/下载时,将任务(一个文件或压缩包)人为...

    ACb0y 评论0 收藏0
  • python初学——网络编程之FTP服务器支持并发版本

    摘要:扩展支持多用户并发访问与线程池。项目请见初学网络编程之服务器。不允许超过磁盘配额。该文件是一个使用模块编写的线程池类。这一步就做到了线程池的作用。 对MYFTP项目进行升级。扩展支持多用户并发访问与线程池。MYFTP项目请见python初学——网络编程之FTP服务器。 扩展需求 1.在之前开发的FTP基础上,开发支持多并发的功能2.不能使用SocketServer模块,必须自己实现多线...

    oysun 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<