输出控制(Output Control)用来在 PHP 脚本有输出时控制输出情况。在不少情况下都很有用,尤其在脚本开始输出数据后,发送 HTTP 头信息到浏览器。输出控制函数对 header()
或 setcookie()
发送的头信息没有影响,只会影响像 echo
这样的函数以及 PHP 代码块间的数据。
ob_start();
echo "Hello\n";
setcookie("cookiename", "cookiedata");
ob_end_flush();
上面的例子中,echo
函数的输出将一直被保存在输出缓冲区(Output Buffering Cache)中,直到调用 ob_end_flush()
。同时,对 setcookie()
的调用也成功存储了一个 cookie,而不会导致错误。(正常情况下,在数据被发送到浏览器后,就不能再发送 HTTP 头信息了。)
在现代操作系统中,几乎所有的设备在涉及数据交换的地方都设置了缓冲区。缓冲区由专门的寄存器组成,但由于硬件成本较高,容量也相对较小,一般用于速度要求非常高的地方(相对于内存,作为内存的缓冲)。而对于低速的 I/O 设备,内存就可以作为缓冲区。
由于数据的输入和输出速率不同,导致某一硬件的数据上行或下行产生时间差,为了减少这种时间差,引入缓存。
如果缓冲只有一位,那么每来一位数据 CPU 就要中断一次,起到的缓冲效果一般;如果缓冲变大,那么装满缓冲区就要一段时间,对于 CPU 来讲中断次数明显减少。
缓冲区可以解决生产者和消费者之间单位数据大小问题。比如生产者消费者问题中,生产者单位数据小于消费者单位数据时,生产者为了满足消费者需求可以连续生产好几个单位;而大于的话,消费者可以分几次取一个单位。
提高并行性和吞吐量。比如,CPU 向块设备输出数据时,可以先将数据存放在缓冲区中(速度快),然后返回,接下来由缓冲区慢慢向块设备写数据,而不是直接由 CPU 向慢速的块设备输出。
简单来说,缓冲区的作用就是,把输入或输出的内容先放进内存,而不直接显示或者读取,等缓冲的数据到达一定大小时,统一进行处理。缓冲区最本质的作用,就是协调高速 CPU 和相对缓慢的 I/O 设备的运作。
PHP 相关的缓冲区存在于这三个地方:
PHP 缓冲区
web 服务器缓冲区
浏览器缓冲区
当执行 PHP 脚本的时候,如果碰到了 echo
、print_r
等输出数据的函数,PHP 就会把要输出的数据放到 PHP 自身的缓冲区中,等待输出。
当 PHP 自身的缓冲区接到指令,指示要输出缓冲区的内容时,就会把缓冲区内的数据输出到 web 服务器(如 Apache 或 Nginx)的缓冲区,继续等待输出。
当 web 服务器接收到输出缓冲区内容的命令时,就会把缓冲区的内容输出,发送到浏览器。
所以大致流程是这样:脚本输出 -> PHP 缓冲区 -> web 服务器缓冲区 -> 浏览器缓冲区 -> 最终显示
由此可见,PHP 输出数据的时候,将会经过两个缓冲区,先是 PHP 自身的,然后是 web 服务器的,最后将数据发送给浏览器。这两步分别对应 ob_flush()
和 flush()
。
PHP 手册中提到:
flush()
可能不会影响 web 服务器的缓冲策略,不会对浏览器的客户端缓冲产生影响。也不影响 PHP 的用户输出缓冲区机制。因此,必须同时使用ob_flush()
和flush()
函数来刷新输出缓冲内容。
说的就是这个意思。
在 php.ini
中,有两个跟缓冲区紧密相关的配置项:
该配置影响的是 PHP 本身的缓冲区,有三种配置参数:
on
- 开启缓冲区
off
- 关闭缓冲区
4096
- 某一整数,表示缓冲区大小,4k
该配置影响 web 服务器的缓冲区,有两种配置参数:
on
- 自动刷新 web 服务器缓冲区。也就是当 PHP 发送数据到 web 服务器缓冲区的时候,直接就把输出返回到浏览器
off
- 不自动刷新 web 服务器缓冲区,接收数据后存入缓冲区,等待刷新指令再进行输出
对于个别 web 服务器程序,特别是 Win32 下的 web 服务器程序,在发送结果到浏览器之前,仍然会缓存脚本的输出,直到程序结束为止。
有些 Apache 的模块,比如 mod_gzip
,可能自己进行输出缓存,这将导致 flush()
函数产生的结果不会立即被发送到客户端浏览器。
一些版本的 Microsoft Internet Explorer 只有当接收到 256 个字节以后才开始显示页面,所以必须发送一些额外的空格来让这些浏览器显示页面内容。
Nginx 可能会缓冲 PHP-FPM 输出的信息,在达到设置值时才会将缓冲区的内容发送给客户端。
输出缓冲控制相关函数如下:flush
- 刷新 web 服务器缓冲区内容ob_clean
- 清空 PHP 缓冲区内容ob_end_clean
- 清空 PHP 缓冲区内容,并关闭 PHP 缓冲ob_end_flush
- 刷新 PHP 缓冲区内容,并关闭 PHP 缓冲ob_flush
- 刷新 PHP 缓冲区内容ob_get_clean
- 获取当前 PHP 缓冲区内容,并清空 PHP 缓冲区内容ob_get_contents
- 获取当前 PHP 缓冲区内容ob_get_flush
- 获取当前 PHP 缓冲区内容,并关闭 PHP 缓冲ob_get_length
- 获取当前 PHP 缓冲区内容长度ob_get_level
- 获取当前 PHP 缓冲区的嵌套级别ob_get_status
- 获取所有 PHP 缓冲区的状态ob_gzhandler
- ob_start 中用来对 PHP 缓冲区内容进行 gzip 压缩的回调函数ob_implicit_flush
- 打开/关闭 web 服务器缓冲自动刷新ob_list_handlers
- 列出所有使用中的输出处理函数ob_start
- 开启 PHP 输出缓冲output_add_rewrite_var
- 添加 URL 重写器的值output_reset_rewrite_var
- 重置 URL 重写器的值
PHP 缓冲是存在嵌套级别的,关闭 PHP 缓冲意思是「关闭当前最大值对应级别的缓冲」。举例说明:
// echo ob_get_level(); // 结果为 1
ob_start();
echo "1:blah\n";
// echo ob_get_level(); // 结果为 2
ob_start();
echo "2:blah";
// echo ob_get_level(); // 结果为 3
var_dump(ob_get_clean());
// echo ob_get_level(); // 结果为 2
/* 输出
1:blah
string(6) "2:blah"
*/
PHP 默认开启了缓冲,缓冲级别为 1,ob_get_flush()
返回自上次缓冲开启后所有缓冲数据,上例中,该函数执行后。第 3 级缓冲内容(var_dump
的输出)被放入第 2 级缓冲(1:blah\n
)中,最后程序结束,一起放入第 1 级缓冲区,接着缓冲数据被刷新输出。
因为浏览器也存在缓冲,所以输出内容时需要填充空格来填满缓冲区。经测试,缓冲区大小为:
Chrome 67 - 128B
IE 11 - 4096B,即 4KB
for( $i = 0 ; $i < 5 ; $i++ ) {
echo str_pad('', 4092); // 加上 <br> 正好 4096 字节,超过也没关系
echo $i . '<br>';
ob_flush();
flush();
sleep(1);
}
演示效果:
define('EXPIRED_TIME', 3600); // 静态页面缓存过期时间 1 小时
$articleId = isset($_REQUEST['id']) ? $_REQUEST['id'] : null;
if ($articleId) {
// 判断缓存文件是否存在和过期,如果存在且未过期,直接输出内容
$filename = 'article' . $articleId . '.html';
if (file_exists($filename) && (time() - filemtime($filename) < EXPIRED_TIME)) {
echo file_get_contents($filename);
return;
}
// 开启 ob 缓存
ob_start();
// 假设获取并生成了 id 对应的文章内容
$content = 'Content of article, id:' . $articleId;
// 往缓冲区写入文章内容
echo $content;
// 保存文章内容到文件
file_put_contents($filename, ob_get_contents());
} else {
echo 'Article id not defined.';
}
这里你可能会问,我也可以不用 ob 缓存呀,直接将文章内容保存到文件是一样的。但这里的情况还比较简单,实际项目中,可能会将页面内容拆分为很多部分,那就需要将这些内容拼接起来。如果使用 ob 缓存,直接输出到缓冲区就行了,最后统一再获取缓存区数据。
关于输出缓冲控制这一节,直接看 PHP 手册,理解起来还是比较困难的,比如 ob_flush()
和 flush()
的翻译都差不多,但是究竟有什么不同。很多内容受限于译者水平,翻译过来并不太好理解;也可能英文文档更新了,但译文还是原来的,特别是 PHP 7 发布之后,有些内容和原来已经不一样了。这就需要我们去查阅英文文档,并且不断测试和实践,来更深入的理解相关内容。路漫漫其修远兮,吾将上下而求索!