PHP输出缓冲

对于PHP输出的内容(echo、print等等),会先放到输出缓冲,当缓冲区写满了或者程序执行完毕了才会转移到下一层,这么做可以合并数据减少传输次数。

ob-main

PHP涉及到的输出缓冲有三层。

上面的层级关系从上到下,所有输出会依次按顺序进入,只有当前面的缓冲层满了,或者手动冲刷,又或者是代码运行完了,才会转移到下一层。

什么时候冲刷

在非CLI环境下,PHP的默认输出缓冲是默认开启的,缓冲大小是4096字节,这也就是为什么下面的代码,在我们用浏览器打开页面的时候要等两秒才能看到666。

index.php:

<?php
echo 666;
sleep(2);

而同样的代码在CLI环境下截然不同,你可以在命令行执行php -f index.php看看,马上会输出666。这是因为在CLI的SAPI下,PHP默认不使用输出缓冲(output_buffering = Off,implicit_flush = On),所有输出会立即转移到下一层。

假如我们把PHP的配置改一下:

php -d output_buffering=2 -d implicit_flush=1 -S 127.0.0.1:8080

这时候我们用浏览器打开127.0.0.1:8080,666马上就显示出来了,因为我们把PHP的默认输出缓冲区设置为2个字节了,666是3个字节不够放了,这时候只能移交到下一层的SAPI缓冲层了,而又因为我们设置了implicit_flush=1,SAPI的缓冲层也立马将输出发给浏览器了。

直接修改PHP配置不是一个明智的选择,毕竟不是所有脚本要处理的情况都是一模一样的,这时候我们可以通过PHP提供的ob系列函数来控制:

echo 666;
ob_flush();
flush();
sleep(2);
php -S 127.0.0.1:8080

浏览器打开127.0.0.1:8080,PHP以CGI模式运行,不改变PHP配置的情况下,666立马显示了出来。

我们调用了ob_flush()和flush(),这两个函数是干嘛的?怎么感觉他两有点眼熟,好像是经常在一起的。

ob_flush()和flush()

ob_flush()是冲刷PHP的默认输出缓冲层,flush()是冲刷SAPI缓冲层。

我们输出的666并没有让PHP的默认输出缓冲层溢出,这样就到不了SAPI缓冲层也就更谈不上给浏览器了。调用ob_flush()之后会冲刷到SAPI缓冲层,而flush()则是告诉Apache:你也立马把内容发给浏览器吧。Apache也乖乖地从SAPI缓冲层拿到内容直接发给浏览器了。

合理flush可以提高用户体验

正常情况下,4096字节是一个不错的值,设置得太小,SAPI和客户端的传输次数就会变多,但假如我们的页面有些模块加载起来特别耗时,我们可以先把那些能够快速拿到的内容先冲刷给浏览器,这样用户就不会等半天还是个白屏状态。

echo 666;
ob_flush();
flush();

// 耗时任务
sleep(2);
$a = 1;
echo $a;

其中echo 666那块可以自己脑补,比如可以是个网站头部,有登录注册按钮,有导航之类的,这些内容很快就拿到了,用户不用等你写的很烂的sql查询完了才能点击美女频道。

用户缓冲层

上面说到的都是PHP的默认缓冲层,在它之上还可以有用户缓冲层,我们可以通过ob_start()创建用户缓冲区,用户缓冲区可以创建多个,最后创建的会在最上面,这样层层堆叠下来,如果没有关闭,输出会依次进入这些缓冲区,然后到PHP默认缓冲层再到SAPI缓冲层。

echo ob_get_level()."\n"; // 1,PHP默认缓冲层
ob_start(); // 新建一个用户缓冲区,假设它叫UC1
echo ob_get_level()."\n"; // 因为上面新建了一个缓冲区,所以这里会显示2
echo "abc\n";
ob_end_flush(); // 冲刷并关闭最顶端的缓冲区,这里是UC1
echo ob_get_level()."\n"; // 1,UC1已经被关闭了
ob_flush(); // 冲刷PHP默认缓冲层
flush(); // 冲刷SAPI缓冲层
sleep(2);

注意ob_start()以后会创建一个用户缓冲区,它在默认缓冲层之上,而ob_end_flush()是冲刷缓冲层的最顶端,也就是刚刚创建的用户缓冲区,这时候输出从UC1刷到了默认缓冲层。

有人说用默认缓冲层就够了,还搞什么用户缓冲层。下面就来看看用户缓冲区一般用来干嘛。

用户缓冲层应用场景

  1. 对第三方API输出做修改

在ThinkPHP框架中有个dump函数,是var_dump的改良版,能以更友好的格式显示变量结构:

/**
 * 浏览器友好的变量输出
 * @access public
 * @param  mixed       $var   变量
 * @param  boolean     $echo  是否输出(默认为 true,为 false 则返回输出字符串)
 * @param  string|null $label 标签(默认为空)
 * @param  integer     $flags htmlspecialchars 的标志
 * @return null|string
 */
public static function dump($var, $echo = true, $label = null, $flags = ENT_SUBSTITUTE)
{
    $label = (null === $label) ? '' : rtrim($label) . ':';

    ob_start();
    var_dump($var);
    $output = preg_replace('/\]\=\>\n(\s+)/m', '] => ', ob_get_clean());

    if (IS_CLI) {
        $output = PHP_EOL . $label . $output . PHP_EOL;
    } else {
        if (!extension_loaded('xdebug')) {
            $output = htmlspecialchars($output, $flags);
        }

        $output = '<pre>' . $label . $output . '</pre>';
    }

    if ($echo) {
        echo($output);
        return;
    }

    return $output;
}

它里面就是用到了输出缓冲,将本来var_dump输出的内容放到一块用户缓冲区,然后再拿出来通过一个正则替换再输出,如果不借助输出缓冲的话做起来是很费力的,比如自己重新写一个。

总结