PHP 怎么PHP大文件下载

wen PHP项目 5

本文目录导读:

PHP 怎么PHP大文件下载

  1. 最基础但安全的方式(推荐)
  2. 支持断点续传(HTTP Range)
  3. 使用 readfile() 配合 ignore_user_abort
  4. 防内存溢出的通用推荐(X-Sendfile)
  5. 注意事项汇总
  6. 完整示例(推荐使用方案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-TypeContent-LengthContent-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 请求,同时保证安全性和用户体验。

抱歉,评论功能暂时关闭!