直接用 readfile() 下载大文件会超时或内存溢出,因其一次性将整个文件读入内存;应改用分块读取、流式输出,并禁用输出缓冲、设置正确响应头、规避中间件干扰、配置 Web 服务器参数。

为什么直接用 readfile() 下载大文件会超时或内存溢出
ThinkPHP 默认的 response()->download() 底层可能调用 readfile(),一次性把整个文件读进 PHP 内存再输出。100MB 的文件,PHP 就得分配至少 100MB 内存,还容易触发 max_execution_time 超时,Nginx 也可能在 60 秒后主动断连。
真正可行的做法是:不加载全文,而是分块读取、边读边输出,让响应流式传输。
- 禁用输出缓冲:
ob_end_clean()必须调用,否则ob_start()留下的缓冲会吃掉分块输出 - 设置合适的响应头:
Content-Transfer-Encoding: binary和Content-Length(如果能预知大小)要准确,否则 Chrome 可能显示“已下载 0 字节” - 避免框架中间件干扰:比如日志中间件、调试工具栏,在下载路由里应提前终止或跳过
用 Response::stream() 实现可控分块下载(TP6+)
ThinkPHP 6.1+ 提供了 Response::stream(),它接受一个回调函数,每次回调只处理一块数据,天然适合大文件场景。
关键不是“怎么写回调”,而是“怎么控制块大小和刷新节奏”:
- 推荐块大小设为
8192(8KB):太小(如 512B)会频繁系统调用,太大(如 1MB)仍可能卡住低内存环境 - 每块后调用
flush()+ob_flush(),但必须确认output_buffering已关闭(ini_set('output_buffering', 'Off')) - 文件路径必须用绝对路径,相对路径在 CLI 或异步请求中容易失效;建议用
app()->getRootPath() . 'public/uploads/large.zip' - 记得手动设置
Content-Type,不要依赖finfo_file()自动探测——大文件探测本身就很慢
return Response::stream(function () {
$file = fopen('/path/to/big-file.iso', 'rb');
if (!$file) {
throw new HttpException(404, 'File not found');
}
while (!feof($file)) {
echo fread($file, 8192);
flush();
ob_flush();
}
fclose($file);
}, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="big-file.iso"',
'Content-Length' => filesize('/path/to/big-file.iso'),
]);
TP5.1 没有 stream()?手写 readfile_chunked() 更稳
TP5.1 不支持 Response::stream(),但也不必重写整个响应类。直接封装一个安全的分块读取函数更可控。
注意这个函数不是“替代 readfile()”,而是绕过它所有隐式行为:
- 必须显式检查
fopen()返回值,FALSE时立刻抛异常,不能静默失败 -
set_time_limit(0)要放在函数开头,否则大文件读到一半可能被中断 - Apache 的
mod_php和 FPM 模式下,fastcgi_finish_request()不能用——它会提前关闭连接,导致下载中断 - 如果文件路径来自用户输入,务必用
realpath()+ 白名单校验,防止目录穿越(如../../../etc/passwd)
Nginx 下下载中断?重点检查 proxy_buffering 和 send_timeout
即使 PHP 层完全正确,Nginx 仍可能截断响应。这不是 ThinkPHP 的问题,但开发者常误以为是代码 bug。
核心配置项就两个,必须改:
-
proxy_buffering off;—— 否则 Nginx 会缓存整个响应体再吐给浏览器 -
send_timeout 300;—— 默认 60 秒,大文件下载很容易超时,按最大预期下载时间设(比如 5GB 文件设为 3600) - 如果用了
proxy_pass,还要确认proxy_max_temp_file_size 0;,禁用临时文件缓存
改完记得 nginx -t && nginx -s reload,光改 PHP 是没用的。
真正麻烦的从来不是“怎么写代码”,而是 PHP 层、Web 服务器层、浏览器层三者对“流式响应”的理解不一致。尤其 Content-Length 缺失时,Safari 会拒绝开始下载,而 Chrome 会等很久才报错——这种差异很难在本地复现,必须在真实部署环境验证。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!










