以前总以为 echo
,print
之类的输出工具背后没什么了不起的逻辑,不就是给个字符串让终端显示一下吗?其实不然,每一个精心设计的语言特性,函数,方法…背后都可谓独具匠心,你看,PHP 语言从来都不会设计一个一无是处的工具出来,那些雕琢出来的特性都是那么的恰到好处。
缓冲区的一些说明
- 正常情况下,任何会输出内容的函数都会用到输出缓冲区
- 输出缓冲区不是唯一用于缓冲输出的层,它实际上是很多层中的一个
SAPI
中的输出缓冲区
数据写入顺序:echo/print
=> PHP 输出缓冲区 => SAPI
缓冲区 => TCP
缓冲区 => 浏览器

默认的输出缓冲区
在 PHP-FPM
中,与缓冲区相关的配置。
这些值通过 ini_set()
设置后也不起作用,换句话说就是设置的太迟了,因为输出缓冲区层在 PHP 程序启动时,还没有运行任何脚本解析之前就已经启动了。这些值需要在 php.ini
或者在执行程序时使用 -d
选项才有效
output_buffering
默认值为 4096
,设置为 Off
或者 0
,表示禁用输出缓冲区;设置为 On
,表示输出缓冲区不受限制,慎用;
implicit_flush
默认值为 Off
,设置为 On
,表示一旦有任何输出写到 SAPI
缓冲区层,它都会立即刷新,也就是把数据写到更低层,并且缓冲区会被清空
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
实例
设置缓冲区大小为 16 字节,使用 PHP 内置的 Web 服务器 SAPI。
1
| $ php -d output_buffering=16 -d implicit_flush=1 -S 127.0.0.1:8080 -t ~/Downloads/test
|
浏览器访问脚本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| <?php
// 程序脚本运行的后向缓冲区写入 15 个字节
echo str_repeat('Hello', 3);
// 进入 2 秒休眠状态
sleep(2);
// 此时这个字节填满了缓冲区,缓冲区会立即刷新自身,把数据传递给 SAPI 层的缓冲区
// 由于 implicit_flush=1,SAPI 层的缓冲区也会立即刷新到下一层
// 浏览器输出 HelloHelloHelloW
echo 'W';
// 进入 2 秒休眠状态
sleep(2);
// 此时将这 2 个字节写入到缓冲区,由于还不够填满缓冲区,这时还不会输出到浏览器
echo 'or';
// 进入 2 秒休眠状态
sleep(2);
// 此时脚本执行完毕,在执行完毕之前,将这 2 个字节写入到缓冲区,还是不够填满缓冲区
// 但这时脚本已经执行完毕,缓冲区将已有的数据全部输出浏览器
echo 'ld';
|
消息头和消息体
如果使用了输出缓冲区层,那么 PHP 会接管这些消息头和消息体的发送。
PHP 中有关与消息头的函数都使用了内部的 sapi_header_op()
函数,这个函数负责把内容写入到消息头缓冲区中,所以我们才能优雅的使用 header()
,setcookie()
诸如此类的方法。
在输出内容时,内容会先被写入到输出缓冲区(可能是多个),当缓冲区中的内容需要被发送时,PHP 会先发送消息头,再发送消息体,你看,所有的这些都不费吹灰之力,PHP 为我们搞定了所有的事情。
用户的输出缓冲区
多个缓冲区会组成一个堆栈结构,每个新建的缓冲区都会堆叠到之前的缓冲区上,每当它被填满或者溢出,都会执行刷新操作,然后把其中的数据传递给下一个缓冲区。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
| <?php
ob_start(function ($parameter) {
static $index = 0;
return $index++ . ': ' . $parameter . "\n";
}, 6);
ob_start('handle', 3);
function handle($parameter)
{
return ucfirst($parameter);
}
// 此时字符串被写入到第二个缓冲区,缓冲区的长度不够设定值,不会刷新这个缓冲区
echo 'fo';
// 休眠 2 秒
sleep(2);
// 1. 此时字符串写入到第二个缓冲区后,由于 chunk_size 为 3,所以第二个缓冲区会刷新
// 2. 并且将返回的字符串 Foo 写入到第一个缓冲区
// 3. 此刻第一个缓冲区未被填满
echo 'o';
// 休眠 2 秒
sleep(2);
// 1. 此时字符串写入到第二个缓冲区后,立即刷新缓冲区
// 2. 并且将返回的字符串 HelloWorld 写入到第一个缓冲区
// 3. 上一次缓冲区的 Foo 与 本次的 HelloWorld 合并
// 4. 由于第一个缓冲区 chunk_size 为 10,这时缓冲已经被填满,所以第一个缓冲区会刷新输出到浏览器
echo 'helloWorld';
// 休眠 2 秒
sleep(2);
// 1. 此时字符串写入到第二个缓冲区后,立即刷新缓冲区
// 2. 并且将返回的字符串 Enough 写入到第一个缓冲区
// 3. 由于上一次的缓冲被填满刷新输出了,正好本次的缓冲区也刚好被填满,所有第一个缓冲区会刷新输出到浏览器
echo 'enough';
sleep(2);
// 重复此前的步骤,由于脚本已经执行结果,被迫刷新输出到浏览器
echo 'exit';
// 结果
0: FooHelloWorld 1: Enough 2: Exit
|
输出缓冲区实例
默认的缓冲区输出
在脚本处理结束之前,浏览器端不会输出,由于数据量太小,输出缓冲区没有写满。写入数据的顺序:PHP 缓冲区、TCP 缓冲区、浏览器
1
2
3
4
5
6
7
8
| <?php
$i = 0;
while ($i < 10) {
echo $i . '<br>';
$i++;
sleep($i);
}
|
关闭后的缓冲区输出
将 output_buffering
的值改为 0
,执行脚本后,因应缓冲区的容量设置为 0
,即禁用了 PHP 缓冲区机制。这时浏览器端会按程序定义的时间间隔不断输出,不会出现浏览器界面空白等待的情况。写入数据的顺序:TCP 缓冲区、浏览器
1
2
3
4
5
6
7
8
9
| <?php
$i = 0;
while ($i < 10) {
echo $i . '<br>';
flush();
sleep(1);
$i++;
}
|
自定义的缓冲区输出
将 output_buffing
值改为默认的 4096
使用 dd
命令准备一个大小为 4096
文件
1
2
3
| $ dd if=/dev/zero of=file4096 bs=4096 count=1
$ ll file4096
-rw-r--r-- 1 majinyun staff 4.0K Mar 16 11:16 file4096
|
脚本在每次执行时,由于读取的文件内容大小正好等于缓冲区的大小,所以会立即刷新缓冲区,输出到客户端浏览器,在此过程中浏览器不会出现空白等待期,而会持续输出程序中指定的内容
1
2
3
4
5
6
7
| <?php
$filename = dirname(dirname(__DIR__)) . '/../Downloads/test/file4096';
for ($i = 0; $i < 10; $i++) {
echo file_get_contents($filename) . $i . '<br>';
sleep(1);
}
|
使用 ob_start 后的缓冲区输出
使用 ob_start()
后,PHP 的缓冲区会被扩展到足够大,直到 ob_end_flush()
函数调用或者脚本运行结束后,才发送缓冲区中的数据到客户端浏览器
1
2
3
4
5
6
7
8
9
10
| <?php
ob_start();
$filename = dirname(__DIR__, 3) . '/Downloads/test/f4096';
for ($i = 0; $i < 10; $i++) {
echo file_get_contents($filename) . $i . '<br>';
sleep(1);
}
ob_end_flush();
|
执行脚本后,ob_get_contents()
会获取一分缓冲区中的副本,结果中有两次输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| <?php
ob_start();
echo 'hello world' . '<br>';
$contents = ob_get_contents();
ob_end_flush();
echo $contents;
// 结果
hello world
hello world
|
执行脚本结束后,由于使用了 ob_end_clean()
函数,将 PHP 缓冲区中的内容给删除了,所以结果中仅有一次输出
1
2
3
4
5
6
7
8
9
10
11
12
13
| <?php
ob_start();
echo 'hello world' . '<br>';
$contents = ob_get_contents();
ob_end_clean();
echo $contents;
// 结果
hello world
|