在nginx下利用php配置SSE的正确方法

368 查看

SSE简介

SSEServer-Sent Events的缩写。通常情况下,是我们的浏览器向服务器发起请求后,服务器响应,然后关闭连接。为了能够保持通信,以便在服务器有事件发生时主动通知浏览器,后来人们又发明了很多技术,包括websocket等。但是websocket对于代码改动较大,所以又出现了SSE,它的特点是基本不用改写原有的逻辑,只是增加一些小的改动就能实现服务器与客户端之间的长连接,达到服务器主动通知客户端的目的。

但是我在按照网上教程真正去在nginx环境下实现SSE时,颇费了一番周章,留在这里,以便有同学遇到类似问题时参考。

客户端

SSE的主要原理是由客户端,也就是浏览器里的javascript发起一个类似于ajax的请求,但和ajax不同的是,这是一个一直保持的长连接,一旦请求建立之后,客户端开始安静地等待服务端向它发回数据,这个连接可以保持很长很长时间。

所以客户端的代码很简单:

source = new EventSource('http://api.server.com/path/file.php?param=value');
source.onmessage = function (event) {
    json = JSON.parse(event.data);
    ... 后面是你处理数据的部分 ...
};

不用担心断掉,即使断掉的话,客户端会在断掉之后的3秒之后自动再次重新向服务端发起连接请求,而且这个重新连接是浏览器自动帮助我们实现的,我们在编程时可以完全不去考虑它。

服务器端

php部分

header('Access-Control-Allow-Origin: http://www.server.com');
//发送SSE应答
header('X-Accel-Buffering: no');
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');

$old_md5 = '';
//执行100次,每次睡眠3秒钟,总共300秒,也就是5分钟
for ($i=0; $i<100; $i++) {
    //保持一个长连接,每隔3秒钟回答客户端一次
    $o_data = 你的数据;
    $str_return = json_encode($o_data);
    $md5 = md5($str_return);
    if ($md5 != $old_md5) {
        //如果内容发生了变化,则推送,否则不必推送,以节省网络流量
        echo 'data: ' . $str_return . "\n\n";
        $old_md5 = $md5;
    }
    ob_flush();
    flush();
    //等待3秒钟,开始下一次查询
    sleep(3);
}

逐行分解一下:

这三句话必不可少:

header('X-Accel-Buffering: no');
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');

其中的第一句话,是配置nginx必需的。因为nginx缺省会对所有来自php的数据作缓存,它一定要等到所有数据全部写进缓存后才一股脑发给客户端,而我们要建立的是一个很长的连接,这个连接过程中随时可能要发送数据,不需要缓存,所以这里必须通过X-Accel-Buffering告诉nginx你对我这段代码不要给我做缓存,否则你会在客户端等很长很长时间却一个字符也收不到。

注意后面那个100次的for循环,网上几乎所有的例子里在这里都会使用while(1)的一个无限循环,但我个人的经验恰恰相反,在这里应该是一个for循环,而不是while循环。为什么呢?因为我们必须使得整个程序的执行时间可控,如果你用for循环100次,每次sleep 3秒钟的话,整个脚本的执行时长也就是在5分钟左右,而如果你使用while无限循环的话,整个时间会几乎是无限长。有的同学可能会说我在php.ini里设置了max_excution_time60秒钟啊,很好,我一开始也是这么认为的,但是:sleep(3)的时间不算在内!所以你可以想象一下你的脚本要执行多长时间才会结束并安静地从内存中退出去?

nginx设置

最后,还需要配置nginx,只需要一句话,在你的nginx.conflocation里:

fastcgi_read_timeout 600s;

这是由于nginx在和我们的php-fpm进行通讯的时候,这个地方的缺省值是60,如果它等了60秒,结果php-fpm一个字符也没有送过来的话,nginx会强行中止与我们的程序的连接,但我们的程序实际还没有执行结束,这时候js客户端发现连接断了,就会自动重启一个新的连接,结果我们服务器端的资源很快被耗尽了。在这里设为600秒的意思是告诉nginx,即使我的php代码一个字符也不给你发送,你也必须老老实实呆够10分钟才可以退出。而实际上是,还记得我们前面的for循环吗?我们会在100次等待3秒后,也就是5分钟内退出我们的php脚本执行,不会凑够10分钟的。所以这里很安全。