必须放弃readfile()或file_get_contents()——改用fopen()+fread()流式输出,清空缓冲、分块读取(8KB–64KB)、及时flush(),并正确处理断点续传与中文文件名。

直接用 readfile() 或 file_get_contents() 输出大文件,PHP 进程大概率会 OOM(内存溢出),Nginx/Apache 可能直接 kill 掉请求,浏览器卡死或返回 502。这不是代码写错,是设计误用——必须放弃“全量加载进内存”的思路。
用 fopen() + fread() 流式输出,内存恒定
核心是让 PHP 不把整个文件读进内存,而是打开文件句柄,按块读、按块输出。典型块大小为 8KB–64KB,兼顾网络吞吐和系统调用开销。
- 必须用
fopen($path, 'rb')二进制模式打开,避免 Windows 下换行符被误转 - 每次
fread($fp, 8192)后立即echo和flush(),强制输出到 Web 服务器(如 Nginx) - 开头必须调用
ob_end_clean()清掉所有已有缓冲,否则响应头可能被污染 - 结尾必须
fclose($fp),且紧跟exit,防止脚本后续输出干扰流
$fp = fopen($filePath, 'rb');
if (!$fp) {
http_response_code(500);
exit('无法打开文件');
}
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="data.zip"');
header('Content-Length: ' . filesize($filePath));
ob_end_clean();
while (!feof($fp)) {
echo fread($fp, 8192);
flush();
}
fclose($fp);
exit;
断点续传必须手动解析 $_SERVER['HTTP_RANGE']
浏览器暂停/续传、下载工具多线程拉取时,会发 Range: bytes=1024-2047 请求。PHP 默认不处理,直接返回 200 + 全量内容,客户端就判定“断点失败”,重头开始下。
- 先检查
$_SERVER['HTTP_RANGE']是否存在,再用正则提取起始偏移:preg_match('/^bytes=(\d+)-(\d+)?/', $range, $matches) - 计算
$start、$end和实际长度$length,注意$end可能为空(表示“到结尾”) - 必须用
fseek($fp, $start)定位文件指针,再fread($fp, $length)精确读取 - 响应头要设为
HTTP/1.1 206 Partial Content,且Content-Range格式必须严格:bytes 1024-2047/123456(末尾总大小不能错) - 别用
substr(file_get_contents(), ...)——又回到内存加载的老路
中文文件名必须用 filename*=UTF-8'' 编码
直接写 filename="报告.pdf" 在 Chrome/Firefox 下大概率乱码或截断;IE 已淘汰,无需兼容 GBK。RFC 5987 是唯一现代解法。
立即学习“PHP免费学习笔记(深入)”;
- fallback 的 ASCII 文件名必须保留(如
filename="report.pdf"),否则旧 Safari 会失败 - 真实 UTF-8 名用
rawurlencode($zhName)编码,不是urlencode(),也不是iconv() - 完整写法:
header('Content-Disposition: attachment; filename="report.pdf"; filename*=UTF-8\'\''.rawurlencode($zhName)); - 如果
$zhName来自用户输入,务必先basename()过滤路径遍历
Web 服务器层优化比 PHP 层更关键
哪怕 PHP 写得再规范,Nginx/Apache 仍可能缓存响应、截断长连接或限制单次响应大小。真正压测过 >1GB 文件的人,基本都切到了 X-Accel-Redirect(Nginx)或 X-Sendfile(Apache)。
- Nginx 配置需启用
underscores_in_headers on;(因 PHP 设置的X-Accel-Redirect含下划线) - PHP 中只需输出
header('X-Accel-Redirect: /internal/downloads/'.basename($filePath));,其余由 Nginx 直接从磁盘送文件 - 此时 PHP 不读文件、不占内存、不耗 CPU,连
fopen()都省了——但要求你有服务器配置权限 - 没权限?那就老老实实做流式 + 断点 + 缓冲控制,别碰
output_buffering = On,PHP.ini 里关掉它
最易被忽略的一点:Content-Length 在断点续传时必须是实际片段长度,不是整个文件大小;而流式传输中若用 fread() 循环,Content-Length 又必须是完整大小——这两个场景的头设置逻辑完全不同,混用必出错。



















