首頁  >  文章  >  後端開發  >  php的輸出緩衝區詳解

php的輸出緩衝區詳解

小云云
小云云原創
2018-03-21 11:32:071754瀏覽

大家都知道PHP中有一個名為「輸出緩衝區」層(layer)的東西。這篇文章就是來講解它到底是什麼東西的? PHP內部是怎麼實現它的?以及在PHP程式中怎麼使用它?這個層並不複雜,但經常被誤解,很多PHP開發者並沒有完成掌握它。今天我們就一起來徹底把它搞清楚吧。

我們要討論的東西是基於PHP 5.4(以上版本),PHP中的OB層從5.4版開始就發生了很多變化,確切地說是完全重寫了,有些地方可能都不相容P​​HP 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_buffering

  • implicit_flush

  • ##output_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訊息標頭管理以及搞很多其他的事情。

相關推薦:

詳解透過刷新PHP緩衝區為你的網站加速

php中緩衝區的範例詳解

php深入理解刷新緩衝區函數用法

以上是php的輸出緩衝區詳解的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn