PHP 输出缓冲控制是实现长连接、进度条、实时日志、流式下载的关键。ob_flush() 与 flush() 常配对使用,但操作对象完全不同。本文从原理、调用顺序、SAPI 差异、浏览器行为四个维度深度解析。
| 函数 | 操作对象 | 生效条件 |
|---|---|---|
ob_flush() | PHP 内部输出缓冲区 | 开启了 output_buffering 或 ob_start() |
flush() | Web Server (Apache/Nginx) 缓冲区 | PHP 以 Module/Handler 运行(非 CGI/CLI) |
正确调用顺序:ob_flush(); flush(); —— 先推出 PHP 缓冲,再推出 Server 缓冲。
PHP 脚本
↓ echo/print
PHP output_buffering (ob_* 系列控制)
↓ ob_flush()
SAPI 层 (Apache mod_php / php-fpm)
↓ flush() → ap_rflush / fcgi_write
Web Server 发送缓冲区
↓ 网络传输
浏览器接收缓冲区 (256B/换行/标签触发渲染)
↓ 解析渲染
用户可见内容
| SAPI 模式 | flush() 效果 | 说明 |
|---|---|---|
| Apache mod_php (Handler) | ✅ 有效 | 调用 ap_rflush 刷新 Apache 输出桶 |
| Apache mod_php (Filter) | ✅ 有效 | 同上 |
| PHP-FPM (FastCGI) | ⚠️ 依赖配置 | 需 Nginx fastcgi_buffering off; 禁用缓冲 |
| CGI | ❌ 无效 | 请求结束一次性输出 |
| CLI | ✅ 有效 | 直接刷新 stdout,无 Server 层 |
flush() 失效fastcgi_buffers 大小<body> 或 <table> 闭合str_repeat(' ', 1024) 或 <!-- padding --> 填充<?php
// 禁用 PHP 输出缓冲(如 php.ini 已开启)
while (ob_get_level()) ob_end_clean();
// 禁用压缩(防止中间件缓冲)
@apache_setenv('no-gzip', 1);
@ini_set('zlib.output_compression', 0);
@ini_set('implicit_flush', 1);
header('Content-Type: text/html; charset=utf-8');
// 填充触发浏览器渲染
echo str_repeat('<!-- padding -->', 256);
for ($i = 1; $i <= 100; $i++) {
echo "<script>document.getElementById('progress').style.width='$i%';</script>";
echo "<script>document.getElementById('text').innerText='$i%';</script>";
// 关键:双刷新
ob_flush();
flush();
usleep(50000); // 模拟耗时操作
}
?>
<html>
<body>
<div style="width:300px;border:1px solid #ccc">
<div id="progress" style="width:0%;background:#09f;height:20px"></div>
</div>
<span id="text">0%</span>
</body>
</html>
output_buffering 是否过大(建议 Off 或 4096)zlib.output_compression = Offfastcgi_buffering off; 在 location 块中mod_deflate 或 SetEnv no-gzip 1php script.php | catContent-Length 或 Transfer-Encoding: chunked