【问题标题】:Setting time interval in HTML5 server sent events在 HTML5 服务器发送事件中设置时间间隔
【发布时间】:2013-05-01 08:40:30
【问题描述】:

我想从服务器向客户端发送定期更新。为此,我使用了服务器发送的事件。我正在粘贴以下代码:

客户端

获取服务器更新

<script>
if(typeof(EventSource)!="undefined")
{
   var source=new EventSource("demo_see.php");
   source.onmessage=function(event)
   {
      document.getElementById("result").innerHTML=event.data + "<br>";
   }
}
else
{
   document.getElementById("result").innerHTML="Sorry, your browser does not support    server-sent events...";
}
</script>
</body>
</html>

服务器端

<?php
    header('Content-Type: text/event-stream');
    header('Cache-Control: no-cache');
    $x=rand(0,1000);
    echo "data:{$x}\n\n";
    flush();
?>

代码运行良好,但它会在每个3 seconds 中发送更新。我想以毫秒为单位发送更新。我在flush() 之后尝试了sleep(1),但它只会将间隔进一步增加1 秒。有没有人知道我该如何做到这一点?

另外,我可以使用服务器发送的事件发送图像吗?

【问题讨论】:

    标签: javascript php html server-sent-events


    【解决方案1】:

    正如上面 cmets 中所讨论的,在无限循环中使用 sleepusleep 运行 PHP 脚本是不正确的,原因有两个

    • 当该脚本仍在运行时,浏览器将看不到任何事件数据(可能它会先等待连接关闭)。我记得 SSE 的早期浏览器实现允许这样做,但现在不再如此。
    • 即使它确实在浏览器端工作,您仍然会面临 PHP 脚本运行时间过长的问题(直到 PHP.ini time_out 设置启动)。如果这种情况发生一两次,那就没问题了。如果有 X 千个浏览器同时从您的服务器上寻找相同的 SSE,它将会关闭您的服务器。

    正确的做法是让您的 PHP 脚本响应事件流数据,然后像往常一样优雅地终止。如果您想控制浏览器何时重试,请提供 retry 值 - 以毫秒为单位。这是一些示例代码

    function yourEventData(&$retry)
    {
     //do your own stuff here and return your event data.
     //You might want to return a $retry value (milliseconds)
     //so the browser knows when to try again (not the default 3000 ms)
    }
    
    header('Content-Type: text/event-stream');
    header('Cache-Control: no-cache');
    header('Access-Control-Allow-Origin: *');//optional
    
    $data = yourEventData($retry);
    
    echo "data:{$str}\n\nretry:{$retry}\n\n";
    

    作为对原始问题的回答,这有点晚了,但出于完整性考虑:

    以这种方式轮询服务器时得到的只是数据。之后你用它做什么完全取决于你。如果您想将这些数据视为图像并更新网页中显示的图像,您只需这样做

    document.getElementById("imageID").src = "data:image/png;base64," + Your event stream data;
    

    原则就讲这么多。我有时会忘记retry 必须以毫秒为单位并最终返回,例如retry:5\n\n,令我惊讶的是,它仍然有效。但是,我会犹豫是否使用 SSE 以 100 毫秒的间隔更新浏览器端图像。更典型的用法如下所示

    • 用户请求服务器上的作业。该作业要么在其他作业之后排队,要么可能需要相当长的时间才能执行(例如,创建 PDF 或 Excel 电子表格并将其发回)
    • 与其让用户在没有反馈的情况下等待并冒着超时的风险,不如启动一个 SSE,它会告诉浏览器完成作业的 ETA,并设置一个 retry 值,以便浏览器知道何时查看再次获得结果。
    • ETA 用于向用户提供一些反馈
    • 在 ETA 结束时,浏览器会再次查看(浏览器会自动执行此操作,因此您无需执行任何操作)
    • 如果由于某种原因作业没有被服务器完成,它应该在事件流中指出它返回,例如data{"code":-1}\n\n 所以浏览器端代码可以优雅地处理这种情况。

    还有其他使用场景 - 更新股票报价、新闻标题等。以 100 毫秒的间隔更新图像感觉 - 纯粹是个人观点 - 就像对技术的滥用。


    自从我发布这个答案以来,现在已经快 5 年了,它仍然经常得到支持。为了任何仍在使用它作为参考的人的利益——在我看来,SSE 在很多方面都是一种相当过时的技术。随着对 WebSockets 的广泛支持的出现,为什么还要麻烦做 SSE。除了其他任何事情之外,为每个浏览器端重试设置和断开来自浏览器的 HTTPS 连接的成本非常高。 WSS 协议的效率要高得多。

    如果你想实现 websockets 的阅读点

    1. Client Side
    2. Server side via PHP with Ratchet
    3. With Nginx and NChan

    在我看来,PHP 并不是一种很好的处理 websocket 的语言,而且 Ratchet 也远非易于设置。 Nginx/Nchan 路线要容易得多。

    【讨论】:

    • 非常有用的答案,但我认为当 php 处于睡眠状态时,该脚本的 cpu 上应该几乎没有负载。如果浏览器每 100 毫秒出现一次新连接的负载与禁用 php 服务器超时的 php 睡眠之间,我没有任何比较
    • 所以这就是为什么我的 php 服务器在使用无限循环时返回 503 错误的原因,即使它因数据更新而中断。 - 不再支持。 :(
    • 这绝对是答案。不应该使用睡眠,否则你最终会达到 PHP 的最大执行时间(就像我做的那样)。提供带有重试值的单个响应为我解决了这个问题。谢谢!!
    【解决方案2】:

    解释了这种行为的原因(每 3 秒发送一次消息)here

    浏览器在每次连接关闭大约 3 秒后尝试重新连接到源

    因此,每 100 毫秒获取消息的一种方法是更改​​重新连接时间:(在 PHP 中)

    echo "retry: 100\n\n";
    

    虽然这不是很优雅,但更好的方法是 PHP 中的无限循环,每次迭代都会休眠 100 毫秒。有一个很好的例子here,只需将sleep() 更改为usleep() 以支持毫秒:

    while (1) {
        $x=rand(0,1000);
        echo "data:{$x}\n\n";
        flush();
        usleep(100000); //1000000 = 1 seconds
    }
    

    【讨论】:

    • 我对这个答案被两次投票感到有点惊讶。它建议使用usleep 在无限循环中运行 PHP 脚本。想象一下,如果 X 千个浏览器与您的服务器连接并要求服务器发送事件会发生什么? PHP 脚本的正确方法是安静地完成它的工作然后终止。如果浏览器根本无法识别事件的完成并且您的服务器 CPU 将流失......对于服务器发送的事件,浏览器总是会重新连接,频率由重试间隔决定。
    • @DroidOS 谢谢,我不知道。如果你能写一个更好的答案,那就太好了。 (对 PHP 不够熟悉,无法编辑我的答案)
    • 实际上,不需要新的答案。您需要做的就是让 PHP 脚本发回事件流并正常终止。自然,事件流可以包含retry:x\n\n 行,其中 x 以毫秒为单位。浏览器处理其余部分。令我惊讶的是,Mozilla 为 SSE 提出了这种有缺陷的 PHP 代码。如果您遵循该示例,您迟早会发现您的服务器已最大化。
    • @DroidOS 事情是,我真的不知道 PHP。我收集了这个,认为它很好。如果您可以建议对您谈论的更改进行编辑,请执行,并在摘要中说明您是按照我的要求进行的,这样它就不会被拒绝。谢谢! :)
    【解决方案3】:

    我认为接受的答案可能会误导。虽然它正确地回答了这个问题(如何设置 1 秒的间隔),但无限循环通常不是一种糟糕的方法。

    当实际上存在与 Ajax 轮询相反的更新时,SSE 用于从服务器获取更新,该轮询在某些时间间隔内不断检查更新(即使没有更新)。这可以通过无限循环来实现,该循环使服务器端脚本始终运行,不断检查更新并仅在有更改时才回显它们。

    不是真的

    当该脚本仍在运行时,浏览器将看不到任何事件数据。

    您可以在服务器上运行脚本,但仍将更新发送到浏览器,而不会像这样结束脚本执行:

    while (true) {
      echo "data: test\n\n";
      flush();
      ob_flush();
      sleep(1);
    }
    

    通过发送重试参数而不进行无限循环将结束脚本,然后再次启动脚本,结束它,重新开始......这类似于 Ajax-polling 检查更新,即使没有更新,这不是SSE 是如何运作的。当然,在某些情况下这种方法是合适的,就像它在接受的答案中列出的那样(例如等待服务器创建 PDF 并在完成时通知客户端)。

    使用无限循环技术将使脚本始终在服务器上运行,因此您应该小心处理大量用户,因为您将为每个用户都有一个脚本实例,这可能导致服务器过载。另一方面,即使在一些简单的场景中,您会突然在网站上获得大量用户(没有 SSE),或者如果您使用 Web Sockets 而不是 SSE,也会发生同样的问题。一切都有其局限性。

    另一件需要注意的事情是你放入循环中的内容。例如,我不建议将数据库查询放在每秒运行一次的循环中,因为这样您也会将数据库置于过载的风险中。对于这种情况,我建议使用某种缓存(Redis 甚至简单的文本文件)。

    【讨论】:

      【解决方案4】:

      SSE 是一项有趣的技术,但它会对使用 APACHE/PHP 后端的实现产生令人窒息的副作用。

      当我第一次发现 SSE 时,我非常兴奋,我用 SSE 实现替换了所有 Ajax 轮询代码。只做了几分钟,我就注意到我的 CPU 使用率上升到 99/100 并且担心我的服务器很快就会被关闭,迫使我将更改恢复到友好的旧版本阿贾克斯轮询。我喜欢 PHP,尽管我知道 SSE 在 Node.is 上会更好地工作,但我还没有准备好走这条路!

      经过一段时间的批判性思考,我想出了一个 SSE APACHE/PHP 实现,它可以工作而不会让我的服务器窒息而死。

      我将与您分享我的 SSE 服务器端代码,希望它可以帮助某人克服使用 PHP 实现 SSE 的挑战。

      <?php
      /* This script fetches the lastest posts in news feed */
      header("Content-Type: text/event-stream");
      header("Cache-Control: no-cache");
      
      // prevent direct access
      if ( ! defined("ABSPATH") ) die("");
      
      /* push current user in session data into global space so 
      we can release session lock */
      $GLOBALS["exported_user_id"] = user_id();
      $GLOBALS["exported_user_tid"] = user_tid();
      
      /* now release session lock having exported session data 
      in global space. if we don't do this, then no other scripts 
      will run thus causing the website to lag even when 
      opening in a new tab */
      session_commit();
      
      /* how long should this connection be maintained -
      while we want to wait on the server long enoug for
      update, holding the connection forever burn CPU 
      resources, depending on the server resources you have 
      available you can tweak this higher or lower. Typically, the 
      higher the closer your implementation stays as an SSE 
      otherwise it will be equivalent to Ajax polling. However, an 
      higher time burns CPU resource especially when there's 
      more users on your website */
      $time_to_stay = strtotime("1 minute 30 seconds");
      
      /* if no data is sent, we wait 2 seconds then abort 
      connection. You can use this to test when a data you 
      require for script operation is not passed along. Typically 
      SSE reconnects after 3 seconds */
      if ( ! isset( $_GET["id"] ) ){
      exit;
      }
      
      /* if "HTTP_LAST_EVENT_ID" is set, then this is a 
      continue of temporily terminated script operation. This is 
      important if your SSE is maintaining state you can use 
      the header to get last event ID sent */ 
      $last_postid = ( ( isset( 
      $_SERVER["HTTP_LAST_EVENT_ID"] ) ) ? intval( 
      $_SERVER["HTTP_LAST_EVENT_ID"] ) :
                                                           intval( $_GET["id"] ) );
      
      /* keep the connection active until there's data to send to 
      client */
      while (true) {
      /* You can assume this function perform some database
      operations to get latest posts */
      $data = fetch_newsfeed( $last_postid );
      
      /* if data is not empty, we want to push back to the client 
      then there must have been some new posts to push to 
      client */
      if ( ! empty( trim( $data ) ) ){
      /* With SSE its my common practice to Json encode all 
      data because I notice that not doing so, sometimes 
      cause SSE to lose the data packet and only deliver a 
      handful of the data on the client. This is bad since we are 
      returning a structured HTML data and loosing some part 
      of it will cause our HTML page to break when the data is 
      inserted in our page */
      $data = json_encode(array("result" => $data));
      
       echo "id: $last_postid \n"; // this is the lastEventID 
       echo "data: $data\n\n"; // our data
       /* flush to avoid waiting for script to terminate - make 
       sure its in the same order */
       @ob_flush(); flush(); 
      }
      
      // the amount of time that has been spent on this script
      $time_stayed = intval(floor($time_to_stay) - time());
      /* if we have stayed more than time to stay, then abort 
      this connection to free up CPU resource */
      if ( $time_stayed <= 0 ) { exit; }
      
      /* we simply wait 5 seconds and continue again from 
      start . We don't want to keep pounding our DB since we 
      are in a tight loop so we sleep a few seconds and start 
      from top*/
       sleep(5);
      }
      

      【讨论】:

        【解决方案5】:

        Nginx 驱动的 PHP 网站上的 SSE 似乎有一些更细微的差别。首先,我必须在 Nginx 配置的 Location 部分给出这个设置

         fastcgi_buffering off; 
        

        有人建议我将 fastcgi_read_timeout 更改为更长的时间,但它并没有真正帮助......或者我可能没有足够深入

          fastcgi_read_timeout 600s; 
        

        这两个设置都将在 Nginx 配置的位置部分中给出。

        许多人在 SSE 代码中推荐的标准无限循环往往会挂起 Nginx(或可能是 PHP7.4fpm),这很严重;因为它会关闭整个服务器。虽然人们建议在 PHP 中使用 set_time_out(0) 来更改默认超时(我相信是 30 秒),但我不太确定这是一个好策略

        如果您完全移除无限循环,SSE 系统似乎就像轮询一样工作:EventSource 的 Javascript 代码不断回调 SSE PHP 模块。这使得它比 Ajax 轮询更简单(因为我们不必为 Javascript 编写任何额外的代码来进行轮询),但是它仍然会继续重试,因此与 Ajax 轮询非常相似。而且每次重试都是一次完整的重新加载PHP SSE代码,所以比我最后做的要慢。

        这对我有用。这是一种混合解决方案,可以有一个循环,但不是无限循环。一旦该循环完成,SSE PHP 代码就会终止。这在浏览器中注册为失败(您可以在检查器控制台中看到),然后浏览器在服务器上再次调用 SSE 代码。这就像轮询,但间隔更长。

        在 SSE 的一次加载和下一次重新加载之间,SSE 继续循环工作,在此期间可以将额外的数据推送到浏览器中。所以你确实有足够的速度,不用担心整个服务器挂了。

        <?php
        $success = set_time_limit( 0 );
        ini_set('auto_detect_line_endings', 1);
        ini_set('max_execution_time', '0');
        
        ob_end_clean();
        
           
        header('Content-Type: text/event-stream');
        header('Cache-Control: no-cache');
        header('X-Accel-Buffering: no');
        
        //how fast do you want the browser to reload this SSE
        //after the while loop fails:
        echo "retry: 200\n\n"; 
        //If any dynamic data comes into your application 
        //in this 'retry' time period, and disappears, 
        //then SSE will NOT be able to push that data
        //If it is too short, there may be insufficient 
        //time to finish some work within the execution
        //of one loop of the SSE while loop below  
        
        $emptyCount = 0;
        $execCount = 0;
        $countLimit = 60; //Experiment with this, which works for you 
        $emptyLimit = 5;
        $prev = "";
        
        while($execCount < $countLimit){
        
           $execCount++;
           if( connection_status() != CONNECTION_NORMAL or connection_aborted() ) break;
        
        
           if(file_exists($file_path)) {
        
             //The file is to be deleted 
             //so that it does not return back again
             //There can be better method than one suggested here
             //But not getting into it, as this is only about SSE overall
             $s= file_get_contents("https://.....?f=$file_path");
              if($s == "")
                 { 
                  $emptyCount++;
                  $prev = "";
                 } 
               else {
                 if($s != $prev){
                    $prev = $s;
                    echo $s; //This is formatted as data:...\n\n 
                             //as needed by SSE
                 }
              }
        
              //If it is continually empty then break out of the loop. Why hang around?
              if($emptyCount >$emptyLimit) {
                  $emptyCount=0;  
                  $prev = "";         
                  break;
              } 
        
             } else $prev = ""; 
        
             @ob_flush();
             @flush();
             sleep(1);
           
            
          }
        

        【讨论】:

          猜你喜欢
          • 2020-09-02
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2019-06-12
          • 2014-10-03
          • 2011-11-20
          • 1970-01-01
          相关资源
          最近更新 更多