php 8.1 web 环境中无法用 echo + flush() 实现浏览器实时下载进度条,根本原因是 http 协议、web 服务器(nginx/apache)、php 运行模式(fpm/cgi)及浏览器共同拦截流式输出;唯一可靠方案是前后端分离轮询或 cli 下使用 curl 进度回调。

PHP 8.1 下载服务器文件时无法靠 echo + flush() 直接在浏览器里显示实时进度条——这不是代码写得不对,而是 HTTP 协议、Web 服务器(Nginx/Apache)、PHP 运行模式(FPM/CGI)和浏览器本身共同拦截了“边下载边推送”的能力。真正在生产环境能稳定工作的方案,只有「前后端分离轮询」或「cURL 进度回调 + CLI 场景」两种路径。
cURL 的 CURLOPT_PROGRESSFUNCTION 只在 CLI 下可靠
很多人抄来就用的 curl_setopt($ch, CURLOPT_PROGRESSFUNCTION, 'progress'),在 Web SAPI(比如 PHP-FPM + Nginx)下基本失效。原因很实在:
-
CURLOPT_NOPROGRESS必须设为0,但 PHP-FPM 默认禁用进度回调(尤其在启用了 opcache 或 fastcgi_buffering 的情况下) - 进度函数触发时机依赖底层 libcurl 的 chunk 报告,而 Web 服务器常把响应体缓存成整块发出去,前端根本收不到中间状态
- PHP 8.1 对
register_tick_function和信号处理更严格,pcntl方案也难复用
如果你是在命令行跑下载脚本(例如定时任务、部署工具),这段代码能用:
$ch = curl_init('https://example.com/big.zip');
$fp = fopen('/tmp/big.zip', 'wb');
curl_setopt($ch, CURLOPT_FILE, $fp);
curl_setopt($ch, CURLOPT_NOPROGRESS, false);
curl_setopt($ch, CURLOPT_PROGRESSFUNCTION, function($ch, $download_size, $downloaded, $upload_size, $uploaded) {
if ($download_size > 0) {
$p = round($downloaded / $download_size * 100, 1);
echo "\rDownload: {$p}% ({$downloaded}/{$download_size} bytes)";
flush();
}
});
curl_exec($ch);
fclose($fp);
curl_close($ch);
Web 页面要进度条,必须用轮询 + 临时文件
这是 PHP 8.1 下唯一可落地的方案:后端分三步走(prepare → start → poll),前端用定时器查当前已写入字节数。关键点不在 PHP 多“聪明”,而在规避所有缓冲层:
- 下载过程必须写入一个**可被
filesize()瞬时读取的本地临时文件**(不能是内存 stream、不能是 /dev/shm、不能是 NFS 挂载点) - PHP 脚本执行期间禁止任何输出(包括 warning、notice),否则 JSON 返回会被污染
- 前端轮询接口必须返回纯 JSON,且响应头不含
Content-Encoding: gzip(Nginx 需配gzip off;) - 临时文件路径需全局可读,且不能被 PHP OPcache 扫描(避免 .php 后缀误判)
示例轮询接口(status.php):
// 假设临时文件路径存在 $_GET['tmp']
$tmp = $_GET['tmp'] ?? '';
if (file_exists($tmp) && is_readable($tmp)) {
header('Content-Type: application/json');
echo json_encode(['size' => filesize($tmp)]);
} else {
http_response_code(404);
echo json_encode(['size' => 0]);
}
ob_flush() + flush() 在 PHP 8.1 Web 环境里大概率白忙
网上大量教程教你在循环里加 echo "progress: 50%"; ob_flush(); flush();,但在 PHP 8.1 + FPM + Nginx 组合下,这些调用几乎不起作用。不是你漏了 @ini_set('output_buffering', 'Off'),而是:
- Nginx 的
fastcgi_buffering on(默认开启)会把整个响应体攒够再发 - PHP-FPM 的
buffer_output设置无法在运行时覆盖 - Chrome/Firefox 对小于 1KB 的响应块会主动缓冲,除非你先输出 1024 字节占位符
- 即使全关掉,HTTPS 中间设备(如 CDN、WAF)也可能重缓冲
所以别花时间调 ob_start() 嵌套层级或纠结 implicit_flush——它只适合开发机上验证逻辑,上线即崩。
真正麻烦的是临时文件生命周期管理:下载中断时要自动清理、并发请求要隔离 tmp 文件名、大文件写入要防止磁盘爆满。这些细节不处理好,进度条没出来,先挂掉的是服务器。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











