服务器发送事件的循环导致页面冻结

While loops for server-sent events are causing page to freeze

我目前正在使用服务器发送事件来接收消息的聊天。但是,我 运行 遇到了问题。服务器发送的事件永远不会连接并保持挂起状态,因为页面未加载。

例如:

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

我希望每3秒输出一次"data: This is the message."。相反,页面只是不加载。但是,对于服务器发送的事件,我需要这种行为。有办法解决这个问题吗?

编辑:

完整代码:

<?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 网站上也是如此。

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

基本上您想要一个简单的 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,你只会得到一个白色/空白页面,因为它无法验证整个 page/script。

我在我的项目中使用这种方法,无论我在 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/

毫无疑问,您现在已经明白了这一点,但偶尔您还没有,我在几个 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();
    }
?>

我要抓住机会陈述显而易见的事情,

您可以每 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
}

会不会是脚本超时这么简单?

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

所以您可能只需要一个简单的加法:

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

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

更新:我在评论中注意到您正在使用一些免费托管服务。如果他们 运行ning PHP 在 safe mode 那么你不能重置你的超时。

我在这里注意到的一件事是 sleep() 函数与 ob_start() 结合使用 - 没有 - ob_start() 任何地方在完整的代码示例中,还有 flush()ob_flush() ..

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

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

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

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

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

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

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

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

希望这能解决您的问题

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

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

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

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

Session Locks
First and foremost, if you're using sessions for whatever reason you will need to make them read-only on the stream. If they're writable, this will lock them everywhere else, so any page loads will hang while the server waits for them to become writable again. This is easily fixed by calling; session_write_close();