本文目录导读:

- 最基础但安全的方式(推荐)
- 支持断点续传(HTTP Range)
- 使用
readfile()配合ignore_user_abort - 防内存溢出的通用推荐(X-Sendfile)
- 注意事项汇总
- 完整示例(推荐使用方案1 + 增强)
在 PHP 中实现大文件下载是一个常见需求,直接使用 readfile() 或 file_get_contents() 可能会因为文件过大导致内存溢出,以下是几种安全高效的大文件下载方案。
最基础但安全的方式(推荐)
使用 fopen + fread 分块读取,配合 flush 输出:
<?php
$file = '/path/to/your/large-file.zip';
if (!file_exists($file)) {
http_response_code(404);
die('文件不存在');
}
// 强制下载
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . basename($file) . '"');
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize($file));
// 打开文件
$handle = fopen($file, 'rb');
if ($handle === false) {
die('无法打开文件');
}
// 设置输出缓冲大小(1MB 比较合适)
$bufferSize = 1024 * 1024; // 1MB
// 循环读取并输出
while (!feof($handle)) {
$buffer = fread($handle, $bufferSize);
echo $buffer;
ob_flush(); // 刷新输出缓冲
flush(); // 发送到浏览器
}
fclose($handle);
exit;
?>
优点:简单、可靠、内存占用仅固定为缓冲区大小(1MB)。 缺点:不支持断点续传,不支持分段下载。
支持断点续传(HTTP Range)
这个方案更专业,支持用户暂停/继续下载、拖动进度条:
<?php
$file = '/path/to/your/large-file.zip';
if (!file_exists($file)) {
http_response_code(404);
die('文件不存在');
}
$fileSize = filesize($file);
$handle = fopen($file, 'rb');
// 解析 Range 请求
$range = isset($_SERVER['HTTP_RANGE']) ? $_SERVER['HTTP_RANGE'] : null;
if ($range) {
list($start, $end) = sscanf($range, 'bytes=%d-%d');
if ($end === null) {
$end = $fileSize - 1;
}
http_response_code(206); // 部分内容
header('Content-Range: bytes ' . $start . '-' . $end . '/' . $fileSize);
fseek($handle, $start);
$length = $end - $start + 1;
} else {
$start = 0;
$end = $fileSize - 1;
$length = $fileSize;
http_response_code(200);
}
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . basename($file) . '"');
header('Accept-Ranges: bytes');
header('Content-Length: ' . $length);
header('Content-Range: bytes ' . $start . '-' . $end . '/' . $fileSize);
$bufferSize = 1024 * 1024; // 1MB
$bytesSent = 0;
while (!feof($handle) && $bytesSent < $length) {
$readLength = min($bufferSize, $length - $bytesSent);
$buffer = fread($handle, $readLength);
echo $buffer;
ob_flush();
flush();
$bytesSent += strlen($buffer);
}
fclose($handle);
exit;
?>
优点:支持暂停/恢复、多线程下载。 缺点:代码稍复杂。
使用 readfile() 配合 ignore_user_abort
如果文件不是特别大(<500MB),且内存限制较高时,可以直接用:
<?php
$file = '/path/to/your/large-file.zip';
if (!file_exists($file)) {
http_response_code(404);
die('文件不存在');
}
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . basename($file) . '"');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize($file));
// 设置较长的执行时间
set_time_limit(0);
// 直接读取并输出
readfile($file);
exit;
?>
注意:这会将整个文件读入内存(若文件过大可能 OOM),仅限小文件使用。
防内存溢出的通用推荐(X-Sendfile)
如果使用 Nginx 或 Apache,可以配置 X-Sendfile 由服务器发送文件,PHP 只负责鉴权:
<?php
$file = '/path/to/your/large-file.zip';
// 鉴权逻辑...
// Nginx
header('X-Accel-Redirect: /protected/' . basename($file));
// 或者 Apache + mod_xsendfile
// header('X-Sendfile: ' . realpath($file));
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . basename($file) . '"');
exit;
?>
注意事项汇总
| 要点 | 说明 |
|---|---|
| 内存限制 | 避免 file_get_contents() 或直接 readfile() 大文件 |
| 执行时间 | 使用 set_time_limit(0) 防止脚本超时 |
| 输出缓冲 | ob_end_flush() 或配合 ob_flush() + flush() |
| 用户中断 | 使用 ignore_user_abort(true) 让用户关闭时不报错 |
| 请求头 | 必须包含 Content-Type、Content-Length、Content-Disposition |
| 文件权限 | 确保 PHP 有读取权限,检查目录权限 |
| 安全 | 对外部传入的文件路径做安全验证,防止路径穿越 |
完整示例(推荐使用方案1 + 增强)
<?php
// download.php
$file = isset($_GET['file']) ? $_GET['file'] : '';
// 安全验证:仅允许特定目录内的文件
$baseDir = '/var/www/downloads/';
$realPath = realpath($baseDir . $file);
if (!$realPath || strpos($realPath, $baseDir) !== 0) {
http_response_code(403);
die('非法访问');
}
if (!file_exists($realPath)) {
http_response_code(404);
die('文件不存在');
}
set_time_limit(0);
ignore_user_abort(true);
ob_end_flush(); // 关闭输出缓冲
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . basename($realPath) . '"');
header('Content-Length: ' . filesize($realPath));
header('Cache-Control: must-revalidate');
header('Pragma: public');
$handle = fopen($realPath, 'rb');
while (!feof($handle)) {
echo fread($handle, 1024 * 1024);
flush();
}
fclose($handle);
exit;
?>
| 场景 | 推荐方案 |
|---|---|
| 一般文件(<200MB) | 方案1(fread 分块) |
| 大文件 + 断点续传 | 方案2(Range) |
| 服务器支持 X-Sendfile | 方案4 |
| 仅限小文件 | 方案3(readfile) |
最佳实践:真实项目中尽量使用 fread 循环 + 支持 Range 请求,同时保证安全性和用户体验。