前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >PHP 与 JavaScript SSE:实现服务器实时推送功能的绝佳组合

PHP 与 JavaScript SSE:实现服务器实时推送功能的绝佳组合

作者头像
Tinywan
发布2023-10-22 18:57:45
5230
发布2023-10-22 18:57:45
举报
文章被收录于专栏:开源技术小栈开源技术小栈

简介

SSE 的全称是 Server Sent Events,即服务器推送事件。它是一种基于 HTTP 的服务器到客户端的单向(半双工)通信机制,使服务器能够主动将实时数据推送给客户端,而不需要客户端多次发起请求。 官方文档:https://developer.mozilla.org/en-US/docs/Web/API/EventSource

数据通信

问题解决

常规的HTTP请求响应流程无法做到服务器主动推送数据到客户端,SSE可以解决此问题。

适用场景

实时更新订阅数据、实时通知、实时日志监控、实时数据统计、简单的文本数据传输。

示例代码

服务端

代码语言:javascript
复制
// 这行代码用于关闭输出缓冲。关闭后,脚本的输出将立即发送到浏览器,而不是等待缓冲区填满或脚本执行完毕。
ini_set('output_buffering', 'off');
// 这行代码禁用了 zlib 压缩。通常情况下,启用 zlib 压缩可以减小发送到浏览器的数据量,但对于服务器发送事件来说,实时性更重要,因此需要禁用压缩。
ini_set('zlib.output_compression', false);
// 这行代码使用循环来清空所有当前激活的输出缓冲区。ob_end_flush() 函数会刷新并关闭最内层的输出缓冲区,@ 符号用于抑制可能出现的错误或警告。
while (@ob_end_flush()) {}
// 这行代码设置 HTTP 响应的 Content-Type 为 text/event-stream,这是服务器发送事件(SSE)的 MIME 类型。
header('Content-Type: text/event-stream');
// 这行代码设置 HTTP 响应的 Cache-Control 为 no-cache,告诉浏览器不要缓存此响应。
header('Cache-Control: no-cache');
// 这行代码设置 HTTP 响应的 Connection 为 keep-alive,保持长连接,以便服务器可以持续发送事件到客户端。
header('Connection: keep-alive');
// 这行代码设置 HTTP 响应的自定义头部 X-Accel-Buffering 为 no,用于禁用某些代理或 Web 服务器(如 Nginx)的缓冲。这有助于确保服务器发送事件在传输过程中不会受到缓冲影响
header('X-Accel-Buffering: no');


/**
 * @function 封装sse格式的数据
 * @param  $data string
 * @return string
 */
function sse($data) {
    //data:\n\n不能少,sse固定格式
    return "data:{$data}\n\n";
}

// 开启输出缓冲
ob_start();
while (true) {
    $json = json_encode(['data' => ['time' => date('Y-m-d H:i:s')]], JSON_UNESCAPED_UNICODE);
    echo sse($json);
    //刷新缓冲区
    ob_flush();
    //将输出缓冲区的内容立即发送到客户端
    flush();
    sleep(1);
}

客户端

代码语言:javascript
复制
<!doctype html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>

</body>

<script>
    if(!! window.EventSource) {
        var sse = new EventSource('http://127.0.0.1/test/sse.php');
        //通信事件
        sse.onmessage = function (event) {
            var response  = JSON.parse(event.data);
            console.log(response.data.time);
        };

        // 打开事件
        sse.onopen = function (event) { console.log('连接成功'); };

        //关闭事件
        sse.onclose = function(event) {console.log('连接关闭');};

        //错误事件
        sse.onerror = function (event) {console.error('连接失败');};
    } else {
        alert('您的浏览器不支持SSE');
    }
</script>
</html>

时同步到SSE模块方案

方案1 纯粹轮询模式

  • 做法:不停对数据库做查询。
  • 优点:实现简单。
  • 缺点:很不优雅的方案,性能消耗大。
  • 场景:数据量不大且赶工时,可作为临时方案。

示例

代码语言:javascript
复制
ob_start();
$user_id = 1; //假设用户id为1,实际可传参获取。
while (true) {
    $notice_count = DB::table('notice')->where('user_id', $user_id)->count();
    echo sse(json_encode(['notice_num' => notice_count]));
    ob_flush();
    flush();
    sleep(1);
}

方案2 基于事件触发,用消息队列做订阅发布模式

  • 做法:对要实时获取的数据,先赋一个初始值的实际值,传递给客户端,当数据发生变化时,触发生产消息* 的通知,SSE模块不停的消费消息。
  • 优点:避免了轮询模式疯狂查询。
  • 缺点:仍旧需要消耗一些资源,实现稍微繁琐。
  • 场景:方法优雅,适用于订阅端根据消息做更复杂的业务逻辑操作时使用。

示例

代码语言:javascript
复制
暂时用redis队列简单实现:技术选型可根据实际情况做高可用或更复杂的设计。

//例如要实现一个通知数量实时变更的功能:
//发布端:
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
//假设用户id为1
$user_id = 1;
//执行完其它的针对notice表写操作的业务逻辑代码...,然后向队列丢一个任务
$redis->lPush('add_one_notice_task:'. $user_id, 1);
//------------------------------------------------------------------------------------------------------------------
//订阅端
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);

//先查询数据库通知数量
$user_id = 1; //假设用户id为1,实际可传参获取。
$notice_count = DB::table('notice')->where('user_id', $user_id)->count();
$redis->set('user_notice_num:'. $user_id, $notice_count);

while (true) {
    //若检测到有自增一个通知数量的任务,则消费时触发一个增加数量的动作。
    $add_one_notice_task = $redis->rPop('add_one_notice_task:'. $user_id);
    if($add_one_notice_task) {
        $redis->incr('user_notice_num:'. $user_id);
    }

    echo sse(json_encode(['notice_num' => $redis->get('user_notice_num:' . $user_id) ?? 0]));
    ob_flush();
    flush();
    sleep(1);
}

方案3 基于事件触发的轮询

  • 做法:触发端直接一步到位,修改好数据后缓存,监听端不停的监听缓存的值。
  • 优点:实现起来比订阅发布简单,又避免轮询频繁查库,通过缓存解耦,避免了方案1的性能问题,又能保证缓存一致性。
  • 缺点:终究还是轮询,仍旧需要消耗一些资源。
  • 场景:相对简单的,不需要在监听端做业务处理,只做纯粹返回数据的场景。

示例

代码语言:javascript
复制
//触发端
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
 //假设用户id为1,实际可传参获取。
$user_id = 1;
//执行完其它的针对notice表写操作的业务逻辑代码...
$notice_count = DB::table('notice')->where('user_id', $user_id)->count();
$redis->set('user_notice_num:'. $user_id, $notice_count);
//------------------------------------------------------------------------------------------------------------------
//监听端
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$user_id = 1; //假设用户id为1,实际可传参获取。
while (true) {
    echo sse(json_encode(['notice_num' => $redis->get('user_notice_num:' . $user_id) ?? 0]));
    ob_flush();
    flush();
    sleep(1);
}

项目案例

代码语言:javascript
复制
/**
 * @function 与客户端server send event通信方式
 * @param    $callback    callable 回调,若返回数组代表要输出json,返回null代表本次循环不进行输出
 * @param    $millisecond int      数据分发间隔,单位:毫秒
 * @return   string
 * @other    void
 */
function sse($callback, $millisecond = 1000) {
    set_time_limit(0);
    ini_set('output_buffering', 'off');
    ini_set('zlib.output_compression', false);
    while (@ob_end_flush()) {}
    header('Content-Type: text/event-stream; Charset=UTF-8');
    header('Cache-Control: no-cache');
    header('Connection: keep-alive');
    header('X-Accel-Buffering: no');
    header("Access-Control-Allow-Origin: *");
    header("Access-Control-Allow-Credentials: true");
    header('Access-Control-Allow-Methods: *');
    header('Access-Control-Allow-Headers: *');

    ob_start();
    while (true) {
        $callback_res = $callback();
        if($callback_res !== null) {
            $data = json_encode($callback_res, JSON_UNESCAPED_UNICODE);
            echo "data:{$data}\n\n";
        }
        ob_flush();
        flush();
        usleep($millisecond * 1000);
    }
}

//调用
sse(function() {
 if('业务逻辑数据存在') {
  return ['k' => 'v'];
 }
 return null;
}, 1000);

SSE优点

  1. 实现简单易用。
  2. 有断线重连的能力,即使网络中断,SSE仍旧会尝试每隔几秒自动重试的机制。
  3. 避免了客户端使用短轮询造成请求量过大的问题,避免在项目中因需要一个实时的通信小模块就需要另外搭建WebSocket的问题,得不偿失。

SSE缺点

  1. 完全不兼容IE浏览器。
  2. SSE是一种半双工通信,因为数据只能在一个方向上流动,即从服务器到客户端。与之相比,全双工通信(例如WebSocket)允许数据在两个方向上同时流动,允许双向的数据传输。
  3. 为了避免滥用和资源占用,一些浏览器可能会限制单个域名下的SSE连接数,例如同时最多打开6个连接。而另一些浏览器可能会限制整个浏览器实例中的SSE连接总数,这个限制不是由JavaScript语言本身所设定的,而是由浏览器实现所定义的。

SSE对比WebSocket

协议区别

协议:SSE是基于HTTP协议,而WebSocket则是独立的协议,它们都可以在浏览器和服务器之间建立持久的连接。

数据格式

SSE通过HTTP协议传输的数据格式是文本(通常是JSON格式),因此它适合用于传输简单的文本数据或者事件。而WebSocket可以传输文本和二进制数据,在处理音频、视频等大型数据时更有优势。

通信方式

SSE基于半双工模式,服务器可以通过发送事件流(event stream)来主动推送数据给客户端。客户端通过监听这些事件来接收数据。而WebSocket是全双工通信协议,客户端和服务器可以随时发送和接收数据。

兼容性

IE10及以上支持 WebSocket。但IE都不兼容SSE,并且不同浏览器对SSE兼容性不一样,可通过Polyfill解决,官网:https://developer.mozilla.org/en-US/docs/Glossary/Polyfill。

本文参与?腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-10-14,如有侵权请联系?cloudcommunity@tencent.com 删除

本文分享自 开源技术小栈 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与?腾讯云自媒体分享计划? ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 简介
  • 数据通信
  • 问题解决
  • 适用场景
  • 示例代码
    • 服务端
      • 客户端
      • 时同步到SSE模块方案
        • 方案1 纯粹轮询模式
          • 示例
        • 方案2 基于事件触发,用消息队列做订阅发布模式
          • 示例
        • 方案3 基于事件触发的轮询
          • 示例
      • 项目案例
      • SSE优点
      • SSE缺点
      • SSE对比WebSocket
        • 协议区别
          • 数据格式
            • 通信方式
              • 兼容性
              相关产品与服务
              云数据库 Redis
              腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
              http://www.vxiaotou.com