【问题标题】:While loops for server-sent events are causing page to freeze服务器发送事件的循环导致页面冻结
【发布时间】:2015-06-11 09:54:02
【问题描述】:

我目前正在处理使用服务器发送事件来接收消息的聊天。但是,我遇到了一个问题。服务器发送的事件永远不会连接并停留在等待状态,因为页面没有加载。

例如:

<?php
    while(true) {
        echo "data: This is the message.";
        sleep(3);
        ob_flush();
        flush();
    }
?>

我希望每 3 秒出现一次“数据:这是消息”。将被输出。相反,页面只是不加载。但是,对于服务器发送的事件,我需要这种行为。有没有办法解决这个问题?

编辑:

完整代码:

<?php
   session_start();

    require "connect.php";
    require "user.php";

    session_write_close();

    echo $data["number"];

    header("Content-Type: text/event-stream\n\n");
    header('Cache-Control: no-cache');

    set_time_limit(1200);

    $store = new StdClass(); // STORE LATEST MESSAGES TO COMPARE TO NEW ONES
    $ms = 200; // REFRESH TIMING (in ms)
    $go = true; // MESSAGE CHANGED

    function formateNumber ($n) {
            $areaCode = substr($n, 0, 3);
            $part1 = substr($n, 3, 3);
            $part2 = substr($n, 6, 4);
            return "($areaCode) $part1-$part2";
    }

    function shorten ($str, $mLen, $elp) {
        if (strlen($str) <= $mLen) { 
            return $str;
        } else {
            return rtrim(substr($str, 0, $mLen)) . $elp;
        }
    }

   do {
    $number = $data["number"];
        $sidebarQ = "
            SELECT * 
            FROM (
                SELECT * 
                FROM messages 
                WHERE deleted NOT LIKE '%$number%' 
                AND (
                    `from`='$number' 
                    OR 
                    `to`='$number'
                ) 
                ORDER BY `timestamp` DESC
            ) as mess 
            GROUP BY `id` 
            ORDER BY `timestamp` DESC";
        $query = $mysqli->query($sidebarQ);

        if ($query->num_rows == 0) {
            echo 'data: null' . $number;
            echo "\n\n";
        } else {

            $qr = array();
            while($row = $query->fetch_assoc()) {
                $qr[] = $row;
            }

            foreach ($qr as $c) {
                $id = $c["id"];
                if (!isset($store->{$id})) {
                    $store->{$id} = $c["messageId"];
                    $go = true;
                } else {
                    if ($store->{$id} != $c["messageId"]) {
                        $go = true;
                        $store->{$id} = $c["messageId"];
                    }
                }
            }

            if($go == true) {
                $el = $n = "";

                foreach ($qr as $rows) {
                    $to = $rows["to"];
                    $id = $rows["id"];
                    $choose = $to == $number ? $rows["from"] : $to;
                    $nameQuery = $mysqli->query("SELECT `savedname` FROM `contacts` WHERE `friend`='$choose' AND `number`='$number'");
                    $nameGet = $nameQuery->fetch_assoc();
                    $hasName = $nameQuery->num_rows == 0 ? formateNumber($choose) : $nameGet["savedname"];

                    $new = $mysqli->query("SELECT `id` FROM `messages` WHERE `to`='$number' AND `tostatus`='0' AND `id`='$id'")->num_rows;
                    if ($new > 0) {
                        $n = "<span class='new'>" . $new . "</span>";
                    }

                    $side = "<span style='color:#222'>" . ($to == $number ? "To you:" : "From you:") . "</span>";
                    $el .= "<div class='messageBox sBox" . ($nameQuery->num_rows == 0 ? " noname" : "") . "' onclick=\"GLOBAL.load($id, $choose)\" data-id='$id'><name>$hasName</name><div>$side " . shorten($rows["message"], 25, "...") . "</div>$n</div>";
                }
                echo 'data: '. $el;
                echo "\n\n";

                $go = false;
            }
        }

        echo " ";

        ob_flush();
        flush();
        sleep(2);
    } while(true);
?>

我还想指出,这个无限循环不应该导致这种情况发生。这就是 SSE 通常的设置方式,甚至在 MDN 网站上也是如此。

【问题讨论】:

  • 你到底为什么要为此使用无限循环,这就是页面永远不会加载的原因,怎么可能?
  • MDN上有一些简单的例子,请关注。
  • 如你所见,在 MDN 网站上......他们使用无限循环。
  • 在设置 Content-Type 标头之前取出echo $data["number"];。如果您在设置 Content-Type 标头之前打印一些内容,那么您将获得默认的内容类型,可能是 text/html。
  • 在这种情况下,它应该返回数据,因为它正在使用输出缓冲,但有时这对于每个服务器来说可能有点棘手。值得用更简单的页面测试你的输出缓冲代码,然后看看是否在那种情况下它仍然有效

标签: php server-sent-events


【解决方案1】:

毫无疑问,现在你已经明白了这一点,但如果你还没有在几个 sse 脚本中使用如下代码,那么它就像一个魅力。下面的代码是通用的,不包含您的 sql 或记录集处理,但这个想法是合理的(!?)

<?php
    set_time_limit( 0 );
    ini_set('auto_detect_line_endings', 1);
    ini_set('mysql.connect_timeout','7200');
    ini_set('max_execution_time', '0');

    date_default_timezone_set( 'Europe/London' );
    ob_end_clean();
    gc_enable();



    header('Content-Type: text/event-stream');
    header('Cache-Control: no-cache');
    header('Access-Control-Allow-Credentials: true');
    header('Access-Control-Allow-Methods: GET');
    header('Access-Control-Expose-Headers: X-Events');  




    if( !function_exists('sse_message') ){
        function sse_message( $evtname='chat', $data=null, $retry=1000 ){
            if( !is_null( $data ) ){
                echo "event:".$evtname."\r\n";
                echo "retry:".$retry."\r\n";
                echo "data:" . json_encode( $data, JSON_FORCE_OBJECT|JSON_HEX_QUOT|JSON_HEX_TAG|JSON_HEX_AMP|JSON_HEX_APOS );
                echo "\r\n\r\n";    
            }
        }
    }

    $sleep=1;
    $c=1;

   $pdo=new dbpdo();/* wrapper class for PDO that simplifies using PDO */

    while( true ){
        if( connection_status() != CONNECTION_NORMAL or connection_aborted() ) {
            break;
        }
        /* Infinite loop is running - perform actions you need */

        /* Query database */
        /*
            $sql='select * from `table`';
            $res=$pdo->query($sql);
        */

        /* Process recordset from db */
        /*
        $payload=array();
        foreach( $res as $rs ){
            $payload[]=array('message'=>$rs->message);  
        }
        */

        /* prepare sse message */
        sse_message( 'chat', array('field'=>'blah blah blah','id'=>'XYZ','payload'=>$payload ) );

        /* Send output */
        if( @ob_get_level() > 0 ) for( $i=0; $i < @ob_get_level(); $i++ ) @ob_flush();
        @flush();

        /* wait */
        sleep( $sleep );
        $c++;

        if( $c % 1000 == 0 ){/* I used this whilst streaming twitter data to try to reduce memory leaks */
            gc_collect_cycles();
            $c=1;   
        }
    }



    if( @ob_get_level() > 0 ) {
        for( $i=0; $i < @ob_get_level(); $i++ ) @ob_flush();
        @ob_end_clean();
    }
?>

【讨论】:

  • 您将使用 VPS 或专用服务器,其中 set_time_limit 和其他 ini_set 默认启用,在某些主机上这些功能将无法使用,因为它们已被系统管理员禁用,因为共享环境。
【解决方案2】:

虽然这不是问题的直接答案,但请尝试使用此方法查找错误。您没有收到错误,但这应该可以帮助您找到错误?

基本上你想要一个简单的 PHP 脚本,其中包含你的主脚本,但是这个页面会导致错误...下面的示例..

index.php / 简单错误包含器

<?php
    ini_set('display_errors',1);
    ini_set('display_startup_errors',1);
    error_reporting(-1);
    require "other.php";
?> 

other.php / 你主脚本

<?php
    ini_set('display_errors',1);
    ini_set('display_startup_errors',1);
    error_reporting(-1);
 weqwe qweqeq
 qweqweqweqwe

?> 

如果您创建这样的设置,如果您查看 index.php,您将看到以下错误Parse error: syntax error, unexpected 'qweqeq' (T_STRING) in /var/www/html/syntax_errors/other.php on line 5,因为它在主页上没有无效语法,并且允许对任何包含进行错误检查..

但是如果你在哪里查看 other.php,你只会得到一个白色/空白页面,因为它无法验证整个页面/脚本。

我在我的项目中使用这种方法,这样无论我在 other.php 或任何链接的 php 页面中做什么,我都会看到它们的错误报告。

评论前请看懂代码 说这禁用错误控制意味着你没有费心 RTM

填充缓冲区

我记得过去的另一个问题是在缓冲区输出到浏览器之前填充缓冲区。所以在你的循环之前尝试这样的事情。

echo str_repeat("\n",4096);  // Exceed the required browser threshold 
for($i=0;$i<70;$i++)     {
    echo "something as normal";
    flush();
    sleep(1);
}

http://www.sitepoint.com/php-streaming-output-buffering-explained/ 的示例

【讨论】:

  • 我不确定这就是 OP 想要的。而且,这是完全错误的。您暂时将错误设置为不显示,因此您看不到它们。那里有同样的错误。这与两个文件无关,也不是什么“技巧”。关闭所有错误是一个非常非常糟糕的主意。
  • 第一句声明它不是直接答案,这是在 PHP 脚本上显示错误的最常用方法。它不会禁用错误控制。error_reporting -1 表示显示所有错误。 . 参考php.net/manual/en/function.error-reporting.php ... 至于这个“技巧”其实很得心应手。对不起,你不同意......我会让大多数人决定这个;)
  • 而“填充 [浏览器!] 缓冲区”的建议实际上是救命稻草!这完全不明显,在示例中很少提及,并且 SO 充满了试图避免第一条消息延迟的巫术......所以,至少谢谢你!
【解决方案3】:

睡眠功能似乎干扰了输出。将睡眠功能置于 AFTERWARDS 之后确实有效:

<?php
while(true) {
    echo "data: This is the message.";
    ob_flush();
    flush();
    sleep(3);
    }

正如其他人建议的那样,我鼓励使用 AJAX 而不是无限循环,但这不是你的问题。

【讨论】:

  • 请注意脚本很可能在第 30 次迭代(在我的环境中为第 37 次)之后才会显示任何输出。如果你将睡眠时间减少到 1,它可能会在冲洗前进入睡眠。
  • 这听起来就像你没有考虑到 30 秒的最大脚本执行时间..
  • 30 秒只是默认值。即使不更改 php.ini,也可以使用 ini_set 或 .htaccess 文件覆盖该指令。对于这个问题,我认为暗示这种限制不是问题。
  • 当然是的,但是这是针对您的第一条评论的.. 虽然现在考虑一下.. 这只是缓冲区未满.. 使用输出缓冲时,...最好发送几个 100 或 1000 个空格来填充初始缓冲区。这是 OB 的常见做法。这也取决于浏览器
【解决方案4】:

我在这里注意到的一件事是 sleep() 函数与 ob_start() 和 - 没有 - ob_start() 在完整代码示例中的任何位置,但有 flush()ob_flush()..

你到底在冲什么? 为什么不简单地ob_end_flush() 呢?

问题是sleep()echo(),又比sleep(),又比echo(),等等,等等.. 当输出缓冲打开时没有效果。当输出缓冲未发挥作用时,睡眠功能按预期工作 - 介于两者之间。事实上,它可能*(而且会)产生出乎意料的结果,而这些结果不会是我们想要看到的结果。

【讨论】:

  • 我知道我也会在这里使用 flush() 来尝试将数据发送到浏览器,而无需填充 PHP 和 Apache 缓冲区。这些缓冲区与您提到的代码空间输出缓冲区不同。
  • 您的代码中的ob_start() .. 在哪里?我问'无论如何,你冲洗什么?正因为如此。你从哪里开始 output_buffering ?在使用其他缓冲区功能之前,您需要在某个地方启动它。我看到的所有“开始”都是session_start(),这与 output_buffering 无关。两个不同的世界。
  • ob_flush() 在这里什么都不做,因为没有要刷新的输出缓冲区。编码草率,但也可能解决可能在其他地方打开输出缓冲区的库(不在提供的示例中)。
  • 关于自己编码,set_time_limit() 在调用header() 函数后也是一个坏主意。另外,ob_flush()ob_end_flush() 不是一回事。 flush()ob_flush()ob_end_flush() 一样.. 就像$var = ob_get_clean();$var = ob_get_contents(); ob_end_clean(); 一样 从你的角度来看,我可以说你仍然没有习惯看什么最可信php 的源代码 *(php.net - manual) 对我们有用,简要说明。为 php arsenal 中的每个函数包括非常有价值的 cmets。
【解决方案5】:

尝试下面给出的代码,而不是使用循环,它可以根据您的要求正常工作(我自己测试)

echo "data: This is the message."; $url1="&lt;your-page-name&gt;.php"; header("Refresh: 5; URL=$url1");

它会每 5 秒调用一次(在您的情况下将其设置为 3 而不是 5)并回显输出。

希望这能解决您的问题

【讨论】:

    【解决方案6】:

    以下代码在这里运行良好,还使用 ​​Mayhem 的 str_repeat 函数添加 4k 数据(这通常是 php 刷新 tcp 数据包的最小值)

    echo str_repeat(' ', 4096);
    while(true)
    {
        echo "data: This is the message.";
        flush();
        sleep(3);
    }
    

    【讨论】:

    • 通常你不需要循环中的 4096 个字符,最初只需要一次。之后你可以根据需要用小内容/输出刷新。我会删除 str_repeat 函数到上面而/循环..
    • 是的,这里有一个常见的混淆:有(至少...)两个缓冲区:PHP 的 OB 和浏览器自己的输入缓冲区!驯服PHP端相对容易。 initial 4(甚至 8,例如这里的 Firefox,显然)KB 用于 browser,以使其实际呈现“未完成”页面。
    【解决方案7】:

    我要抓住机会,说出显而易见的事实,

    您可以每 3 秒查询一次服务器,让客户端等待...

    这可以用javascript轻松完成

    例如,如果file.php,试试这个代码和名称

    <?php
    $action='';
    if (array_key_exists('action',$_GET))
    {$action=$_GET['action'];}
    if ($action=='poll')
    {
     echo "this message will be sent every 3 sec";
    }
    else
    {
    ?><HTML><HEAD>
    <SCRIPT SRC="http://code.jquery.com/jquery-2.1.3.min.js"></SCRIPT>
    <SCRIPT>
    function doPoll()
    {
        $('#response').append($.get("file.php?action=poll"));
        setTimeout(doPoll, 3000);
    }
    doPoll();
    </SCRIPT>
    </HEAD><BODY><DIV id="response"></DIV></BODY></HTML><?php
    }
    

    【讨论】:

    • 这是一个完全错误的答案。您在上面所做的是长轮询,这违背了服务器发送事件的目的 - 并且与 OP 试图实现的目标(websockets、服务器发送事件、永远帧然后这个长轮询)相差至少 3 到 4 个数量级技术)在服务器和客户端的压力方面请看这个教程howopensource.com/2014/12/introduction-to-server-sent-events
    【解决方案8】:

    会不会像脚本超时一样简单?

    最终,如果 PHP 脚本运行时间过长,它们会自行终止。不希望发生这种情况的解决方案是不断重置超时。

    所以你可能只需要一个简单的添加:

    <?php
        while(true) {
            echo "data: This is the message.";
            set_time_limit(30);
            sleep(3);
            ob_flush();
            flush();
        }
    ?>
    

    当然,这可能不是问题,但我的直觉是这就是问题所在。

    更新:我在 cmets 中注意到您正在使用一些免费托管。如果他们在safe mode 中运行 PHP,则您无法重置超时。

    【讨论】:

    • 纯 PHP 无法做到这一点。更新需要重新加载页面或 AJAX。
    • @EvanCarslake 这可以用纯 PHP 来完成,这是关于输出缓冲的最酷部分。如果你有一个 iframe 加载一个带输出缓冲的 PHP,你可以用 PHP 代码触发更新...虽然它并不总是一个干净的解决方案,但它确实有效并且是一种众所周知的方法。
    【解决方案9】:

    我建议使用 if() 语句而不是使用 while。在您的情况下,您的条件始终为真,因此它处于无限循环中。

    【讨论】:

    • 他的脚本的重点是......要拥有循环,用 IF 替换 DO/WHILE 将只执行一次然后结束。循环/while true 意味着它将始终运行而不需要一个虚假的条件。它是始终返回真实条件检查的最简单方法。
    【解决方案10】:

    我遇到了同样的问题,终于在kevin choppin's blog上找到了简单快捷的解决方案:

    会话锁定
    首先,如果您出于某种原因使用会话,则需要将它们设置为在流上只读。如果它们是可写的,这将在其他任何地方锁定它们,因此任何页面加载都会在服务器等待它们再次变为可写时挂起。这很容易通过调用来解决; session_write_close();

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-02-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-06-08
      • 2018-09-17
      相关资源
      最近更新 更多