您的位置 首页 php

PHP实时输出为何一直失败?

以前在写PHP接口的时候,有时候为了方便就直接在浏览器上测试,通过 echo 打印内容进行调试。不过有些地方打印的“日志”数据太多,一下子看不过来,就想着先 sleep 一下再输出。

当时不太会搜索,找到的各种方法都不行,每次都是延迟后输出全部结果,并不会一点一点输出。

代码差不多长这样:

 <?php
ob_start();
for($i = 0; $i < 10; $i++) {
    echo "aaa:{$i}";
    echo str_pad('',4096)."<br/>";
    ob_flush();
    flush();
    sleep(1);
}
ob_end_flush();
  

实际运行结果是前端等待10秒后,一次性打印全部内容。

最近深入学习了网络原理后,突然想到这个问题,想着会不会是因为 TCP协议 里的 Nagle 算法 的缘故,于是就再次试了一下,结果依然是失败。不过现在我的搜索能力和以前大不相同,经过多番查找和实验,终于成功了。

这个其实和 Nagle 算法关系不大,毕竟网上给的例子很多都会在输出的末尾加上一大堆空字符串,这远远超出了超出了窗口大小,而且延迟确认应答也有时间限制,最大延迟0.5秒发送确认应答,很多操作系统设置为0.2秒左右,换句话说,最多0.5秒,后端就会将数据发送给前端,从而实现稍带应答。但现实情况是网页等待的时间是所有 sleep 的总和,而且网页是一次性打印出所有内容的,因此和它无关。

真正的原因是 输出缓冲区 ,后端发送的数据只有等缓冲区满了才会真正发送给前端,而这个缓冲区多达四层,甚至可以更多。只要其中一个缓冲区未满,数据就发送不出去,前端就不会立即看到结果,这就是我为何一直失败的原因。那么多层缓冲区,总有一个你没设置好,然后就被卡住了。

下面我们就来一个一个地攻克这些难关。

1、PHP默认缓冲区(Default Output Layer)

除了 HTTP 首部外,不管你是用 print 还是用 echo 等打印内容,都会先存储在这个 缓存 区中,只有缓冲区满了,才会输出到下一个缓冲区中。

可以在 php. ini 配置中设置 output_buffering = Off 以关闭缓冲区,我试过用 ini_set 函数进行设置是无效的。

你也可以使用 ob_flush 强制刷新缓冲区。

2、SAPI Buffer

PHP-FPM 实现了 SAPI 接口,PHP 默认缓冲区输出的内容会被 PHP-FPM 进程缓存。

php.ini 配置中设置 implicit_flush = On 可以自动刷新缓冲区,这个同样无法用 ini_set 进行修改。

使用 flush 函数强制刷新缓冲区也可以起到同样的效果。

这里有一个地方要注意,当 output_buffering = On 时,对 implicit_flush 进行修改是无效的,只能使用 flush 方法强制刷新缓冲区。

3、 Nginx 缓冲区

在 Nginx 配置文件中设置 fastcgi_buffering off 可以禁用缓冲区,也可以在 PHP 脚本中添加 header (‘X-Accel-Buffering: no’) 禁用缓冲区。

4、浏览器缓存

到了这里,数据已经通过 HTTP 协议发送给了前端(浏览器),但浏览器并不会立即显示,其本身同样会缓存数据。可以在控制台中使用 curl -N url 进行测试。或者在数据尾部添加一些空字符,强行填满缓冲区。

实际我测试的时候特别奇怪,在命令行上使用 curl 是没有问题的,但是在 Chrome 浏览器中,访问时并不会立即显示内容,而这并非因为浏览器缓存,因为当我在程序中加入 ob_flush 后,浏览器是会立即打印内容的,而我所打印的内容只有三个字节,这浏览器缓存肯定是没有用到的。这样看来 output_buffering = Off 并没有生效,可是如果你把这个配置去掉的话,那不管你加不加 ob_flush ,浏览器都不会立即输出结果。另外去掉 Accept-Encoding 头同样可以立即输出结果,确实奇怪。

其实大家也不用过于纠结,现实也不会有这么奇葩的需求,真要遇到了,多测试一下就行了。

User Output Layers

除了应用本身自带的缓存,我们也可以使用 ob_start 创建缓冲区。

多次调用 ob_start 会创建多个缓冲区,它们就像连在一起的漏斗,上一层慢了,就会把数据刷新到下一层。

flush 只能刷新最顶层的缓冲区,不管它是不是满的。

ob_end_flush 在刷新完顶层缓冲区后会关闭该缓冲区,这样多次调用后就可以把数据刷新到 SAPI 缓冲区。

一些框架所使用的模板语言就是利用自定义的缓冲区,将数据收集起来,再使用 ob_get_contents ob_end_clean 将数据从缓冲区中读取出来,最后才打印数据。

代码

最后我们来看下完整代码,方便需要的时候直接拷贝。

 <?php

// PHP输出缓冲区关闭
// 配置 php.ini,这个 ini_set("output_buffering", "Off") 是无法修改的
// output_buffering = Off;
// 可用 ob_flush() 刷新

// 关闭 SAPI 的缓冲区
// 配置 php.ini,这里 ini_set('implicit_flush', 1) 设置无效
// implicit_flush = On
// 可用 flush() 刷新

// 关闭nginx缓冲区
header('X-Accel-Buffering: no');
// 或在 Nginx 配置文件中设置 fastcgi_buffering off

echo 'abc'; // 少于三个字符也不行
ob_flush(); // 在浏览器中运行要加这个
// flush(); // 保险起见,最好 flush 一下
sleep(3);
echo 'b';

// 测试
// curl 可以禁用缓存,建议用这个测试,当然最好的方式是 cli+日志文件
// curl -N 

  

说明

命令行CLI 下运行是不会有缓冲区这个问题的,默认都是关闭的,除非用户主动创建。

切忌在生产环境里关闭缓冲区,服务器炸锅了我可不负责。

以上结果在不同的PHP版本中可能会有不同的表现,以实际运行结果为准,下面是我的测试环境数据:

Docker 版本:19.03.5

PHP版本(运行于Docker内):v7.4.6

Openresty版本(运行于Docker内):1.15.8.3

Chrome版本:87.0.4280.88(正式版本) (x86_64)

总结

这个我研究了很久,各种修改代码和配置,搞得我都想**了,sun of *****.

最后,作为一个PHPer,我想说, PHP是最好的语言 ,没有之一,哈哈。

喜欢的朋友欢迎点赞,让更多的人看到。

文章来源:智云一二三科技

文章标题:PHP实时输出为何一直失败?

文章地址:https://www.zhihuclub.com/78237.shtml

关于作者: 智云科技

热门文章

网站地图