目錄
什麼是輸出緩衝區?
預設PHP輸出緩衝區
訊息頭和訊息體
使用者輸出緩衝區(user output buffers)
输出缓冲区的内部实现
陷阱
总结
首頁 後端開發 php教程 php的輸出緩衝區詳解

php的輸出緩衝區詳解

Mar 21, 2018 am 11:32 AM
php 緩衝區 輸出

大家都知道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

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

熱門話題

Java教學
1664
14
CakePHP 教程
1423
52
Laravel 教程
1317
25
PHP教程
1268
29
C# 教程
1243
24
在PHP API中說明JSON Web令牌(JWT)及其用例。 在PHP API中說明JSON Web令牌(JWT)及其用例。 Apr 05, 2025 am 12:04 AM

JWT是一種基於JSON的開放標準,用於在各方之間安全地傳輸信息,主要用於身份驗證和信息交換。 1.JWT由Header、Payload和Signature三部分組成。 2.JWT的工作原理包括生成JWT、驗證JWT和解析Payload三個步驟。 3.在PHP中使用JWT進行身份驗證時,可以生成和驗證JWT,並在高級用法中包含用戶角色和權限信息。 4.常見錯誤包括簽名驗證失敗、令牌過期和Payload過大,調試技巧包括使用調試工具和日誌記錄。 5.性能優化和最佳實踐包括使用合適的簽名算法、合理設置有效期、

解釋PHP中的晚期靜態綁定(靜態::)。 解釋PHP中的晚期靜態綁定(靜態::)。 Apr 03, 2025 am 12:04 AM

靜態綁定(static::)在PHP中實現晚期靜態綁定(LSB),允許在靜態上下文中引用調用類而非定義類。 1)解析過程在運行時進行,2)在繼承關係中向上查找調用類,3)可能帶來性能開銷。

php程序在字符串中計數元音 php程序在字符串中計數元音 Feb 07, 2025 pm 12:12 PM

字符串是由字符組成的序列,包括字母、數字和符號。本教程將學習如何使用不同的方法在PHP中計算給定字符串中元音的數量。英語中的元音是a、e、i、o、u,它們可以是大寫或小寫。 什麼是元音? 元音是代表特定語音的字母字符。英語中共有五個元音,包括大寫和小寫: a, e, i, o, u 示例 1 輸入:字符串 = "Tutorialspoint" 輸出:6 解釋 字符串 "Tutorialspoint" 中的元音是 u、o、i、a、o、i。總共有 6 個元

什麼是PHP魔術方法(__ -construct,__destruct,__call,__get,__ set等)並提供用例? 什麼是PHP魔術方法(__ -construct,__destruct,__call,__get,__ set等)並提供用例? Apr 03, 2025 am 12:03 AM

PHP的魔法方法有哪些? PHP的魔法方法包括:1.\_\_construct,用於初始化對象;2.\_\_destruct,用於清理資源;3.\_\_call,處理不存在的方法調用;4.\_\_get,實現動態屬性訪問;5.\_\_set,實現動態屬性設置。這些方法在特定情況下自動調用,提升代碼的靈活性和效率。

PHP和Python:比較兩種流行的編程語言 PHP和Python:比較兩種流行的編程語言 Apr 14, 2025 am 12:13 AM

PHP和Python各有優勢,選擇依據項目需求。 1.PHP適合web開發,尤其快速開發和維護網站。 2.Python適用於數據科學、機器學習和人工智能,語法簡潔,適合初學者。

PHP行動:現實世界中的示例和應用程序 PHP行動:現實世界中的示例和應用程序 Apr 14, 2025 am 12:19 AM

PHP在電子商務、內容管理系統和API開發中廣泛應用。 1)電子商務:用於購物車功能和支付處理。 2)內容管理系統:用於動態內容生成和用戶管理。 3)API開發:用於RESTfulAPI開發和API安全性。通過性能優化和最佳實踐,PHP應用的效率和可維護性得以提升。

PHP:網絡開發的關鍵語言 PHP:網絡開發的關鍵語言 Apr 13, 2025 am 12:08 AM

PHP是一種廣泛應用於服務器端的腳本語言,特別適合web開發。 1.PHP可以嵌入HTML,處理HTTP請求和響應,支持多種數據庫。 2.PHP用於生成動態網頁內容,處理表單數據,訪問數據庫等,具有強大的社區支持和開源資源。 3.PHP是解釋型語言,執行過程包括詞法分析、語法分析、編譯和執行。 4.PHP可以與MySQL結合用於用戶註冊系統等高級應用。 5.調試PHP時,可使用error_reporting()和var_dump()等函數。 6.優化PHP代碼可通過緩存機制、優化數據庫查詢和使用內置函數。 7

PHP的持久相關性:它還活著嗎? PHP的持久相關性:它還活著嗎? Apr 14, 2025 am 12:12 AM

PHP仍然具有活力,其在現代編程領域中依然佔據重要地位。 1)PHP的簡單易學和強大社區支持使其在Web開發中廣泛應用;2)其靈活性和穩定性使其在處理Web表單、數據庫操作和文件處理等方面表現出色;3)PHP不斷進化和優化,適用於初學者和經驗豐富的開發者。

See all articles