前言
默认情况下,我们在PHP里使用echo
等函数输出的内容,是不会马上发送给前端的,原因是有 buffer 的存在,buffer又分两处,一处是PHP本身的buffer,另一处是Nginx的buffer。只有当buffer满了之后,内容才会发送。
但有时候我们会希望输出的内容可以马上发送给前端,例如类似ChatGPT之类的应用,回答都是一个一个字的实时输出的,给用户良好的体验。
那么,怎么关闭 PHP 和 Nginx 的 buffer 呢?
环境
Nginx 1.19
PHP 7.4
解决方法
一、PHP的buffer
PHP里有两个函数可以关闭buffer缓冲,一个是ob_end_flush,一个是ob_end_clean,前者是输出缓冲区内容后关闭缓冲区,后者是销毁缓冲区内容直接关闭。
但即使我们关闭了 PHP 的缓冲区,每次输出完内容也还是要手动 flush 的,例如:
echo 'Hello World';
flush();
每次echo
完都要调用一次flush
函数,太麻烦了,此时我们可以使用ob_implicit_flush函数来解决这个问题。
二、Nginx的buffer
Nginx有两种方法关闭缓冲区,第一种是改Nginx的配置文件:
加上图中红色框的配置指令就可以。
这种改配置文件的方法影响范围会比较大,会导致所有的PHP请求都会关闭缓冲区,不太推荐。
第二种方法是在 PHP 里输出 HTTP 响应头,只要在响应头里加上一个X-Accel-Buffering: no
,Nginx看到此响应头就会放弃使用buffer缓冲。由于这种方法是通过代码来控制,所以影响范围我们可以自由操控,推荐使用。
PHP代码示例
function stream()
{
// 如果缓冲区没有开启,直接调用ob_end_clean()会报错的,要先判断缓冲区有没有开启
// 如果ob_get_contents()不是返回false,说明有开启缓冲区(ob_start())
$buf = ob_get_contents();
if ($buf !== false) {
// 输出header前,不能有任何输出内容,否则会报错,所以缓冲区里的内容要全部清空
ob_end_clean();
}
ob_implicit_flush(); // 每次输出后都自动flush,这样就不需要咱们手动flush了
// 输出header,让Nginx不要使用buffer
header('X-Accel-Buffering: no');
// 每隔一秒输出一个数字
for ($i = 0; $i < 10; $i++) {
echo "$i\n";
sleep(1);
}
}
stream();
在 PowerShell 命令行中访问此页面,可以看到数字会一个一个的实时显示出来:
为什么要在命令行中访问?
答:因为有部分前端程序也是有自己的缓冲区的,即使后端实时输出内容了,前端也不会马上显示出来,为了避免这种问题,使用命令行来访问就很适合了。
PHP curl调用
如果用 PHP 的 curl_exec 函数调用以上实时输出的接口,默认情况下是要等全部内容输出完毕后,才会得到响应结果的,比如下面这段代码:
// 实时输出内容的第三方接口,每隔1秒输出一个数字,输出10个,全部输出完需要10秒时间
$url = "http://www.demo.com/stream";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$result = curl_exec($ch); // 要等待10秒,才能获取到第三方接口的全部输出内容,然后一次性var_dump出来
var_dump($result);
那么怎样才能实时获取第三方接口的输出内容呢?可以使用 CURLOPT_WRITEFUNCTION 配置项:
function write_fn($ch, $data)
{
var_dump($data); // 实时获取接口输出内容,并打印
return strlen($data);
}
$url = "http://www.demo.com/stream";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_WRITEFUNCTION, 'write_fn'); // 设置了这个之后,curl_exec会返回bool值,而不是响应结果字符串(即使CURLOPT_RETURNTRANSFER设置为了1)
$result = curl_exec($ch);
if ($result === false) {
var_dump('调用接口失败');
} else {
var_dump('调用接口成功');
}