断点下载的原理: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批量请求,每个请求执行一个范围,保存为多个文件,最后合并文件即可。
逛公众号文章看到文章"php实现事件监听与触发的方法,你用过吗?",我就好奇了,php又不是asp.net的webform,哪里来的服务端事件监听。于是学习了一波。先看下监听类:class Event { /** &nbs...
上篇文章已经讲解arrayacces的原理,现在来讲解下arrayaccess的实际应用。一个大型的互联网项目中必然会存在各种配置信息,例如多种数据库信息:mysql,tidb,mongodb,redis,某个业务模块单独的配置信息如比例,额度等等,那么该如何治理配置信息?PHP项目中大部分的框架都...
Redis提供了发布订阅功能,可以用于消息的传输,Redis的发布订阅机制包括三个部分,发布者(publisher),订阅者(subscriber)和频道(channel)。 发布者和订阅者都是Redis客户端,Channel则为Redis服务器端,发布者将消息发送到某个的频道,订阅了这个...
(1).config.php 配置文件<?php /** * RabbitMQ_Config */ $config = [ 'host' => ...
(1)swoole启动的主进程是master进程负责全局管理,然后master进程会再fork一个manager进程。(2)manager进程开始统一管理进程创建回收管理。(3)manager进程根据设置的worker_num和task_worker_num来创建work进程和task进程因此启动s...
今天帮朋友查询wordpress执行超级慢的原因,特此记录开启fpm的慢日志,记录执行超过30秒的脚本request_slowlog_timeout = 30 slowlog = var/log/slow.log查看日志[23-May-2019 17...