本文目录导读:

- 最基础但安全的方式(推荐)
- 支持断点续传(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 请求,同时保证安全性和用户体验。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播