9

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

客户端

获取服务器更新

<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. 我想以毫秒为单位发送更新。我试过sleep(1)了,flush()但它只会将间隔进一步增加 1 秒。有谁知道我该如何做到这一点?

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

4

5 回答 5

16

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

  • 当该脚本仍在运行时,浏览器将看不到任何事件数据(可能它等待连接首先关闭)。我记得 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. 客户端
  2. 服务器端通过 PHP 和 Ratchet
  3. 使用 Nginx 和 Nchan

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

于 2015-08-28T04:33:46.303 回答
6

此处解释了此行为的原因(每 3 秒发送一次消息):

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

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

echo "retry: 100\n\n";

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

while (1) {
    $x=rand(0,1000);
    echo "data:{$x}\n\n";
    flush();
    usleep(100000); //1000000 = 1 seconds
}
于 2013-05-01T08:57:19.597 回答
4

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

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

这是不正确的:

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

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

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

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

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

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

于 2017-09-10T08:44:32.007 回答
0

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

 fastcgi_buffering off; 

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

  fastcgi_read_timeout 600s; 

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

许多人在 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);
   
    
  }
于 2022-01-07T13:16:06.110 回答
0

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

当我第一次发现 SSE 时,我非常兴奋,以至于我用 SSE 实现替换了所有 Ajax 轮询代码。这样做仅几分钟,我就注意到我的 CPU 使用率上升到99/100,并且担心我的服务器很快会被关闭,迫使我将更改恢复为友好的旧 Ajax 轮询。我喜欢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);
}
于 2019-08-23T16:12:43.850 回答