SSE非常缓慢

7

我目前正在编写一个网络游戏的通信框架,通信图如下所示: 代码如下:

test.php:

<!DOCTYPE html>
<html>
    <head>
        <title> Test </title>
        <script>
            function init()
            {
                var source = new EventSource("massrelay.php");
                source.onmessage = function(event)
                {
                    console.log("massrelay sent: " + event.data);
                    var p = document.createElement("p");
                    var t = document.createTextNode(event.data);
                    p.appendChild(t);
                    document.getElementById("rec").appendChild(p);
                };
            }

            function test()
            {
                var xhr = new XMLHttpRequest();
                xhr.onreadystatechange = function () 
                {
                    if(xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) 
                    {
                        console.log("reciver responded: " + xhr.responseText);
                    }
                }
                xhr.open("GET", "reciver.php?d=" + document.getElementById("inp").value , true);
                xhr.send();
                console.log("you sent: " + document.getElementById("inp").value);
            }
        </script>
    </head>
    <body>
        <button onclick="init()">Start Test</button> 
        <textarea id="inp"></textarea>
        <button onclick="test()">click me</button>
        <div id="rec"></div>
    </body>
</html>

此功能接收用户输入(目前仅为测试的文本框),并将其发送至接收器,之后将接收器的响应写回到控制台中。我从未遇到过来自接收器的错误。此外,它还会为所发送的SSE添加事件侦听器。

reciver.php:

<?php 
    $data = $_REQUEST["d"];
    (file_put_contents("data.txt", $data)) ? echo $data : echo "error writing";
?>

正如您所看到的,这很简单,只是将数据写入data.txt,然后发送回写入成功的消息。data.txt只是数据传递到massrelay.php的“管道”。

massrelay.php:

<?php
    header('Content-Type: text/event-stream');
    header('Cache-Control: no-cache');
    while(1)
    {
        $data = file_get_contents("data.txt");
        if ($data != "NULL")
        {
            echo "data: " . $data . "\n\n";
            flush();
            file_put_contents("data.txt", "NULL");
        }
    }
?>

massrelay.php用于检查data.txt中是否有数据,如果有,则通过SSE将其传递给任何监听它的事件侦听器的人,一旦读取数据,它将清除数据文件。
整个过程实际上是完美的,除了一个小问题:massrelay.php发送数据可能需要从30秒到10分钟。对于Web游戏来说,这完全无法接受,因为你需要实时操作。我想知道这是否是由于我的代码存在缺陷还是硬件问题(在2006年的Dell上托管,具有Sempron处理器)。如果有人看到任何问题,请告诉我,谢谢。

@Polyov 听起来是个好主意,我怎么做呢? - James Oswald
这个 Stack Overflow 的问题展示了如何使用文件日志记录。 - Polyov
如果在massrelay.php中放入usleep(1000)会发生什么?我只能猜测while(1){file_get_contents();}会对您的机器造成压力。 - Peter
还有,如果您创建一个测试massreceiver,例如 while(1) { echo "test"; flush(); sleep(3); },会发生什么? - Peter
最后一件事,你的输出可能会被缓存,请参见php.ini中的 output_buffering。我经常很难强制执行 flush,可以参考 https://dev59.com/Qm445IYBdhLWcg3wws8u。 - Peter
显示剩余6条评论
5个回答

6

我在您的代码中看到了三个问题:

  • 没有 sleep
  • 没有 ob_flush
  • Sessions

您的 while() 循环一直在读取文件系统,需要减慢它的速度。我在下面添加了一段半秒钟的睡眠时间;可以通过试验来确定可接受延迟的最大值。

PHP 有自己的输出缓冲区。您可以使用 @ob_flush() 来刷新它们(@ 抑制错误),用 flush() 来刷新 Apache 的缓冲区。两者都是必要的,而且顺序也很重要。

最后,PHP sessions 会被锁定,因此如果您的客户端可能发送 session cookies,即使您的 SSE 脚本不使用 session 数据,也必须在进入无限循环之前关闭 session。

我已将这三个更改添加到了您的代码中,如下所示。

<?php
    header('Content-Type: text/event-stream');
    header('Cache-Control: no-cache');
    session_write_close();
    while(1)
    {
        $data = file_get_contents("data.txt");
        if ($data != "NULL")
        {
            echo "data: " . $data . "\n\n";
            @ob_flush();flush();
            file_put_contents("data.txt", "NULL");
        }
        usleep(500000);
    }

顺便说一句,其他答案中使用内存数据库的建议很好,但文件系统开销仅为毫秒级别,因此无法解释“30秒到10分钟”的延迟。


不要忘记使用 file_get_contentsfile_put_contents 进行“魔法”文件锁定。它们非常不可靠。 - Peter
@Peter,你能提供一个关于这个“魔法”的好链接吗?你是不是指上面的代码很危险,因为它没有进行任何显式锁定,所以在调用file_get_contents()file_put_contents()之间存在竞争条件? - Darren Cook
可以允许我推销一下我的书《使用HTML5 SSE进行数据推送应用》吗?虽然这本书已经出版两年了,但是SSE中的内容没有改变,甚至浏览器也没有太大的变化。书中的示例主要使用PHP编写,像@ob_flush();flush();这样的习惯用法在第一章中有详细介绍。 - Darren Cook
@DarrenCook 确实,我插入了您修订过的massrelay.php版本,它完美地运行了。通信时间几乎是即时的。我不太确定它是如何工作的,但结果几乎让我想读一下您的书(不幸的是它要3美元而不是免费的)。尽管如此,我仍可能会购买一本。 - James Oswald
@JamesOswald 这本书的成本相对于你因不了解你正在做什么而浪费的时间或我花费在写作上的大量时间来说是很小的。让你的公司购买它吧。如果这是一个业余项目,考虑尝试让当地图书馆收藏它。但请不要发布非法下载链接 :-) - Darren Cook
如果一个通道过于繁忙,请尝试使用两个通道。一个通道只负责推送一个功能到客户端,这将提高性能。 - Subhanshuja

5

我不知道将数据写入一个平面文件是否是最好的方法。文件 I/O 将成为你的主要瓶颈 (读取和写入意味着你很快就会达到最大值)。但假设你想继续使用这种方法...

你的应用可以受益于 PHP 会话,以存储一些数据,这样你就不必等待 I/O 操作。这就是中间件软件如 MemcachedRedis 可以帮助你的地方。你需要将 reciver.php 中的数据存储在文本文件中,并将其写入内存缓存 (或将其放入会话中,然后写入内存存储器)。这样就可以快速检索数据,减少文件 I/O。

我强烈建议你使用数据库来存储数据。特别是 MySQL,它会将常访问的数据加载到内存中,以加快读取操作。


现在这应该是注释了,不是答案。 - Peter
2
@Peter 这个问题有点过于宽泛,简单的评论无法解决。而且如果没有构建完整的工作模型,就无法构建代码示例。 - Machavity
1
他想使用平面文件和SSE,所以让我们帮助他,而不是要求他使用memcached、reddit和数据库。我认为他的问题在于输出缓冲而不是平面文件。 - Peter
我实际上打算尝试做一个数据库,但问题在于游戏过程中可能会有大量的数据涌入,每个人都想同时移动。文本文件的想法是在被massrelay读取之前可以写入许多命令,massrelay将传递许多命令给用户进行显示处理。理论上,您可以使用1x1的MYSQL表来完成此操作,但我认为使用文件I/O会更快。 - James Oswald
@Peter 我自己也试过使用平面文件,虽然在小规模上它可以工作,但如果你尝试扩展它,它会死得很惨。因此,使用内存缓存或数据库更有意义。 - Machavity
显示剩余2条评论

1
编辑:我删除了这个答案,因为OP说我的建议的测试1(见下文)运行良好,所以我的输出缓冲理论是错误的。但另一方面,他说使用本机函数freadfwritefcloseflock的相同代码不起作用,所以如果缓冲和文件I/O不是解决方案,我不知道是什么。我删除了帖子,因为我认为这不是一个有效的答案。让我总结一下:

  • 启用错误显示E_ALL
  • flush运行良好
  • OP说他正确使用了本机文件函数fopenfreadfwriteflock,但没有帮助。

如果flush正在工作,文件系统正在工作,我除了信任OP是正确的并放弃之外,什么也做不了。

所以现在我的工作完成了,如果我无法在OP的系统、配置和代码上尝试它,我就无法提供帮助。

我取消了对我的答案的删除,这样OP就可以获得文档链接,其他人也可以看到我的尝试解决方案。

我删除的旧帖子


1. 在测试massrelay.php上工作。
while(true) {
    echo "test!";
    sleep(1);
} 

所以你可以确定问题不是与文件相关。

2. 确保你已启用error_reportingdisplay_errors

我猜测你在30秒后才收到响应,是因为PHP脚本在时间限制后被终止。如果你启用了错误报告,你将会看到错误消息告诉你这一点。

3. 确保你实际上刷新了输出,而且它没有被缓冲。

可能需要30秒到10分钟的时间。

你能在30秒后看到数据是有道理的,因为30秒是PHP的最大执行时间的默认值。

看起来flush()在你的场景中不起作用,你应该检查php.ini文件中的output_buffering设置。

请参阅此处:php flush not working

文档:


while(true) { echo "测试!"; sleep(1); } 我实际尝试过这个代码,它在5-10毫秒内运行得非常完美。 - James Oswald
实际上我尝试了 fopen(); fread(); fclose();,现在我会尝试 flock(); - James Oswald

1
多年前,我曾尝试过使用平面文件和数据库存储数据,以便在多个并发用户之间进行通信(这是为了Flash游戏,但同样的原则也适用)。
平面文件的性能最差,因为您最终会遇到读/写访问问题。
对于数据库,如果您每秒钟访问数据库数千次且没有负载均衡,则最终也会崩溃。
我的答案并没有解决您当前的问题,而是引导您朝一个不同的方向思考。您真的需要考虑使用套接字服务器。也许可以尝试一些类似https://github.com/reactphp/socket的东西。
使用套接字服务器可能会遇到一些问题,因为共享主机不允许您运行shell脚本。我的解决方案是使用家用电脑进行套接字通信,并将我的域用作托管游戏的公共入口点。显然,我们并不都有静态IP地址来指向我们的游戏,所以我不得不使用DynDNS,当时它是免费的:http://dyn.com(现在可能有其他新的免费服务)。使用家庭服务器,您还需要设置路由器进行端口转发,将任何特定端口请求发送到您的IP/路由器以发送到您的LAN服务器。确保您在路由器和服务器上运行防火墙,以保护其他可能暴露的端口。
我知道这可能看起来很复杂,但请相信这是最优解。如果您需要任何帮助,请私信我,我可以尝试引导您解决可能遇到的任何问题。

啊,谢谢,我会研究一下,如果我遇到任何问题,我会私信你。 - James Oswald

0
在多个调试SSE的个别实例之一中,我发现讽刺的是,if (ob_get_level() > 0) {ob_end_clean();}引起了问题。那就是你需要防止PHP错误生成的代码,如果没有任何级别。恢复到ob_end_clean();解决了这个问题。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接