当前位置:首页 > PHP > 正文内容

php下载远程文件(支持断点续传,支持超大文件)

高老师6年前 (2019-09-29)PHP2125

断点下载的原理:http请求头添加Range参数告诉文件服务器端需要的字节范围

例如1个文本文件的字节为1000,

第一次请求Range: bytes=0-500

第二次请求Range: bytes=501-1000

通过每次的请求将返回的流追加写入到文件。

注意的项目:断点下载服务器端的每次只返回字节传输的范围的字节流,同时返回的状态码应该为206。

以下是我封装的php下载远程文件,可以通过命令行执行,也可以通过fpm执行,由于web服务器存在执行超时的问题,代码中做了重复执行继续断点下载,超时后再次执行即可,解决大文件下载超时问题。例如我没有服务器,只有虚拟主机,就可以用这个脚本来下载超大文件。

<?php

class download
{
    /**
     * 远程文件的路径
     * @var string
     */
    private $siteUrl = '';

    /**
     * 分片下载大小
     * @var int
     */
    private $burstBytes = 2048;

    /**
     * 设置远程下载文件的路径
     * @param string $url 远程文件的路径
     * @return $this
     */
    public function setUrl($url)
    {
        $this->siteUrl = $url;
        return $this;
    }

    /**
     * 设置分段下载字节大小
     * @param int $byte 字节大小
     * @return $this
     */
    public function setBurst($byte)
    {
        $this->burstBytes = $byte;
        return $this;
    }

    /**
     * 获取远程文件的信息
     * @return array
     * @throws Exception
     */
    private function getSiteFiLeInfo()
    {
        if (!$this->siteUrl)
        {
            throw new Exception('请先设置远程文件url!');
        }
        $responseHeader = get_headers($this->siteUrl, 1);
        if (!$responseHeader)
        {
            throw new Exception('获取远程文件信息失败!');
        }
        if (!empty($responseHeader['Location']))
        {
            //处理文件下载302问题
            $this->siteUrl = $responseHeader['Location'];
            return $this->getSiteFiLeInfo();
        }

        return $responseHeader;
    }

    /**
     * 保存文件到本地
     * @param string $fileName 保存到本地的文件名称
     * @throws
     */
    public function saveFile($fileName)
    {
        //获取远程文件的信息
        $siteFileInfo = $this->getSiteFiLeInfo();
        $siteFileLength = $siteFileInfo['Content-Length'] ?? 0;

        //根据文件是否存在创建文件句柄、计算断点下载开始字节
        $fd = null;
        if (file_exists($fileName))
        {
            $fd = fopen($fileName, 'ab');

        }
        else
        {
            $fd = fopen($fileName, 'wb');
        }
        if (!$fd)
        {
            throw new Exception('创建或打开本地文件失败!');
        }

        //加上文件锁,防止刷新抢占资源句柄
        if (!flock($fd, LOCK_EX | LOCK_NB))
        {
            throw new Exception('已有相关进程操作执行下载本文件!');
        }

        //检查文件是否已经下载完成
        $fileSize = filesize($fileName);
        if ($fileSize && $fileSize >= $siteFileLength)
        {
            throw new Exception('原文件已下载完成,请勿重复下载!');
        }

        //计算断点下载结束字节
        $sByte = $fileSize;
        $eByte = $sByte + $this->burstBytes;

        //循环下载文件
        while (true)
        {
            //文件下载完成
            if ($fileSize >= $siteFileLength)
            {
                fclose($fd);
                break;
            }

            //传递分片范围
            $xRange = "{$sByte}-$eByte";

            //请求curl
            $result = $this->curl($xRange);

            //检查是否正常请求
            $code = $result['code'] ?? 0;
            if (!$code)
            {
                throw new Exception('Http请求异常!');
            }
            if ($code != 206)
            {
                throw new Exception('Http状态码异常,可能不支持断点的资源或已完成下载!');
            }

            //返回流长度
            $streamLength = $result['length'] ?? 0;

            //返回流内容
            $streamContent = $result['stream'] ?? '';
            if ($streamLength > 0)
            {
                file_put_contents('log.txt', $xRange . PHP_EOL, FILE_APPEND);
                $saveRes = fwrite($fd, $streamContent);
                if (!$saveRes)
                {
                    throw new Exception('写入流到文件失败!');
                }
                if ($saveRes != $streamLength)
                {
                    //讲道理这种情况基本不会遇到,除非分段数设置过大,暂时未做兼容处理,重新执行就行
                    throw new Exception('数据异常:返回大小和写入大小不一致!');
                }

                //递增range
                $sByte = $eByte + 1;
                $eByte = $sByte + $this->burstBytes;

                //记录文件大小
                $fileSize = $fileSize + $saveRes;
            }
        }
    }

    /**
     * 获取下载文件流
     * @param string $range 分片字节范围
     * @param array $header Http请求头
     * @return array
     * @throws
     */
    private function curl($range, $header = [])
    {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $this->siteUrl);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
        curl_setopt($ch, CURLOPT_HEADER, TRUE);

        //设置关闭SSL
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);

        //设置分片
        curl_setopt($ch, CURLOPT_RANGE, $range);

        //设置header
        if ($header)
        {
            curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
        }

        //执行请求
        $response = curl_exec($ch);
        if (curl_errno($ch))
        {
            throw new Exception('下载文件异常:' . curl_error($ch));
        }

        //提取response_header和response_body
        $headSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
        $httpHeader = substr($response, 0, $headSize);
        if (!$httpHeader)
        {
            throw new Exception('下载文件异常:未获取到响应头');
        }
        $fileStream = substr($response, $headSize);

        //解析header
        $length = $this->getResHeaderValue('Content-Length', $httpHeader);
        $httpCode = $this->getResHeaderValue('Http-Code', $httpHeader);
        curl_close($ch);

        //返回
        return [
            'code' => $httpCode,
            'length' => $length,
            'stream' => $fileStream,
        ];
    }

    /**
     * 获取响应头某个Key的值
     * @param string $key header头的key
     * @param string $responseHead header头字符串
     * @return string
     */
    private function getResHeaderValue($key, $responseHead)
    {
        $value = '';
        $headArr = explode("\r\n", $responseHead);
        foreach ($headArr as $loop)
        {
            if ($key == 'Http-Code')
            {
                if (preg_match('/HTTP\/1\.[0-9]{1} ([0-9]{3})/', $loop, $matches))
                {
                    return $matches['1'];
                }
            }
            else
            {
                if (strpos($loop, $key) !== false)
                {
                    $value = trim(str_replace($key . ':', '', $loop));
                }
            }
        }

        return $value;
    }

}


//设置下载文件的url
$url = 'https://blog.20230611.cn/demo/1.tar.gz';

//设置分段下载的字节大小
$burst = 4048;

//设置保存到服务器本地的文件名
$filename = '11.tar.gz';

try
{
    //初始化下载器
    $download = new download();

    //开始下载
    $download->setUrl($url)->setBurst($burst)->saveFile($filename);
}
catch (Exception $exception)
{
    var_dump($exception->getMessage());
}

脚本是单进程下载文件,如果你愿意可以修改为curl批量请求,每个请求执行一个范围,保存为多个文件,最后合并文件即可。

扫描二维码推送至手机访问。

版权声明:本文由高久峰个人博客发布,如需转载请注明出处。

本文链接:https://blog.20230611.cn/post/108.html

分享给朋友:

“php下载远程文件(支持断点续传,支持超大文件)” 的相关文章

PHP中Session文件过多的解决方法

PHP中Session文件过多的解决方法

PHP的session文件夹默认保存在同一个文件,随着访客的增多,访问的速度会非常慢,例如部分管理系统后台每天登陆1000次,1个月就创建了30000个session文件,一年后是多少?假设这个系统的用户是上万人呢?方法1:每个网站使用自己的session文件夹,代码如下:<?php $pat...

php 开启错误提示,php 关闭错误提示

php 开启错误提示,php 关闭错误提示

开启错误提示代码:ini_set("display_errors", "On"); error_reporting(E_ALL | E_STRICT);关闭错误提示代码:error_reporting(E_ALL ^&n...

PHP异常处理,PHP自定义错误,PHP记录错误日志

PHP异常处理,PHP自定义错误,PHP记录错误日志

面试中PHP面试官会问调用一个不存在的方法,如何知道是哪个文件哪行调用的?假设方法是getWorkLoad()回答1:开启PHP错误输出,PHP会输出Fatal error: Call to undefined function getWorkLoad() in D:\wwwroot\thinkpa...

redis订阅和发布,redis消息订阅与发布, phpredis订阅和发布

redis订阅和发布,redis消息订阅与发布, phpredis订阅和发布

Redis提供了发布订阅功能,可以用于消息的传输,Redis的发布订阅机制包括三个部分,发布者(publisher),订阅者(subscriber)和频道(channel)。 发布者和订阅者都是Redis客户端,Channel则为Redis服务器端,发布者将消息发送到某个的频道,订阅了这个...

posix_ttyname函数详解

posix_ttyname函数详解

posix_ttyname - 获取当前终端设备名称。<?php     var_dump( posix_ttyname(STDOUT) );我们启动一个终端,执行上面的代码输出:/dev/tty1我们再启动一个终端,执行上面的代码输...

php代理下载,php代下载文件,php下载远程文件,php远程文件下载

php代理下载,php代下载文件,php下载远程文件,php远程文件下载

经常我们下载国外资源容易被墙,可以通过php脚本获取远程文件流然后输出给我们的浏览器来下载。<?php //设置下载文件的url $url = 'https://mirrors.huaweicloud.com/ubuntukylin/ubuntukylin-19....