开发环境
- PHP:7.2
- Nginx 1.25.1
- Linux:3.10
简介
使用 输出控制 能够获得更多的输出控制权。
例如:
ob_start();
echo "world !"; // 这个输出将会输入到缓冲区当中,不会输出到客户端。
$string = ob_get_clean(); // 获取缓冲区的内容,并且清除缓冲区的内容 和 关闭缓冲区。
echo "hello ";
echo $string; // 这里就是上面输出的 "world !"
运行结果:
hello world !
开启方式
如何开启这个输出控制缓冲区呢?通常有两种开启方式:全局开启和会话开启。
全局开启
在 PHP 7.2 是处于默认开启状态,在程序开启时为程序自动开启一个输出控制缓冲区,并程序中的运行结束时,将缓冲区的内容输出出来。
在 php.ini 文件中设置以下配置项:
// 表示默认开启输出控制缓冲区,缓冲区的大小为 4096。
output_buffering = 4096
如果想关闭默认开启的状态:
output_buffering = off
会话开启
在代码中使用 ob_start() 函数开启即可。
ob_start();
// ... 业务代码 ...
示例
接下来通过下面的几个示例来加深对输出控制的认识。
生成静态文件
// 开启输出控制缓冲区
ob_start();
// ... 输出的静态文件内容 ...
// 获取写入缓冲区的内容 - 就是输出的文件内容。
$page = ob_get_clean();
// ... 以下是保存静态文件的操作 ...
$file = getcwd() . DIRECTORY_SEPARATOR . "public" . DIRECTORY_SEPARATOR . "index.html";
@chmod($file, 0755);
$fd = fopen($file, "w");
fwrite($fd, $page);
fclose($fd);
值得注意的是,ob_start 的 chunk_size 参数要是设置大于 0 时 ,并且,输出的内容大小 大于等于 缓冲区的大小,那么缓冲区的内容将被冲刷出来,而不会被 ob_get_clean() 赋值给 $page。
所以在处理这种内容大小不确认的场景时, chunk_size 最好保持默认值 0。
错误的示例
// 设置缓冲区大小为 100 字节
ob_start(null, 100);
echo str_repeat('a', 50);
echo str_repeat('a', 50);
// 获取写入缓冲区的内容 - 就是输出的文件内容。
$page = ob_get_clean();
// 将获取到的内容包裹在 {} 中。
echo "{" . $page . '}';
运行结果为:
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{}
可以看得出 $page 并没有获取到输出的内容,因为缓冲区满了所以直接输出来。
如果希望在程序出现异常时,不将输出的内容显示出来。
对输出控制缓冲区进行小小的封装:
class OutputBuffer {
// 控制是否将输出内容显示的参数
protected $output = false;
// 控制输出缓冲区的回调函数
protected function outputBufferCallback(string $buffer) {
if ($this->output) return $buffer;
return '';
}
// 开启控制输出缓冲区
public function obStart(): bool {
$this->output = false;
return ob_start([$this, 'outputBufferCallback']);
}
// 换取缓冲区的内容 并清除和关闭缓冲区
public function obGetClean(): string {
$this->output = true;
$result = ob_get_clean();
$this->output = false;
return $result;
}
}
业务逻辑处理:
$OutputBuffer = new OutputBuffer();
// 开启输出控制缓冲区
$OutputBuffer->obStart();
// ... 业务处理 ...
// 获取缓冲区的内容
// 要是业务处理过程中出现异常,那么就不会走到这一步。
// 那时 output 属性将保持为 false ,程序结束自动触发刷新缓冲区的操作,执行回调函数(outputBufferCallback) 的逻辑,将原本输出的内容替换为空字符串。
echo $OutputBuffer->obGetClean();
这样也就可以在程序出现异常时,不将输出的内容显示在客户端中。
输出控制缓冲区甚至可以嵌套
ob_start(); // 开启输出控制缓冲区 1
echo "a";
ob_start(); //开启输出控制缓冲区 2
echo "b";
$s2 = ob_get_contents(); // 获取缓冲区 2 的内容
ob_end_flush(); // 将缓冲区 2 的内容输出来 并且 关闭缓冲区 2 -> 这些输出的内容被上层缓冲区(缓冲区 1)接收。
echo "c"; // 继续往缓冲区 1 写入内容
$s1 = ob_get_contents(); // 获取缓冲区 1 的内容
ob_end_flush(); // 将缓冲区 1 的内容输出来 并且 关闭缓冲区 1
echo PHP_EOL;
echo "缓冲区 1:" . $s1 . PHP_EOL;
echo "缓冲区 2:" . $s2 . PHP_EOL;
输出结果:
abc
缓冲区 1:abc
缓冲区 2:b
实时向浏览器输出内容
Nginx
因为从PHP到浏览器显示会经过的缓冲区有这些:PHP输出控制缓冲区 -> Fastcgi缓冲区 -> WebServer缓冲区(Nginx|Apache) -> 浏览器。
如果只是单单使用 flush() 函数,是没有办法在 PHP 脚本未运行结束前,就将部分已输出的内容发送到浏览器显示的。
flush() 的作用是刷新 WebServer 的缓冲区,使 WebServer 的缓冲区的内容发送到浏览器。
但是 PHP 的输出内容并不会直接发送给 WebServer ,而是会先发送到 fastcgi 的缓冲区。在程序运行结束后,由 fastcgi 将缓冲区的内容转发给 WebServer。最后 WebServer 再将缓冲区的内容发送给客户端。
当然这也是比较理想的情况,如果其中某个缓冲区满了或者溢出了,会脚本运行结束前,自动将缓冲区的内容发送给上层缓冲区的。
所以为了我们能够顺利测试效果,我们需要将 Nginx 的 fastcgi 的缓冲区关闭。
全局关闭
# nginx.conf
...
http {
...
// 关闭 fastcgi 缓冲区
fastcgi_buffering off;
...
}
单个服务关闭
# conf.d/*.conf
...
location ~ \.php$ {
// 关闭 fastcgi 缓冲区
fastcgi_buffering off;
...
}
重新加载 Nginx:nginx -s reload。
现在我们开始代码测试了。
// 先关闭PHP默认开启的输出控制缓冲区
ob_end_clean();
for(var $i = 0; $i < 10; $i++) {
// 因为已经将 "输出控制缓冲区" 和 "fastcgi缓冲区" 关闭了。
// 所以这条输出的内容会直接发送到 Nginx(或者Apache) 的缓冲区。
echo "第{$i}条消息:" . date("Y-m-d H:i:s") . "<br />";
// 这样我们使用flush()来刷新Nginx缓冲区,就能将输出的内容发送给客户端了。
flush();
sleep(1);
}
现在我们就可以用浏览器来访问脚本了。
参考
更多详情请查看以下这些连接。