本文目录导读:

- 方案一:使用
readfile+ 缓冲控制(最推荐,简单高效) - 方案二:手动分块读取(最可控,适合超大文件或网络不稳定)
- 方案三:利用 Web 服务器加速(最高性能,生产环境首选)
- 方案四:支持断点续传(HTTP Range)
- 必须处理的关键问题
- 选择策略
在PHP项目中处理大文件下载,核心挑战在于避免内存溢出和超时,PHP默认会将文件内容读入内存再输出,这对于几百兆甚至上G的文件是致命的。
以下是几种专业、可靠的解决方案,按推荐程度排序:
使用 readfile + 缓冲控制(最推荐,简单高效)
这是处理大文件下载的标准做法,关键在于关闭输出缓冲并强制刷新。
<?php
$file = '/path/to/your/largefile.zip'; // 实际文件路径
if (!file_exists($file)) {
header("HTTP/1.1 404 Not Found");
exit('文件不存在');
}
// 1. 关闭所有输出缓冲(重要!)
while (ob_get_level()) {
ob_end_clean();
}
// 2. 设置下载头
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream'); // 通用二进制流
header('Content-Disposition: attachment; filename="' . basename($file) . '"');
header('Content-Length: ' . filesize($file));
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Expires: 0');
// 3. 一次性输出文件内容(PHP内部会分块处理)
readfile($file);
exit;
为什么可行? readfile() 在PHP内部使用了内存映射或分块读取,不会把整个文件加载到PHP内存中,配合 ob_end_clean(),避免输出缓冲积累。
手动分块读取(最可控,适合超大文件或网络不稳定)
当文件极大(>2GB)或需要添加暂停/续传逻辑时,手动分块更可靠。
<?php
$file = '/path/to/largefile.iso';
$chunkSize = 1024 * 1024; // 每次读取 1MB
if (!file_exists($file)) {
header("HTTP/1.1 404 Not Found");
exit;
}
// 关闭缓冲
while (ob_get_level()) {
ob_end_clean();
}
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . basename($file) . '"');
header('Content-Length: ' . filesize($file));
header('Cache-Control: must-revalidate');
header('Pragma: public');
$handle = fopen($file, 'rb');
if ($handle === false) {
header("HTTP/1.1 500 Internal Server Error");
exit('无法打开文件');
}
// 逐块发送,每发送一块立即刷新
while (!feof($handle)) {
$buffer = fread($handle, $chunkSize);
echo $buffer;
flush(); // 强制将当前输出发送到客户端
// 可选:计算已发送大小,检查连接是否断开
if (connection_aborted()) {
break;
}
}
fclose($handle);
exit;
优势: 可以精确控制内存占用(如设置 $chunkSize = 4096 时仅占用4KB内存)。
利用 Web 服务器加速(最高性能,生产环境首选)
如果条件允许,不要让PHP处理文件传输,让专业的Web服务器(Nginx/Apache)来做,这可以解放PHP进程,显著提高并发性能。
Nginx:使用 X-Accel-Redirect 头
<?php
$file_path = '/path/to/your/protected/files/largefile.zip';
// 可以在此进行权限检查、下载次数统计等逻辑...
// 设置文件路径(注意:该路径是Nginx内部映射的路径,不是公开URL)
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="largefile.zip"');
header('X-Accel-Redirect: /protected-files/largefile.zip');
// 替换为你在nginx中配置的internal位置
exit;
Nginx配置示例:
location /protected-files/ {
internal; # 禁止外部直接访问
alias /real/path/on/server/; # 映射到真实文件路径
}
原理: PHP返回一个特殊的HTTP头,Nginx收到后接管文件发送,不占用PHP进程。
Apache:使用 mod_xsendfile
<?php
// 需要安装 mod_xsendfile 模块
header("X-Sendfile: /full/path/to/file.zip");
header("Content-Type: application/octet-stream");
header("Content-Disposition: attachment; filename=\"file.zip\"");
exit;
支持断点续传(HTTP Range)
对于大文件下载,支持断点续传是极佳的体验,以下核心思路(代码较长,此处给出关键步骤):
- 接收
Range头:解析$_SERVER['HTTP_RANGE'],获取start和end字节。 - 返回
206 Partial Content。 - 定位文件指针:使用
fseek($handle, $start)。 - 发送指定范围的字节。
必须处理的关键问题
- 内存限制:禁止使用
file_get_contents()读取整个文件。 - 执行时间:大文件下载可能耗时数十分钟,使用
set_time_limit(0)取消时间限制。 - 输出缓冲:务必用
ob_end_clean()+flush()循环刷新。 - 超大文件 ( > 2GB ):32位系统上
filesize()和fread()可能失效,需要使用fopen('rb')+ 循环读取,或使用CURLFile/SplFileObject。 - 浏览器兼容性:某些浏览器依赖
Content-Length显示进度条,务必正确设置。
选择策略
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 简单下载,文件 < 2GB | readfile() + 关闭缓冲 |
代码最少,性能足够 |
| 超大文件 ( > 2GB ) | 手动分块读取 ( fopen+fread ) |
克服32位限制,内存可控 |
| 高并发生产环境 | Web服务器加速 (Nginx/Apache) | 性能最好,不阻塞PHP进程 |
| 需要权限控制/统计 | 权限验证 + Web服务器加速方案 | 平衡安全与性能 |
| 需要断点续传 | 手动实现 HTTP Range | 用户体验最佳 |
最终建议: 如果你是新手,先使用 方案一,如果你有Nginx,强烈建议学习 方案三。