PHP输出缓冲区的总结

PHP输出缓冲区
   缓冲区:实际上是一个内存地址空间。它用来存储速度不同步的设备或者优先级不同的设备之间传输数据的区域。通过缓冲可
   以使进程之间的交互时间等待变小,从而使从速度慢的设备读取数据时,速度快的设备的操作进程不发生间断
   
   PHP的输出流包含很多内容,通常都是开发者要PHP输出的文本,这些文本大多是用echo或printf()函数来输出的
   

    1>任何输出内容的函数都会用到输出缓冲区
      这是指正常的PHP脚本,如果开发的是PHP扩展,使用的函数(C函数)可能会直接输出写到SAPI缓冲区层,不需要经过输出缓冲层(我们可以在PHP源文件 main/php_output.h了解这些C函数的API文档)
 

    2>输出缓冲区不是唯一用于缓冲输出的层,它实际上只是很多层中的一个,输出缓冲层的行为与使用的SAPI(Web或CLI)有关,不同的SAPI可能有不同的行为

                           缓冲逻辑关系图

最上端的两次就是我们通常所认识的‘输出缓冲区’


3>SAPI中的输出缓冲区,这些都是PHP中的层,当输出的字节离开PHP进入计算机体系结构中的更底层时,缓冲区又会不断出现(终端缓冲区(terminal buffer)、fast-cgi缓冲区、Web服务器缓冲区、操作系统缓冲区、TCP/IP栈缓冲区等)。
   PHP CLI的SAPI有点特殊,CLI也称命令行界面,它会将php.ini配置中output_buffer选项强制设置为0,这表示禁用默认PHP输出缓冲区,所以在CLI中,默认情况下你要输出的内容会直接传递到SAPI层,除非你手动调用ob_()类函数,并且在CLI中,implicit_flush的值也会被设置为1,
   注:我们经常混淆implicit_flush的作用,php的源代码已说明一切:当implicit_flush被设置为打开(值为1)时,一旦有任何输出写到SAPI缓冲区,它都会立刻刷新(flush,意思把这些数据写到更底层,并且缓冲区会被清空)
   也就是说任何数据到CLI SAPI中时,CLI SAPI都会立即将这些数据仍到它的下一层去,一般会是标准输出管到,write()和fflush()这两个函数就是负责做这件事情的

默认PHP输出缓冲区:如果使用不同于CLI的SAPI,比如PHP-FPM,会用到下面3个与缓冲区相关的php.ini配置选项
   output_buffering
   implicit_flush
   output_handler
 首先不能再运行时使用ini_set()函数修改这几个选项的值,因为这些值会在PHP程序启动的时候,还没有运行任何脚本之前解析,所以在运行时可以使用ini_set()改变值,但是并不会生效。我们只能通过编辑php.ini文件或者是在执行PHP程序的时候使用-d选项才能改变它们的值
 默认情况下,PHP发行版会在php.ini中会把output_buffering设置为4096个字节,如果将它的值设置为ON,那么默认的输出缓冲区的大小为16KB
   在Web应用环境中对输出的内容使用缓冲区对性能有好处
      默认的4K的设置是一个合适的值,意味着你可以先写入4096个ASCLL字符,然后在与下面的SAPI层通信,并且在Web应用环境中,通过Socket一个字节一个字节地传输消息的方式对性能并不好,更好的方式是把所有内容一次性传输给服务器,或者至少是一块一块地传输,层与层之间的数据交换次数越少,性能越好,应该总是保持输出缓冲区处于可用状态,PHP会负责在请求结束后把它们中的内容传输给终端用户,开发者不用做任何事

      implicit_flush默认是设置为关闭,这样的话新数据写入就不会刷新SAPI,对于FastCGI协议,刷新操作是每次写入后都发送一个FastCGI数组包,如果发送数据包之前先把FastCGI的缓冲器写满会更好。如果需要手动刷新SAPI的缓冲区,使用flush()函数,如果想写一个就刷新一次可以设置implicit_flush或者调用一次ob_implicit_flush()函数
      推荐使用配置:
      output_buffering=4096
      implicit_flush = Off/no
      要修改输出缓冲区的大小,硬确保使用的值是4/8的倍数,它们分别是32/64位操作系统
      
      output_handler是一个回调函数,它可以在缓冲区刷新之前修改缓冲区中的内容

       缓冲区中的内容会传递给你选择的回调函数(只能用一个)来执行内容转换的工作,所以说如果想获取PHP给Web服务器以及用户的内容,可以使用输出缓冲区回调,输出是指:消息头、消息头。HTTP的消息头也是输出缓冲区层的一部分
  消息头和消息体
      实际上,任何与消息头的输出有关的PHP函数(header()、setcookie()、session_start())都使用内部的sapi_header_op函数,这个函数只会把内容写入消息头缓冲区中。 当我们如使用printf()函数的时候,内容会先被写入到输出缓冲区(可能是多个),当这个输出环城区的内容需要被发送的时候,PHP会先发送消息头,然后发送消息体。PHP为了搞定了所有的事情,如果想自己动手,那就只能禁掉输出缓冲区

  用户输出缓冲区:

      如果想使用默认PHP输出缓冲区层,就不能使用CLI,因为它已经禁用了这个层

/*launched via php -d output_buffering=32 -d implicit_flush=1 
 * */
echo str_repeat('a',31);
sleep(3);
echo 'b';
sleep(3);
echo 'c';
?>

   默认输出缓冲区的大小设置32字节,程序运行会先写入31字节,然后休眠,然后在写入1一个字节,这个字节填满缓冲区,它会立即刷新自身,把里面的数据传递给SAPI层的缓冲区,因为把implicit_flush设置为1,所以SAPI层的缓冲区也会立即刷新到下一层,所以输出aa...b,然后休眠,然后在输出一个字节,此时缓冲区有31空字节,但是脚本执行完毕,所以包含这个字节的缓冲区也会立即刷新,从而会在屏幕输出c

  用户输出缓冲区:通过ob_start()创建,我们可以创建多个这种缓冲区(直到内存耗尽为止),这些缓冲区组成一个堆栈结构,每个新建缓冲区都会堆叠到之前的缓冲区上,每次当它被填满或者溢出,都会执行刷新操作,然后把其中的数据传递给下一个缓冲区

  function callback($buffer)
{
  // replace all the apples with oranges
  return ucfirst($buffer);
}
 function callback1($buffer)
{
  // replace all the apples with oranges
  static $a=0;
  return $a++.'-'.$buffer."\n";
}
ob_start('callback1',10);
ob_start("callback",3);
echo "fo";
sleep(2);
echo 'o';
sleep(2);
echo "barbazz";
sleep(2);
echo "hello";

按照栈的先进后出原则,任何输出都会先存放在缓冲区2中。缓冲区2的大小为3个字节,所以当第一个echo语句输出的字符串'fo'会先存放在缓冲区2中,还差一个字符,当第二个echo语句输出'o'后,缓冲区2满了,所以它会刷新(flush)。在刷新之前先调用ob_start()的回调函数,这个函数将缓冲区的首字母转换成大写,所以输出为‘Foo’,然后它会被保存在缓冲区1中,第三个输出为‘barbazz’,它还是会先放在缓冲区2中,这个字符串有7个字节,缓冲区2已经溢出了,所以它立刻刷新,调用回调函数得到的结构是‘Barbazz’,然后传递给缓冲区1中,这个缓冲区1中保存了'FooBarbazz'这十个字符,缓冲区1会刷新,同样的先会调用ob_start()的回调函数,缓冲区1的回调函数会在字符串签名添加型号,所以第一行是'0-FooBarbazz',

    最后一个echo语句输出一个字符串'hello',它大于三个字符,所以会触发缓冲区2,因为此时脚本执行完毕,所以也会立即刷新缓冲区1,得到 ‘1-Hello’。

 因此使用echo函数如此简单的事情,如果涉及缓冲区和性能也是复杂的,所以要注意使用echo输出内容的大小,如果缓冲区配置与输出内容相似,那么性能会比较优良,如果缓冲器配置小于输出内容,需要在应用中输出的内容做切分处理。

输出缓冲区的机制:主要是在5.4以后整个缓冲层都被重写,我们自己开发PECL扩展时,可以声明属于自己的输出缓冲区回调方法,这样可以于其他PECL扩展做区分,避免产生冲突

输出缓冲区的陷阱

      有些PHP的内部函数也使用了输出缓冲区,它们会叠加到其他的缓冲区上,这些函数会填满自己的缓冲区然后刷新,或者返回里面的内容,比如print_r()、higglight_file()和highlight_file::handle()都是此类,所以不应该在输出缓冲区的回调函数中使用这些函数,这样会导致未定义的错误

     同样的道理,当PHP执行echo、print时,也不会立即通过tcp输出到浏览器,而时将数据先写入PHP的默认缓冲区,我们可以理解PHP有一套自己的输出缓冲机制,在传送给系统缓存之前建立一个新的队列,数据经过该队列,当一个PHP缓冲区写满以及脚本执行逻辑需要输出时,脚本会把里面的数据传输给SAPI浏览器

    echo/print->php输出缓冲区->SAPI缓冲区->TCP缓冲区->浏览器缓冲区->浏览器展示

     ob_flush()和flush()区别

        ob_flush():把数据从php的缓冲区中释放出来

        flush():把不再缓冲区中的或者说是被释放出来的数据发送到浏览器,严格来讲, 这个只有在PHP做为apache的Module(handler或者filter)安装的时候, 才有实际作用. 它是刷新WebServer(可以认为特指apache)的缓冲区.

      在nginx中ob_flush和flush两个都失效

        解决办法:  发现在nginx的配置中,有如下的设置

              fastcgi_buffer_size 128k;

     fastcgi_buffers 8 128k;

Nginx会缓冲PHP输出的信息,当达到128k时才会将缓冲区的数据发送给客户端,那么我们首先需要将这个缓冲区调小

比如:

     fastcgi_buffer_size 4k;

     fastcgi_buffers 8 4k;

并且,必须禁用gzip

             gzip off;

然后,在php中,在ob_flushflush前,输出一段达到4k的内容,例如:

            echo str_repeat(‘ ‘, 1024*4);

到此,PHP就可以正常通过ob_flushflush逐行输出需要的内容了。

输出缓冲区实践

    1> 通过ob_start()函数手动处理PHP缓冲区机制,这样即便输出内容超过配置参数大小,也不会把数据传输给浏览器,ob_start()将PHP缓冲区空间设置到足够大,只有脚本执行结束后或调用ob_end_flush()函数,才会把数据发送给浏览器 

for($i=0;$i<10;$i++){
echo $i.'<br/>';
sleep($i+1);
}

 执行之后不会每隔几秒就有输出,知道脚本循环结束后,才会一次性输出,这是因为数据量太小,输出缓冲区没有写满

2>当修改output_buffering=0

for($i=0;$i<10;$i++){
echo $i.'<br/>';
flush();
sleep($i+1);
}

 因为缓冲区的容量设置为0,禁用PHP缓冲区机制,这是我们在浏览器看到断断续续输出,而不必等到脚本执行完毕才看到输出,这是因为数据没有在缓存中停留

3>我们把参数修改为output_buffering=4096,输出数据大于一个缓冲区,不调用ob_start()函数

    首先先输出一个4k的内容记下来加本来输出的内容:   

for($i=0;$i<10;$i++){
echo   echo str_repeat(' ', 1024*4*8).$i<br/>;
sleep($i);
}

发现可以HTTP连接未关闭,可以看到间断输出,尽管启用了PHP输出缓冲区机制,但是也不是一次性输出,这还是因为PHP缓冲区空间不够,每写满一个缓冲区,数据就会发送到浏览器。

4>参照上例子,这次我们调用ob_start()

ob_start(); //开启PHP缓冲区
for($i=0;$i<10;$i++){
echo   echo str_repeat(' ', 1024*4*8).$i<br/>;
sleep($i);
}

等到服务端脚本全部处理完,响应结束才会看到完整的输出,在输出前浏览器会一直保持空白,这是因为,PHP一旦调用了ob_start()会将PHP缓冲区扩展到足够大,知道ob_end_flush函数调用或者脚本运行结束才发送PHP缓冲区中的数据到客户端浏览器

  可以通过tcpdump命令监控TCP的报文,来观察一下使用ob_start()和没有使用它的区别

 总结:ob_start激活output_buffering机制,一旦激活,脚本不再直接输出给浏览器,而是先暂时写入PHP缓冲区

            PHP默认开启out_buffering机制,通过调用ob_start函数把output_buffering值扩展到足够大,也可以通过$chunk_size来指定output_buffering的值,$chunk_size默认值是0,表示直到脚本运行结束后,PHP缓冲区中的数据才会发送到浏览器,若设置了$chunk_size的大小,则只要缓冲区达到这个值,就会发送给浏览器你

           可以通过指定output_callback参数来处理PHP缓冲区的数据,比如ob_gzhandler()将缓冲区中的数据压缩后传送给浏览器,ob_get_contents()是获取一份PHP缓冲区中的数据拷贝

ob_start();
echo date('Y-m-d h:i:s');

$output=ob_get_contents();
ob_end_flush();
echo '<!output>'.$output;

 后者是从ob_get_contents取的缓冲区的内容

 ob_end_flush()与ob_end_clean(0这两个函数都会关闭输出缓冲,区别是前者只是把PHP缓冲中的数据发生给客户端浏览器,而后者将PHP缓冲区中的数据删掉,但不发送给客户端,前者调用之后数据依然存在,ob_get_contents()依然可以获取PHP缓冲区中的数据拷贝

输出缓冲与静态页面

     大家都知道静态页面的加载速度快,不用请求数据库,以下就是生成静态页面的脚本代码

echo str_pad('',1024);//使缓冲区溢出
ob_start();//打开缓冲区
$content=ob_get_contents();//获取缓冲区内容
$f=fopen('./index.html','w');
fwrite($f,$content);//写入到文件
fclose($f);
ob_end_clean()清空并关闭缓冲区

  在一些模板引擎和页面文件缓冲中ob_start()函数被使用,如 WP、Drupal、Smarty,例如:

在Wp经常在主题目录header.php看到类似的代码:

只有一个flush(),它的所在的位置就是告诉浏览器那一部分的缓存需要更新,即页面头部以上部分需缓存

以及Wp的部分代码,可以看到,在缓冲区开启时,加入自己的回调方法
 内容压缩输出:就是把输出到客户端浏览器的内容进行压缩

好处:降低客户端对服务器出口带宽的占用,提升带宽的利用率。降低Web服务器(如Nginx、Apache、Tomcat等)处理文本时引入的开销,用户端可以减少网络传输延时对用户体验的影响,降低浏览器加载页面内容时占用的内存,有利于改善浏览器稳定性 

ob_start('ob_gzhandler');//使用gz格式压缩输出
print'my contents';
ob_end_flush();

   之压缩当前脚本与缓冲区,对其他脚本没有影响,PHP还提供另外一种压缩方式,在php.ini修改

  zlib_output_compression=On

  这样输出时所有页面都以zlib的压缩方式输出,但是两者混用是毫无意义的,只会额外消耗CPU性能,让它压缩已经压缩好的内容,但是基于实践,使用PHP压缩效果并不是十分理想,通常做法是放在Web服务器,比如Apache启用defate、Nginx使用gzip的方式都比PHP端压缩效果好得多

   输出缓冲允许第三方库和应用框架(Laravel、TP等)开发者完全控制它们自己输出的内容。比如把他们放在一个全局缓冲区中处理,对于任何输出流的内容(如数据压缩)和任何HTTP消息头、PHP都以正确的书序发送,使用输出缓冲区能够有效地节省带宽,比如图片、字体、CSS、JS等前端内容,特别是限制前端框架也越来越来,让它使用户反应速度更快,从而有效提高系统性能

猜你喜欢

转载自blog.csdn.net/ligupeng7929/article/details/87936868