弁言
大家都知道PHP中有一个名为“输出缓冲区”层(layer)的东西。这篇文章便是来讲解它到底是个什么东西的?PHP内部是怎么实现它的?以及在PHP程序中怎么利用它?这个层并不繁芜,但常常会被误解,很多PHP开拓者并没有完成节制它。本日我们就一起来彻底把它搞清楚吧。
我们要谈论的东西是基于PHP 5.4(及以上版本),PHP中的OB层从5.4版开始就发生了很多变革,确切说是完备重写了,有些地方可能都不兼容PHP 5.3了。
什么是输出缓冲区?
PHP的输出流包含很多字节,常日都是程序员要PHP输出的文本,这些文今年夜多是echo语句或者printf()函数输出的。对付PHP中的输出缓冲区,你要知道三点内容。
第一点是任何会输出点什么东西的函数都会用到输出缓冲区,当然这说的是用PHP写的程序。如果你是编写PHP扩展,你利用的函数(C函数)可能会直接将输出写到SAPI缓冲区层,而不须要经由OB层。你可以在源文件main/php_output.h中理解到这些C函数的API文档,这个文件给我们供应了很多其他的信息,例如默认的缓冲区大小。
第二点你须要知道的是输出缓冲区层不是唯一用于缓冲输出的层,它实际上只是很多层中的一个。末了一点你要记住输出缓冲区层的行为跟你利用的SAPI(web或cli)干系,不同的SAPI可能有不同的行为。我们先通过一个图片来看看这些层的关系:
上面这张图片展示了PHP中的三种缓冲区层的逻辑关系。上面的两层便是我们常日所认识到的“输出缓冲区”,末了一个是SAPI中的输出缓冲区。这些都是PHP中的层,当输出的字节离开PHP进入打算机体系构造中的更底层时,缓冲区又会不断涌现(终端缓冲区(terminal buffer),fast-cgi缓冲区,web做事器缓冲区,OS缓冲区,TCP/IP栈缓冲区。。。)。请记住一个通用原则,除了这篇文章中谈论的PHP中的情形外,一个软件的很多部分都会先保留信息,然后再把它们通报到下一部分,直到终极把这些信息通报给用户。
CLI的SAPI有点分外,这里重点讲一下。CLI会将INI配置中的output_buffer选项逼迫设置为0,这表示禁用默认PHP输出缓冲区。以是在CLI中,默认情形下你要输出的东西会直接通报到SAPI层,除非你手动调用ob_()类函数。并且在CLI中,implicit_flush的值也会被设置为1。我们常常会搞不清implicit_flush的浸染,源代码已解释统统:当implicit_flush被设置为打开(值为1),一旦有任何输出写入到SAPI缓冲区层,它都会立即刷新(flush,意思是把这些数据写入到更低层,并且缓冲区会被清空)。换句话说便是:任何时候当你写入任何数据到CLI SAPI中时,CLI SAPI都会立即将这些数据扔到它的下一层去,一样平常会是标准输出管道,write()和fflush()这两个函数便是卖力干这个事情的。大略,对吧!
默认PHP输出缓冲区
如果你利用不同于CLI的SAPI,像PHP-FPM,你会用到下面三个跟缓冲区干系的INI配置选项:
output_bufferingimplicit_flushoutput_handler
在搞清楚这几个选项的含义之前,有一点须要先解释下,不能在运行时利用ini_set()改这几个选项的值。这些选项的值会在PHP程序启动的时候,还没有运行任何脚本之前解析,以是大概在运行时可以利用ini_set()改变它们的值,但改变后的值并不会生效,统统都已经太迟了,由于输出缓冲区层已经启动并已激活。你只能通过编辑php.ini文件或者是在实行PHP程序的时候利用-d选项才能改变它们的值。
默认情形下,PHP发行版会在php.ini中把output_buffering设置为4096个字节。如果你不该用任何php.ini文件(或者也不会在启动PHP的时候利用-d选项),它的默认值将为0,这表示禁用输出缓冲区。如果你将它的值设置为“ON”,那么默认的输出缓冲区的大小将是16kb。你可能已经猜到了,在web运用环境中对输出的内容利用缓冲区对性能有好处。默认的4k的设置是一个得当的值,这意味着你可以先写入4096个ASCII字符,然后再跟下面的SAPI层通信。并且在web运用环境中,通过socket一个字节一个字节的传输的办法对性能并不好。更好的办法是把所有内容一次性传输给做事器,或者至少是一块一块地传输。层与层之间的数据交流的次数越少,性能越好。你该当总是保持输出缓冲区处于可用状态,PHP会卖力在要求结束后把它们中的内容传输给终端用户,你不用做任何事情。
implicit_flush已在前面评论辩论CLI的时候提到过。对付其他的SAPI,implicit_flush默认被设置为关闭(off),这是精确的设置,由于只要有新数据写入就刷新SAPI的做法很可能并非你所希望的。对付FastCGI协议,刷新操作(flushing)是每次写入后都发送一个FastCGI数组包(packet),如果发送数据包之前先把FastCGI的缓冲区写满会更好一些。如果你想手动刷新SAPI的缓冲区,利用PHP的flush()函数。如果你想写一次就刷新一次,你可以设置INI配置中的implicit_flush选项,或者调用一次ob_implicit_flush()函数。
output_handler是一个回调函数,它可以在缓冲区刷新之前修正缓冲区中的内容。PHP的扩展供应了很多回调函数(用户也可以自己编写回调函数,下面会讲到)。
ob_gzhandler : 利用ext/zlib压缩输出mb_output_handler : 利用ext/mbstring转换字符编码ob_iconv_handler : 利用ext/iconv转换字符编码ob_tidyhandler : 利用ext/tidy整理输出的HTML文本ob_[inflate/deflate]_handler : 利用ext/http压缩输出ob_etaghandler : 利用ext/http自动天生HTTP的Etag
缓冲区中的内容会通报给你选择的回调函数(只能用一个)来实行内容转换的事情,以是如果你想获取PHP传输给web做事器以及用户的内容,你可以利用输出缓冲区回调。当前有一点也须要提一下,这里说的“输出”指的是头(headers)和体(body)。HTTP的头也是OB层的一部分。
头和体
当你利用一个输出缓冲区(无论是用户的,还是PHP的)的时候,你可能想以你希望的办法发送HTTP头和内容。你知道任何协议都必须在发送体之前发送头(这也是为什么叫做“头”),但是如果你利用了输出缓冲区层,那么PHP会接管这些,而不须要你操心。实际上,任何跟头的输出有关的PHP函数(header(),setcookie(),session_start())都利用了内部的sapi_header_op()函数,这个函数只会把内容写入到头缓冲区中。然后当你输出内容是,例如利用printf(),这些内容会写入到输出缓冲区(假设只有一个)。当这个输出缓冲区中的内容须要被发送时,PHP会先发送头,然后发送体。PHP为你搞定了所有的事情。如果你以为不爽,想自己动手,那你就只有把输出缓冲区禁用掉,除此之外别无他法。
用户输出缓冲区(user output buffers)
对付用户输出缓冲区,我们先通过一个示例来看看它是怎么事情的,以及你可以用它来做什么。再强调一下,如果你想利用默认PHP输出缓冲区层的话,你不能利用CLI,由于它已禁用了这个层。下面的这个示例用的便是默认PHP输出缓冲区,利用了PHP的内部web做事器SAPI:
/ launched via php -doutput_buffering=32 -dimplicit_flush=1 -S127.0.0.1:8080 -t/var/www /echo str_repeat('a', 31);sleep(3);echo 'b';sleep(3);echo 'c';
在这个示例中,启动PHP的时候将默认输出缓冲区的大小设置为32字节,程序运行后会先向个中写入31个字节,然后进入就寝状态。此时屏幕是空的,什么都不会输出,跟估量一样。2秒之后就寝结束,再写入了一个字节,这个字节填满了缓冲区,它会立即刷新自身,把里面的数据通报给SAPI层的缓冲区,由于我们将implicit_flush设置为1,以是SAPI层的缓冲区也会立即刷新到下一层。字符串’aaaaaaaaaa{31个a}b’会涌如今屏幕上,然后脚本再次进入就寝状态。2秒之后,再输出一个字节,此时缓冲区中有31个空字节,但是PHP脚本已实行完毕,以是包含这1个字节的缓冲区也会立即刷新,从而会在屏幕上输出字符串’c’。
从这个示例我们可以看到默认PHP输出缓冲区是如何事情的。我们没有调用任何跟缓冲区干系的函数,但这并不虞味这它不存在,你要认识到它就存在当出路序的运行环境中(在非CLI模式中才有效)。
OK,现在开始谈论用户输出缓冲区,它通过调用ob_start()创建,我们可以创建很多这种缓冲区(至到内存耗尽为止),这些缓冲区组成一个堆栈构造,每个新建缓冲区都会堆叠到之前的缓冲区上,每当它被填满或者溢出,都会实行刷新操作,然后把个中的数据通报给下一个缓冲区。
ob_start(function($ctc) { static $a = 0; return $a++ . '- ' . $ctc . \公众\n\"大众;}, 10);ob_start(function($ctc) { return ucfirst($ctc); }, 3);echo \"大众fo\"大众;sleep(2);echo 'o';sleep(2);echo \"大众barbazz\"大众;sleep(2);echo \公众hello\公众;/ 0- FooBarbazz\n 1- Hello\n /
在此我代替原作者讲解下这个示例。我们假设第一个ob_start创建的用户缓冲区为缓冲区1,第二个ob_start创建的为缓冲区2。按照栈的后进先出原则,任何输出都会先存放到缓冲区2中。
缓冲区2的大小为3个字节,以是第一个echo语句输出的字符串'fo'(2个字节)会先存放在缓冲区2中,还差一个字符,当第二echo语句输出的'o'后,缓冲区2满了,以是它会刷新(flush),在刷新之前会先调用ob_start()的回调函数,这个函数会将缓冲区内的字符串的首字母转换为大写,以是输出为'Foo'。然后它会被保存在缓冲区1中,缓冲区1的大小为10。
第三个echo语句会输出'barbazz',它还是会先放到缓冲区2中,这个字符串有7个字节,缓冲区2已经溢出了,以是它会立即刷新,调用回调函数得到的结果为'Barbazz',然后被通报到缓冲区1中。这个时候缓冲区1中保存了'FooBarbazz',10个字符,缓冲区1会刷新,同样的先会调用ob_start()的回调函数,缓冲区1的回调函数会在字符串前面添加行号,以及在尾部添加一个回车符,以是输出的第一行是'o- FooBarbazz'。
末了一个echo语句输出了字符串'hello',它大于3个字符,以是会触发缓冲区2刷新,由于此时脚本已实行完毕,以是也会立即刷新缓冲区1,终极得到的第二行输出为'1- Hello'。
输出缓冲区的内部实现
自5.4版后,全体缓冲区层都被重写了(由Michael Wallner完成)。之前的代码很垃圾,很多事情都做不了,并且有很多bug。这篇文章会给你供应更多干系信息。以是PHP 5.4才会对这部分进行重新,现在的设计更好,代码也更整洁,添加了一些新特性,跟5.3版的不兼容问题也很少。赞一个!
个中最赞的一个特性是扩展可以声明它自己的输出缓冲区回调与其他扩展供应的回调冲突。在此之前,这是不可能的,之前如果要开拓利用输出缓冲区的扩展,必须先搞清楚所有其他供应了缓冲区回调的扩展可能带来的影响。
下面是一个大略的示例,它展示了若何注册一个回调函数来将缓冲区中的字符转换为大写,这个示例的代码可能不是很好,但是足以知足我们的目的:
#ifdef HAVE_CONFIG_H#include \公众config.h\公众#endif#include \公众php.h\"大众#include \"大众php_ini.h\公众#include \"大众main/php_output.h\"大众#include \"大众php_myext.h\公众static int myext_output_handler(void nothing, php_output_context output_context){ char dup = NULL; dup = estrndup(output_context->in.data, output_context->in.used); php_strtoupper(dup, output_context->in.used); output_context->out.data = dup; output_context->out.used = output_context->in.used; output_context->out.free = 1; return SUCCESS;}PHP_RINIT_FUNCTION(myext){ php_output_handler handler; handler = php_output_handler_create_internal(\"大众myext handler\"大众, sizeof(\"大众myext handler\"大众) -1, myext_output_handler, / PHP_OUTPUT_HANDLER_DEFAULT_SIZE / 128, PHP_OUTPUT_HANDLER_STDFLAGS); php_output_handler_start(handler); return SUCCESS;}zend_module_entry myext_module_entry = { STANDARD_MODULE_HEADER, \"大众myext\公众, NULL, / Function entries / NULL, NULL, / Module shutdown / PHP_RINIT(myext), / Request init / NULL, / Request shutdown / NULL, / Module information / \"大众0.1\公众, / Replace with version number for your extension / STANDARD_MODULE_PROPERTIES};#ifdef COMPILE_DL_MYEXTZEND_GET_MODULE(myext)#endif
陷阱
大部分陷阱都已经揭示出来了。有一些是逻辑的问题,有一些是隐蔽的。逻辑方面,最明显的是你不应该在输出缓冲区回调函数内调用任何缓冲区干系的函数,也不要在回调函数中输出任何东西。
相对不太明显的是有些PHP的内部函数也利用了输出缓冲区,它们会叠加到其他的缓冲区上,这些函数会填满自己的缓冲区然后刷新,或者是返回里面的内容。print_r()、highlight_file()和highlight_file::handle()都是这类函数。你不应该在输出缓冲区的回调函数中利用这些函数。这种行为会导致未定义的缺点,或者至少得不到你期望的结果。
总结
输出层(output layer)就像一个网,它会把所有从PHP”遗漏“的输出圈起来,然后把它们保存到一个大小固定的缓冲区中。当缓冲区被填满了的时,里面的内容会刷新(写入)到下一层(如果有的话),或者是写入到下面的逻辑层:SAPI缓冲区。开拓职员可以掌握缓冲区的数量、大小以及在每个缓冲区层可以实行的操作(打消、刷新和删除)。这种办法非常灵巧,它许可库和框架设计者可以完备掌握它们自己输出的内容,并把它们放到一个全局的缓冲区中。对付输出,我们须要知道任何输出流的内容和任何HTTP头,PHP都会以精确的顺序发送它们。
输出缓冲区也有一个默认缓冲区,可以通过设置3个INI配置选项来掌握它,它们是为了防止涌现过大量的眇小的写入操作,从而造成访问SAPI层过于频繁,这样网络花费会很大,不利于性能。PHP的扩展也可以定义回调函数,然后在每个缓冲区上实行这个回调,这种运用已经有很多了,例如实行数据压缩,HTTP头管理以及搞很多其他的事情。