10分钟上手Guzzle批量下载:断点续传+实时进度条完整指南
2026-02-05 05:10:08作者:羿妍玫Ivan
你是否还在为批量文件下载中断后需重新开始而烦恼?是否需要给用户展示直观的下载进度?本文将通过Guzzle实现企业级批量下载功能,包含断点续传、实时进度条和并发控制,让你10分钟内掌握专业下载解决方案。
读完本文你将获得:
- 基于Guzzle Pool的并发下载实现
- 断点续传核心代码与文件校验机制
- 文本进度条与百分比实时显示
- 错误处理与任务重试策略
批量下载架构设计
Guzzle作为PHP生态最流行的HTTP客户端,提供了完善的异步请求和并发控制能力。批量下载功能主要基于以下核心组件构建:
- 并发请求管理:src/Pool.php实现请求池管理,默认支持25个并发连接,可通过
concurrency参数调整 - 流式传输:src/Handler/StreamHandler.php提供底层流处理,支持断点续传
- 进度追踪:StreamHandler的
progress回调函数可实时获取传输状态 - 文件系统交互:结合PHP流操作实现断点续传的文件写入
graph TD
A[任务队列] -->|迭代生成| B[Guzzle Pool]
B -->|并发控制| C[StreamHandler]
C --> D{断点检测}
D -->|已存在文件| E[获取文件大小]
D -->|新文件| F[创建空文件]
E --> G[设置Range请求头]
F --> H[发起完整请求]
G --> I[接收数据块]
H --> I
I --> J[写入临时文件]
J --> K[进度条更新]
K --> L{下载完成?}
L -->|是| M[重命名临时文件]
L -->|否| I
M --> N[任务完成]
核心功能实现
1. 并发请求池配置
使用Guzzle Pool组件创建请求池,设置并发数和任务回调:
use GuzzleHttp\Client;
use GuzzleHttp\Pool;
use GuzzleHttp\Psr7\Request;
$client = new Client();
$urls = [/* 下载URL列表 */];
// 创建请求生成器
$requests = function ($urls) use ($client) {
foreach ($urls as $url) {
$savePath = '/path/to/save/' . basename($url);
yield function () use ($client, $url, $savePath) {
return $client->getAsync($url, [
'sink' => $savePath,
'progress' => function ($downloadTotal, $downloaded) use ($savePath) {
// 进度回调处理
}
]);
};
}
};
// 配置请求池
$pool = new Pool($client, $requests($urls), [
'concurrency' => 5, // 并发数
'fulfilled' => function ($response, $index) {
// 成功回调
},
'rejected' => function ($reason, $index) {
// 失败回调
},
]);
// 执行并等待完成
$promise = $pool->promise();
$promise->wait();
2. 断点续传实现
断点续传核心是通过HTTP Range头实现部分下载,结合文件系统操作实现断点记录:
function getResumeOptions($savePath) {
$options = [];
if (file_exists($savePath)) {
$fileSize = filesize($savePath);
if ($fileSize > 0) {
// 设置Range请求头,从已下载字节处继续
$options['headers'] = [
'Range' => "bytes=$fileSize-"
];
// 使用临时文件存储新下载内容
$options['sink'] = $savePath . '.part';
}
}
return $options;
}
// 在请求回调中使用
$options = getResumeOptions($savePath);
return $client->getAsync($url, $options);
下载完成后合并文件:
function mergePartialFile($savePath) {
$partFile = $savePath . '.part';
if (file_exists($partFile)) {
$handle = fopen($savePath, 'ab');
$partHandle = fopen($partFile, 'rb');
stream_copy_to_stream($partHandle, $handle);
fclose($handle);
fclose($partHandle);
unlink($partFile);
}
}
3. 进度条显示
利用StreamHandler的progress回调实现文本进度条:
$progressCallback = function ($downloadTotal, $downloaded, $uploadTotal, $uploaded) use ($savePath) {
static $lastProgress = 0;
if ($downloadTotal && $downloaded) {
$percent = round(($downloaded / $downloadTotal) * 100, 2);
// 每1%更新一次,减少IO操作
if ($percent - $lastProgress >= 1 || $percent == 100) {
$lastProgress = $percent;
// 文本进度条
$barLength = 50;
$filledLength = (int)($barLength * $percent / 100);
$bar = str_repeat('=', $filledLength) . str_repeat(' ', $barLength - $filledLength);
// 格式化文件大小
$downloadedSize = formatBytes($downloaded);
$totalSize = formatBytes($downloadTotal);
// 输出进度信息(覆盖当前行)
echo "\rDownloading $savePath: [$bar] $percent% ($downloadedSize/$totalSize)";
if ($percent == 100) echo "\n";
}
}
};
// 文件大小格式化辅助函数
function formatBytes($bytes, $precision = 2) {
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
return round($bytes / pow(1024, $pow), $precision) . ' ' . $units[$pow];
}
错误处理与优化
任务重试机制
使用Guzzle的重试中间件处理临时网络错误:
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use GuzzleHttp\RetryMiddleware;
$stack = HandlerStack::create();
// 添加重试中间件
$stack->push(Middleware::retry(
function ($retries, $request, $response, $exception) {
// 最多重试3次,仅对5xx错误和网络异常重试
return $retries < 3
&& ($exception instanceof ConnectException
|| ($response && $response->getStatusCode() >= 500));
},
// 指数退避策略
function ($retries) {
return (int)pow(2, $retries) * 1000;
}
));
$client = new Client(['handler' => $stack]);
下载任务状态管理
创建任务状态跟踪类,记录每个下载的进度和状态:
class DownloadManager {
private $statusFile;
private $status = [];
public function __construct($statusFile) {
$this->statusFile = $statusFile;
$this->loadStatus();
}
// 加载已保存的状态
private function loadStatus() {
if (file_exists($this->statusFile)) {
$this->status = json_decode(file_get_contents($this->statusFile), true) ?? [];
}
}
// 更新任务状态
public function updateStatus($url, $status, $progress = 0) {
$this->status[$url] = [
'status' => $status, // 'pending', 'downloading', 'completed', 'failed'
'progress' => $progress,
'updated_at' => time()
];
$this->saveStatus();
}
// 保存状态到文件
private function saveStatus() {
file_put_contents($this->statusFile, json_encode($this->status, JSON_PRETTY_PRINT));
}
// 获取未完成的任务
public function getPendingTasks() {
return array_filter($this->status, function($task) {
return $task['status'] !== 'completed';
});
}
}
完整代码示例
以下是整合所有功能的完整示例:
<?php
require 'vendor/autoload.php';
use GuzzleHttp\Client;
use GuzzleHttp\Pool;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use GuzzleHttp\Psr7\Request;
class BatchDownloader {
private $client;
private $downloadDir;
private $concurrency;
private $manager;
public function __construct($downloadDir = './downloads', $concurrency = 5) {
$this->downloadDir = rtrim($downloadDir, '/');
$this->concurrency = $concurrency;
$this->manager = new DownloadManager($this->downloadDir . '/status.json');
// 创建下载目录
if (!is_dir($this->downloadDir)) {
mkdir($this->downloadDir, 0755, true);
}
// 配置带重试机制的客户端
$stack = HandlerStack::create();
$stack->push(Middleware::retry(
function ($retries, $request, $response, $exception) {
return $retries < 3
&& ($exception instanceof ConnectException
|| ($response && $response->getStatusCode() >= 500));
},
function ($retries) {
return (int)pow(2, $retries) * 1000;
}
));
$this->client = new Client(['handler' => $stack]);
}
public function download($urls) {
$requests = function ($urls) {
foreach ($urls as $url) {
$filename = basename(parse_url($url, PHP_URL_PATH));
$savePath = $this->downloadDir . '/' . $filename;
// 跳过已完成的文件
if (isset($this->manager->getPendingTasks()[$url]['status'])
&& $this->manager->getPendingTasks()[$url]['status'] === 'completed') {
continue;
}
$this->manager->updateStatus($url, 'pending');
yield function () use ($url, $savePath) {
return $this->createRequest($url, $savePath);
};
}
};
$pool = new Pool($this->client, $requests($urls), [
'concurrency' => $this->concurrency,
'fulfilled' => function ($response, $index) use ($urls) {
$url = $urls[$index];
$this->manager->updateStatus($url, 'completed', 100);
echo "Completed: $url\n";
},
'rejected' => function ($reason, $index) use ($urls) {
$url = $urls[$index];
$this->manager->updateStatus($url, 'failed');
echo "Failed: $url - " . $reason->getMessage() . "\n";
},
]);
$promise = $pool->promise();
$promise->wait();
echo "All downloads completed!\n";
}
private function createRequest($url, $savePath) {
$options = $this->getResumeOptions($savePath);
$options['progress'] = function ($downloadTotal, $downloaded) use ($url) {
$progress = $downloadTotal ? round(($downloaded / $downloadTotal) * 100, 2) : 0;
$this->manager->updateStatus($url, 'downloading', $progress);
// 显示进度条
$this->showProgress($url, $downloadTotal, $downloaded);
};
return $this->client->getAsync($url, $options)
->then(function ($response) use ($savePath) {
// 合并临时文件(如果存在)
$this->mergePartialFile($savePath);
return $response;
});
}
private function getResumeOptions($savePath) {
$options = ['sink' => $savePath];
if (file_exists($savePath)) {
$fileSize = filesize($savePath);
if ($fileSize > 0) {
$options['headers']['Range'] = "bytes=$fileSize-";
$options['sink'] = $savePath . '.part';
}
}
return $options;
}
private function mergePartialFile($savePath) {
$partFile = $savePath . '.part';
if (file_exists($partFile)) {
$handle = fopen($savePath, 'ab');
$partHandle = fopen($partFile, 'rb');
stream_copy_to_stream($partHandle, $handle);
fclose($handle);
fclose($partHandle);
unlink($partFile);
}
}
private function showProgress($url, $total, $downloaded) {
$filename = basename($url);
$totalSize = $this->formatBytes($total);
$downloadedSize = $this->formatBytes($downloaded);
$percent = $total ? round(($downloaded / $total) * 100, 2) : 0;
$barLength = 50;
$filledLength = (int)($barLength * $percent / 100);
$bar = str_repeat('=', $filledLength) . str_repeat(' ', $barLength - $filledLength);
echo "\r[$bar] $percent% ($downloadedSize/$totalSize) - $filename";
if ($percent == 100) echo "\n";
}
private function formatBytes($bytes, $precision = 2) {
if ($bytes === 0) return '0 B';
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$pow = floor(log($bytes, 1024));
return round($bytes / pow(1024, $pow), $precision) . ' ' . $units[$pow];
}
}
// 使用示例
$downloader = new BatchDownloader('/path/to/downloads', 5);
$downloadUrls = [
'https://example.com/large-file1.zip',
'https://example.com/large-file2.iso',
// 添加更多URL...
];
$downloader->download($downloadUrls);
扩展功能建议
- 任务优先级:修改请求生成器,根据文件大小或重要性排序请求
- 速度限制:添加流量控制中间件,限制总下载带宽
- 校验机制:下载完成后通过MD5或SHA校验文件完整性
- Web界面:结合前端框架创建可视化管理界面,使用WebSocket实时更新进度
- 邮件通知:任务完成后发送邮件通知,包含下载报告
通过本文介绍的方法,你可以构建一个功能完善、健壮可靠的批量下载系统。Guzzle的灵活架构和强大功能让复杂的HTTP操作变得简单,无论是构建企业级下载工具还是简单的批量获取脚本,都能满足需求。
完整实现可参考src/Pool.php和src/Handler/StreamHandler.php的源代码,了解底层实现细节以进行更高级的定制。
登录后查看全文
热门项目推荐
相关项目推荐
GLM-5智谱 AI 正式发布 GLM-5,旨在应对复杂系统工程和长时域智能体任务。Jinja00
GLM-5-w4a8GLM-5-w4a8基于混合专家架构,专为复杂系统工程与长周期智能体任务设计。支持单/多节点部署,适配Atlas 800T A3,采用w4a8量化技术,结合vLLM推理优化,高效平衡性能与精度,助力智能应用开发Jinja00
请把这个活动推给顶尖程序员😎本次活动专为懂行的顶尖程序员量身打造,聚焦AtomGit首发开源模型的实际应用与深度测评,拒绝大众化浅层体验,邀请具备扎实技术功底、开源经验或模型测评能力的顶尖开发者,深度参与模型体验、性能测评,通过发布技术帖子、提交测评报告、上传实践项目成果等形式,挖掘模型核心价值,共建AtomGit开源模型生态,彰显顶尖程序员的技术洞察力与实践能力。00
Kimi-K2.5Kimi K2.5 是一款开源的原生多模态智能体模型,它在 Kimi-K2-Base 的基础上,通过对约 15 万亿混合视觉和文本 tokens 进行持续预训练构建而成。该模型将视觉与语言理解、高级智能体能力、即时模式与思考模式,以及对话式与智能体范式无缝融合。Python00
MiniMax-M2.5MiniMax-M2.5开源模型,经数十万复杂环境强化训练,在代码生成、工具调用、办公自动化等经济价值任务中表现卓越。SWE-Bench Verified得分80.2%,Multi-SWE-Bench达51.3%,BrowseComp获76.3%。推理速度比M2.1快37%,与Claude Opus 4.6相当,每小时仅需0.3-1美元,成本仅为同类模型1/10-1/20,为智能应用开发提供高效经济选择。【此简介由AI生成】Python00
Qwen3.5Qwen3.5 昇腾 vLLM 部署教程。Qwen3.5 是 Qwen 系列最新的旗舰多模态模型,采用 MoE(混合专家)架构,在保持强大模型能力的同时显著降低了推理成本。00- RRing-2.5-1TRing-2.5-1T:全球首个基于混合线性注意力架构的开源万亿参数思考模型。Python00
项目优选
收起
deepin linux kernel
C
27
11
OpenHarmony documentation | OpenHarmony开发者文档
Dockerfile
569
3.84 K
🔥LeetCode solutions in any programming language | 多种编程语言实现 LeetCode、《剑指 Offer(第 2 版)》、《程序员面试金典(第 6 版)》题解
Java
68
20
Nop Platform 2.0是基于可逆计算理论实现的采用面向语言编程范式的新一代低代码开发平台,包含基于全新原理从零开始研发的GraphQL引擎、ORM引擎、工作流引擎、报表引擎、规则引擎、批处理引引擎等完整设计。nop-entropy是它的后端部分,采用java语言实现,可选择集成Spring框架或者Quarkus框架。中小企业可以免费商用
Java
12
1
暂无简介
Dart
801
199
🎉 (RuoYi)官方仓库 基于SpringBoot,Spring Security,JWT,Vue3 & Vite、Element Plus 的前后端分离权限管理系统
Vue
1.37 K
781
喝着茶写代码!最易用的自托管一站式代码托管平台,包含Git托管,代码审查,团队协作,软件包和CI/CD。
Go
24
0
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
350
203
Ascend Extension for PyTorch
Python
379
453
无需学习 Kubernetes 的容器平台,在 Kubernetes 上构建、部署、组装和管理应用,无需 K8s 专业知识,全流程图形化管理
Go
16
1