如何在需要时(而不是定期)将服务器发送的事件通知发送到特定的浏览器会话?

Posted

技术标签:

【中文标题】如何在需要时(而不是定期)将服务器发送的事件通知发送到特定的浏览器会话?【英文标题】:How to send Server-Sent Event Notifications when needed, not periodically, to a specific browser session? 【发布时间】:2019-08-02 09:18:25 【问题描述】:

我想使用 SSE 将数据从服务器发送到 特定个人 登录到我的网络应用程序的客户端,当他们的会话超时“时钟”时即将到期或当有一条消息应该发送给所有“已连接”的用户时,类似于 UNIX 墙命令,例如“系统将在 5 分钟内不可用,请完成您的事务并注销.'

我在 w3schools 和 MSDN 看到的示例,自动/定期向连接的客户端发送消息,例如传输服务器的时间。

我想避免使用 ajax 请求从客户端轮询服务器,以解决诸如“我仍然登录吗?”之类的问题。 (会话超时过期)或“服务器消息是什么(如果有)?”第一个是特定于用户的,第二个是针对所有当前用户的。

这可以做到吗,有没有例子说明如何做到这一点?

谢谢

自从发布这个问题以来,我在拼凑 SSE 服务器/客户端演示工作方面取得了部分成功。我说,部分成功,因为我无法使用重试选项获得不同的工作时间。如果我不选择该选项,客户端会每三秒收到一条消息。如果我包含该选项,那么客户端只会收到一条消息,并且会尝试与服务器连接。这是php服务器端代码:

<?php
header( 'Content-Type: text/event-stream' );
header( 'Cache-Control: no-cache' );

// data: "msg": "First message"\ndata: "msg": "second message"\n\n

$r  = mt_rand( 1, 10 ) * 1000;
$m  = "retry interval: $r";
$r *= 1000;
$t = date( 'r' );

// echo "retry: $r" . PHP_EOL; // <==== With out this, it works!

echo "data: \"message\" : \"$m\"" . PHP_EOL;
echo "data: \"message\" : \"The server time is: $t\"" .  PHP_EOL;
echo PHP_EOL;
ob_flush();
flush();
?>

这是客户端代码:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Server-Sent Events Example</title>
  </head>
  <body>
    <p>
     <button onclick="source.close();">Stop!</button>
    </p>
    <h1>Getting server updates</h1>
    <div id="result"></div>
    <script>

      // Source: https://www.html5rocks.com/en/tutorials/eventsource/basics/

      if( typeof( EventSource ) !== 'undefined' ) 

        // 'http://symfony.dev.iambreakinout.com/SSE_Server.php'

        var source = new EventSource( 'SSE_Server.php' );

        source.addEventListener(
          'message',
          function( event ) 
            if( event.origin != window.location.origin )     
              alert( 'Cross-site scripting: message origin did not match:' +
                     '\r\n' +
                     'expected origin: ' + window.location.origin + '\r\n' +
                     'actual origin:  ' + event.origin );

              return;

            
            else 
              var data = [];

              for( var i = 0, b = 0, e = 0, d = '';; ++i, b = e + 1 ) 

                e         = event.data.indexOf( "\n", b );
                s         = ( ( e > -1 )
                              ? event.data.substr( b, e )
                              : event.data.substr( b ) );
                data[ i ] = JSON.parse( s ).message;

                if( e === -1 ) break;

              

              document.getElementById( 'result' ).innerHTML +=
                data.join( '<br>' ) + '<br>';

            

          ,
          false );

        source.addEventListener(
          'open',
          function( event ) 
            // Connection was opened.

            document.getElementById( 'result' ).innerHTML += 'Open<br>';
          ,
          false );

        source.addEventListener(
          'error',
          function( event ) 
            var readyState;

            //
            // The closed ready state is seen when an error event occurs, but
            // the rest are shown here as a reminder to me of the defined
            // ready state constant values.
            //

            switch( event.currentTarget.readyState ) 
              case EventSource.CONNECTING: readyState = 'Connecting'; break;
              case EventSource.OPEN:       readyState = 'Open';       break;
              case EventSource.CLOSED:     readyState = 'Closed';     break;
              default:                     readyState = '?';          break;
            
            document.getElementById( 'result' ).innerHTML += readyState +
              '<br>';

          ,
          false );

      
      else 

        document.getElementById("result").innerHTML =
          'Sorry, your browser does not support server-sent events...';

      
    </script>
    <button onclick="source.close();">Stop!</button>
  </body>
</html>

无需更改客户端代码并允许 echo "retry: $r" 。 PHP_EOL; 声明指定重试持续时间会导致输出在显示正在连接后停止。

获取服务器更新

打开

重试间隔:3000

连接

我做错了什么或没有做什么以使重试选项起作用?

再次感谢

好的,目前的代码很好,但是当它生成间隔的随机数时,它真的不应该有第一个'times 1000'。 (呃!)。

【问题讨论】:

【参考方案1】:

您需要将活动用户保留在某种数据结构中,以便能够向他们发送事件。

下面是一个关于NodeJS的简单注释示例(基于https://github.com/mchaov/simple-sse-nodejs-setup/):

const http = require('http');
const msg = " - Simple SSE server - ";
const port = 5000;

// Create basic server
http.createServer(function (request, response) 

    // answer only to event stream requests
    if (request.headers.accept && request.headers.accept == 'text/event-stream') 

        // check if the resource is what we want
        // => http://domain.ext/sse
        // store the user into your data structure
        if (/sse/gim.test(request.url)) 
            sendSSE(request, response);
        
    
    else 
        // if not just return that you are online and a string of text
        response.writeHead(200);
        response.write('Welcome to ' + msg + '@ :' + port);
        response.end();
    
).listen(port);

// Setup the SSE "service" :)
// You need to cache the "response" object per user, and then you can use the functions below to send messages whenever you desire. You can have a collection that holds all the references to the response object and decide if you want to send message to one or all.
function sendSSE(request, response) 

    // Setup headers
    // For ease of use: example for enabled CORS
    response.writeHead(200, 
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache',
        'Connection': 'keep-alive',

        // enabling CORS
        'Access-Control-Allow-Origin': "*",
        'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept'
    );

    var id = (new Date()).toLocaleTimeString();

    // send first message
    constructSSE(response, id, (new Date()).toLocaleTimeString());

    // send message every second and a half
    setInterval(function () 
        constructSSE(response, id, (new Date()).toLocaleTimeString());
    , 1500);


// setup simple message
// call this function with proper response you took from data structure with active users to send message to the specific user
function constructSSE(response, id, data) 
    response.write('id: ' + id + '\n');
    // response.write('event: ' + 'logout' + '\n'); // check this
    response.write("data: " + msg + port + '\n\n');

【讨论】:

感谢您的回复。这样做的目的是添加到我的 PHP/Symfony 2 网络应用程序中,该应用程序使用 JQuery 和 BootStrap 将 HTML 和 javascript 输出到用户的浏览器。我犹豫是否尝试在混合中使用 NodesJS。 好吧,我不懂 PHP,但前提是一样的。你需要一些类来管理你的活跃用户。 再次。谢谢。抱歉,我看不到您的示例如何将注销消息发送给特定用户。这是意图还是给所有用户的消息,告诉他们注销> 我打算给你一个函数,你可以调用它来向特定用户发送消息。你在上面的代码中有这个。我不打算为你写你的服务器。我通过将响应对象存储在哈希表中并使用一些可帮助您识别用户的键,为您提供了实现目标的一般指导。然后很简单sendMessage(hash[userID], message) hash[userID] 将指向正确的响应对象,以便您可以发送消息。如果您需要整个解决方案,请考虑提供赏金或聘请某人来完成。

以上是关于如何在需要时(而不是定期)将服务器发送的事件通知发送到特定的浏览器会话?的主要内容,如果未能解决你的问题,请参考以下文章

在服务器事件上从服务器向客户端发送通知

使用Python发送邮件,在自己程序出错时发邮件通知自己

如何将TCP数据包发送到iPhone而不是ICMP数据包

仅当应用程序处于前台而不是通知中时,如何捕获辅助功能事件?

从服务器发送通知时未触发推送事件

当 Google Play 服务无法获得位置修复时,我如何获得通知?