http range

HTTP RANGE 流程图

<?php
// TODO: 权限判断

// 文件
$file = __DIR__ . '/numbers.txt';
$content_type = 'text/plain';

// 检查是否可读,并得到文件的大小
if (($file_length = filesize($file)) === false)
{
    error_log("Problem reading filesize of $file.");
}

// 解析首部来确定发送响应所需的信息
if (isset($_SERVER['HTTP_RANGE']))
{
    // 定界符不区分大小写
    if ( ! preg_match('/bytes=\d*-\d*(,\d*-\d*)*$/i', $_SERVER['HTTP_RANGE']))
    {
        error_log("Client requestd invalid Range.");
        send_error($file_length);
        exit;
    }
    /*
     * 规范: “客户在一个请求多个字节范围(byte-ranges)时,服务器应当按它们在请求中出现的顺序返回这些范围。”
     */
    $ranges = explode(',', substr($_SERVER['HTTP_RANGE'], 6)); // 字节 = 后面的所有内容
    $offsets = array();
    // 抽取和验证每个部分
    // 只保存通过验证的部分
    foreach($ranges as $range)
    {
        $offset = parse_offset($range, $file_length);
        if ($offset !== false)
        {
            $offsets[] = $offset;
        }
    }

    /*
     * 取决于所请求的合法范围的个数,必须采用不同的格式返回响应
     */
    switch (count($offsets))
    {
        case 0:
            // 非合法范围
            error_log("Client requested no valid ranges.");
            send_error($file_length);
            exit;
            break;
        case 1:
            // 一个合法阀内,发送标准应答
            http_response_code(206); // 部分内容
            list($start, $end) = $offsets[0];
            header("Content-Range: bytes $start-$end/$file_length");
            header("Content-Type: $content_type");

            // 设置变量,从而可以在这里以及下一个情况中重用代码(为什么我觉得这是一个很蠢的注意 ==!)
            // 注意: 0-0为1字节,因为范围包含其两个端点
            $content_length = $end - $start + 1;
            $boundaries = array(0 => '', 1 => '');
            break;
        default:
            // 多个合法范围,发送多部分应答
            http_response_code(206); // 部分应答
            $boundary = str_rand(32); // 分隔各个部分的字符串

            /*
             * 需要计算整个响应的内容长度(Content-Length),不过将整个响应加载到一个字符串中占用大量的内存就,所以使用偏移量计算值。另外利用这个机会计算边界。
             */
            $boundaries = array();
            $content_length = 0;

            foreach ($offsets as $offset)
            {
                list($start, $end) = $offset;

                // 用于分解各个部分
                $boundary_header =
                    "\r\n" .
                    "--$boundary\r\n" .
                    "Content-Type: $content_type\r\n" .
                    "Content-Range: bytes $start - $end / $file_length\r\n" .
                    "\r\n";
                $content_length += strlen($boundary_header) + ($end - $start + 1);
                $boundaries[] = $boundary_header;
            }
            // 增加结束边界
            $boundary_footer = "\r\n--$boundary--";
            $content_length += strlen($boundary_footer);
            $boundaries[] = $boundary_footer;

            // 去除第一个边界中多余的 \r\n
            $boundaries[0] = substr($boundaries[0], 2);
            $content_length -= 2;

            // 改为特殊的多部分内容类型(Content-Type)
            $content_type = "multipart/byteranges; boundary= $boundary";
    }
}
else
{
    // 发送整个文件
    // 设置变量, 就好像这是从 Range 首部抽取的
    $start = 0;
    $end = $file_length - 1;
    $offset = array($start, $end);
    $offsets = array($offset);
    $content_length = $file_length;
    $boundaries = array(0 => '', 1 => '');
}

// 指出得到的是什么
header("Content-Type: $content_type");  // 前面已经指定过了,为什么这里还要指定?
header("Content-Length: $content_length");

// 提供给用户
$handle = fopen($file, 'r');
if ($handle)
{
    $offsets_count = count($offsets);
    // 输出各个定界符和文件适当的部分
    for($i = 0; $i < $offsets_count; $i++)
    {
        echo $boundaries[$i];
        list($start, $end) = $offsets[$i];
        send_range($handle, $start, $end);
    }
    // 结束边界

    echo $boundaries[$i - 1];
    fclose($handle);
}
else
{
    error_log("Error: fopen() fail.");
}

// 在文件中移动适当的位置
// 按块输出所请求的部分
function send_range($handle, $start, $end)
{
    $line_length = 4096; // 魔法数
    if(fseek($handle, $start) === -1)
    {
        error_log("Error:fseek() fail.");
    }
    $left_to_read = $end - $start + 1;
    do{
        $length = min($line_length, $left_to_read);
        if (($buffer = fread($handle, $length)) !== false)
        {
            echo $buffer;
        }
        else
        {
            error_log("Error: fread() fail.");
        }
    }while($left_to_read -= $length);
}

// 发送首部失败
function send_error($file_length)
{
    http_response_code(416);
    header("Content-Range: bytes */$file_length");
}

// 将一个偏移量转换为开始和结束位置,如果偏移量是非法的返回 false
function parse_offset($range, $file_length)
{
    /*
     * 规范: “字节范围(byte-range-spec)中的首字节位置(first-byte-pos)值指定了范围中第一个字节的字节偏移量”
     * "末字节位置(last-byte-pos)值指定了范围中最后一个字节的字节偏移量,也就是说,指定的字节位置包含在范围内"
     */
    list($start, $end) = explode('-', $range);
    /*
     *
     */
    if ($start === '')
    {
        if ($end === '' || $end === 0)
        {
            return false;
        }
        else
        {
            /*
             * 规范: "如果实体比指定的后缀长度(suffix-length)短,则使用整个实体体(entity-body)"
             */
            $start = max(0, $file_length - $end);
            $end = $file_length - 1;
        }
    }
    else
    {
        /*
         * 规范: "如果没有提供末字节位置(last-byte-pos)值,或者如果这个值大于或等于实体体的当前长度,末字节位置则等于实体体当前长度减1"
         */
        if ($end === '' || $end > $file_length - 1)
        {
            $end = $file_length - 1;
        }
        /*
         * 规范: "如果提供了末字节位置值,它必须大于或等于字节阀内规范中的首字节,否则在语法中就是不合法的"
         *
         */
        if ($start > $end)
        {
            return false;
        }
    }
    return array($start, $end);
}

// 生成一个随机字符串来分割响应中的各个部分
function str_rand($length = 32, $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ')
{
    if ( ! is_int($length) || $length < 0)
    {
        return false;
    }
    $characters_length = strlen($characters) - 1;
    $string = '';
    for($i = $length; $i > 0; $i--)
    {
        $string .= $characters[mt_rand(0, $characters_length)];
    }
    return $string;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

胡德咏

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值