声明:不同的PHP版本,操作系统环境,Webserver, 参数默认值及配置可能会有差异。本文基于的环境如下:
PHP version: 7.2.8; Nginx version 1.14.0
Mac OS version: 10.10.5; Chrome version: 69.0;
Safari version: 8.0.8
计算机缓冲区(buffer)
计算机中暂时存放输出或输入信息的内存区域。缓和高速部件和低速部件之间通信速度不匹配的矛盾。缓冲(buffer)和缓存(cache)之间还是有本质区别的【点击查看区别】。
PHP输出缓冲区
输出缓冲区顾名思义是输出信息暂时存放的内存区域,通过ob_*系列函数来控制输出缓冲区。当php脚本执行结束(会自动调用ob_flush())或强制刷新(手动调用ob_fush())缓冲区后,才会把数据发送给Nginx fastcgi客户端。当然PHP还有其他的缓冲区,比如字符串缓冲区finfo::buffer。
PHP输出缓冲区默认是关闭的,可以在php.ini中开启,生产环境官方建议大小是4096字节。开启后对所有php页面都生效。
output_buffering = 4096 // 其他值说明 ; On = Enabled and buffer is unlimited. (小心使用) ; Off = Disabled ; Integer = Enables the buffer and sets its maximum size in bytes. ; Note: This directive is hardcoded to Off for the CLI SAPI
另外一种在页面中单独开启缓冲区的办法是ob_start()函数。
bool ob_start ([ callback $output_callback [, int $chunk_size [, bool $erase ]]] )
ob_start()有三个参数,$chunk_size是用来设置缓冲区大小,可以设置0-4096,默认是0表示大小不限。
PHP-FPM缓冲区(暂时这么认为,如果读者有更好的建议,欢迎留言)
参考《PHP output buffer – bigpipe基础》。这篇文章提到认为是fpm缓冲区,但是官网没有明确说明,测试验证确实有8K的缓冲,通过flush()刷新。flush()函数严格来说, 这个只有在PHP做为Apache的Module安装的时候, 才有实际作用,它是刷新WebServer(可以认为特指Apache)的缓冲区。参考鸟哥文章深入理解flush和ob_flush区别。所以flush()并不能刷新Nginx fastcgi缓冲区。
Nginx缓冲区
Nginx默认不会实时把php-fpm响应的数据返回给客户端,而是暂存在Nginx缓冲区中。当php脚本执行结束(自动调用flush())或强制刷新(手动flush())缓冲区后,才会把数据发送给客户端。Nginx buffer有fastcgi_buffering和porxy_buffering两种。Nginx通过fastcgi_buffer*相关指令来控制fastcgi_buffering缓冲区。通过porxy_buffer*指令来控制porxy_buffering。
浏览器缓冲区
浏览器默认不会实时显示从Nginx返回的数据,而是把接受到的数据暂存在浏览器缓冲区中,当缓冲区满后,才会开始显示。不同的浏览器缓冲区大小不同。实际测试发现Mac 下chrome和safari都需要输出1024字节。没有找到刷新缓冲区的办法,可以通过发送额外的空格来解决。
或者通过curl来请求,通过–no-buffer来禁用curl buffer。
curl 'niliu.me' --no_buffer
把上面三个部分串起来,完整的流程如下图【下图只是自己理解使用,仅供参考】:
一、PHP输出缓冲区和Nginx缓冲区都关闭。
0、php.ini关闭PHP输出缓冲区。
output_buffering = Off
1、关闭nginx缓冲区
方式一、nginx.conf中关闭Nginx fastcgi buffer缓冲区。
fastcgi_buffering off;
方式二、PHP文件中起始行增加头信息。
header(‘X-Accel-Buffering: no’);
2、浏览器请求脚本,先输出hello, 过2秒输出php。
header('X-Accel-Buffering: no'); echo str_pad('hello', 1024); // 测试发现调用flush()后,chrome不需要向浏览器发送额外的空格,浏览器也可以实时显示 flush(); // 不加flush(), 输出8K内容也是可以的。 // echo str_pad('hello', 1028 * 8); sleep(2); echo 'php'; die;
二、PHP输出缓冲区关闭,Nginx缓冲区开启。
0、php.ini关闭PHP输出缓冲区。
output_buffering = Off
1、nginx.conf中开启fastcgi 缓冲,buffer size是默认大小4K。(当然也可以在PHP脚本中通过header(‘X-Accel-Buffering: yes’);开启)
fastcgi_buffering on;
fastcgi_buffer_size 4k;
fastcgi_buffers 8 4k;
2、浏览器请求脚本,先输出hello, 过2秒输出php。
echo str_pad('hello', 1024 * 4); flush(); sleep(2); echo 'php'; die;
三、PHP输出缓冲区开启,Nginx缓冲区关闭。
0、php.ini开启PHP输出缓冲区。
output_buffering = 4096
1、nginx.conf中开启fastcgi 缓冲,buffer size是默认大小4K。
fastcgi_buffering off;
2、浏览器请求脚本,先输出hello, 过2秒输出php。
echo str_pad('hello', 1024); // 要通过ob_flush()把输出缓冲区的内容冲出。 ob_flush(); flush(); sleep(2); echo 'php'; die;
四、PHP开启多级输出缓冲区,Nginx缓冲区关闭。
0、php.ini开启PHP输出缓冲区。
output_buffering = 4096
1、nginx.conf中开启fastcgi 缓冲,buffer size是默认大小4K。
fastcgi_buffering off;
2、浏览器请求脚本,先输出hello, 过2秒输出php。
// php.ini中开启了缓冲区,在通过ob_start()开启不限制大小的缓冲区,需要通过ob_end_flush()冲出缓冲区内部嵌套的缓冲,在通过ob_flush()冲出到Nginx fastcgi ob_start(); echo str_pad('hello', 1024); ob_end_flush(); ob_flush(); flush(); sleep(2); echo 'php'; die;
注:有些人提到php.ini中开启了PHP输出缓冲区,ob_start()才生效,应该是一个误区,之间没有依赖关系。
五、PHP输出缓冲区开启,Nginx缓冲区开启。
0、php.ini开启PHP输出缓冲区。
output_buffering = 4096
1、nginx.conf中开启fastcgi 缓冲,buffer size是默认大小4K。(当然也可以在PHP脚本中通过header(‘X-Accel-Buffering: yes’);开启)
fastcgi_buffering on;
fastcgi_buffer_size 4k;
fastcgi_buffers 8 4k;
2、浏览器请求脚本,先输出hello, 过2秒输出php。
// nginx fastcgi缓冲区大小是4K,需要发送额外4K空格 echo str_pad('hello', 1024 * 4); ob_flush(); flush(); sleep(2); echo 'php'; die;
完整写法,参考php手册flush()。
if (ob_get_level() == 0) { ob_start(); } for ($i = 0; $i<10; $i++){ echo "Line to show."; echo str_pad('',4096)."\n"; ob_flush(); flush(); sleep(2); } echo "Done."; ob_end_flush();
问题:
很多人提到用下面的方式发送额外的空格到浏览器, 但是hello php还是同出显示。
echo str_repeat(' ', 4096); // 注意引号之间有空格 ob_flush(); flush(); sleep(2); echo 'php'; die;
解决:需要在输出空格之前,输出一个非空字符。比如echo 1。怀疑空格是被Nginx或浏览器过滤了。
数据下载:参考下篇博文《PHP实现浏览器文件下载》
文件下载:整理待定。
参考:
从php到浏览器的缓存机制,不得不看
深入理解PHP输出缓冲区
php+nginx实时输出的问题
信海龙:从php缓冲区说起
curl no buffer请求不错
对,超级方便