如何使用php函数";file_get_contents";服务器发送事件来监视服务器上文件的修改


How to use php function "file_get_contents" and server sent event to monitor the modification of a file on server?

我想使用Server Sent Event对服务器上的文件进行实时监控,下面是javascript和php代码

$(function(){
    if(typeof(EventSource) !== "undefined") {
        var source = new EventSource("./monitor_server_side.php");
        var update = 0;
        source.onmessage = function(e){
            $('#monitor').html(e.data);
            update++;
            $('#update-time').html("updated " + update + " times");
        };
        source.onerror = function(e){
            console.log(e);
        }
    }
    else {
        $('#no-update-warning').text("no update");
    }
});

<?php
    session_start();
    header('Content-Type: text/event-stream');
    header('Cache-Control: no-cache');
    if(!isset($_SESSION['test']))
        $_SESSION['test'] = 0;
    $_SESSION['test']++;
    if(isset($_SESSION['file_path']))
    {
        $time      = date('r');
        $file_path = $_SESSION['file_path'];
        echo file_get_contents($_SESSION['file_path']);
    }
    flush();
?>

但是,如果我修改了$_SESSION['file_path']的文件内容,则网页上显示的内容不会更新。

如果我使用w3schools.com提供的以下php代码,那么一切都很顺利

<?php
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
$time = date('r');
echo "data: The server time is: {$time}'n'n";
flush();
?>

首先,SSE是一个非常简单的协议,但您必须使用它所需的7个字节。因此,在您的file_get_contents()呼叫前加上"data:",并在其后加上"'n'n"。和flush()一样,也做ob_flush(),除非你能100%确定PHP输出缓冲关闭

难题的另一部分是PHP脚本将永远运行。在文件监视脚本的情况下,您希望将其封装在while(1){...}循环中,并在该循环中放入某种睡眠。

您需要注意PHP会话已被锁定。因此(因为您的PHP脚本现在将永远运行),您希望从会话中读取您需要的内容,但随后通过调用session_write_close();来释放它。请确保在进入while(1){...}循环之前执行此操作。

作为最后的细化,您只想在数据发生更改时发送数据,因此存储前一个字符串,与之进行比较,如果相同,则跳过输出并直接进入sleep

这是你的代码与所有这些修复完成:

<?php
session_start();
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
if(!isset($_SESSION['test']))
    $_SESSION['test'] = 0;
$_SESSION['test']++;
if(!isset($_SESSION['file_path'])){
    echo "data:Bad session'n'n";
    exit;
    }
$file_path = $_SESSION['file_path']
session_write_close();
$prev = '';
while(1)
{
    $s = file_get_contents($file_path);
    if($s != $prev){
        echo "data:{$s}'n'n";
        @ob_flush();@flush();
    }
    sleep(5);   //Poll every 5 secs
}

更新的答案

您自己的代码不起作用的原因是,您需要用data:开始每一行,并用双线中断('n'n)结束每一次"传输"。

以下示例将每秒读取一个文件,并在每次文件更改时向浏览器发送更新。

<?php
//Open session
session_start();
//Set the filename if not already set
if (!isset($_SESSION['file_path']) || !is_file($_SESSION['file_path'])) {
    $_SESSION['file_path'] = __DIR__ . '/updates.txt';
}
//Store the filename in a local variable
$filename = $_SESSION['file_path'];
//Make sure to close the session
session_write_close();
//Send the required header
header('Content-Type: text/event-stream');
//Prevent caching of the response
header('Cache-Control: no-cache');
//Keep track of the previously read content
$previous_content = '';
//Counts number of file reads (only for testing)
$read_number = 0;
//Set reconnect interval
echo "retry: 5000'n'n";
// Enter an eternal loop
while(true) {
    /**
     * Try to increase the max_execution_time (note that your provider may
     * well prevent this!) Calling it inside each loop will reset the timer,
     * allowing it to run forever.
     */
    set_time_limit(30);
    //Increment read counter (only for testing)
    $read_number++;
    //Check that the file exists
    if(is_file($filename)) {
        //Get the files content
        $current_content = file_get_contents($filename);
        //Compare to previous content to only send updates when needed
        if ($current_content !== $previous_content) {
            //Reset output
            $output = '';
            //Append the read counter (only for testing)
            $output .= 'data: read number ' . $read_number . "'n";
            //Append the file content including "data: " prefix on each line
            $output .= 'data: ' . str_replace("'n", "'ndata: ", $current_content) . "'n'n";
            //Store the content, so we know not to resend the same
            $previous_content = $current_content;
            //Send the output
            echo $output;
            //Uncomment for debugging
            //file_put_contents('output.txt', "----" . $output . "----", FILE_APPEND);
            //Flush output buffer
            ob_flush();
            //Flush apache buffer
            flush();
        }
    }
    /**
     * Wait a bit before next iteration - you may want to reduce this to a
     * usleep(100000) or something to get updates faster - keep in mind that
     * file_get_contents is probably an expensive operation, so don't reduce
     * the delay too much or you will hog lots of resuources
     */
    sleep(1);
}
//We'll never get to this point

您的客户端代码按原样工作,不过我建议添加更多的错误处理,以避免在控制台日志中混淆消息——这就是我使用的:

<!doctype html>
<html>
    <head>
        <script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
        <script>
            $ = jQuery;
            $(function(){
                if(typeof(EventSource) !== "undefined") {
                    var source = new EventSource("./monitor_server_side.php");
                    var update = 0;
                    source.onmessage = function(e){
                        $('#monitor').html(e.data);
                        update++;
                        $('#update-time').html("updated " + update + " times");
                    };
                    source.onerror = function(e){
                        switch( e.target.readyState ){
                            case EventSource.CONNECTING:
                                console.log('Reconnecting...');
                                break;
                            case EventSource.CLOSED:
                                console.log('Connection failed. Will not retry.');
                                break;
                            default:
                                console.log(e);
                                break;
                        }
                    };
                }
                else {
                    $('#no-update-warning').text("No Server Sent Events support");
                }
            });
        </script>
    </head>
    <body>
        <div id="no-update-warning"></div>
        <pre id="monitor"></pre>
        <div id="update-time"></div>
    </body>
</html>

我将沿用我原来的答案,因为上面的代码已经克服了所有的障碍——除了一个:Internet Explorer 不支持服务器发送的事件

SSE现在似乎是一个非常有吸引力的替代网络套接字的方案,在无法使用网络套接字的情况下(这是最便宜的共享托管帐户的情况)-只要你为不支持它的浏览器提供后备。

提醒一句

在请求期间,每个客户端都保持与服务器的开放连接(在某些情况下,可能还会再保持30秒或使用任何set_time_limit())-如果这是在apache服务器上实现的,则对同时连接的数量有几个限制-这意味着,在一个预期会有多个客户端的环境中,这可能会"杀死"其余的网站甚至服务器。

每个客户端还需要大量的资源(如RAM),这可能会导致自身的问题。

这些问题在开发过程中很容易被忽视,因为几乎没有同步的客户端,但在许多情况下,一旦系统投入生产,可能会造成严重破坏!

换句话说:协议本身是有效的,但在apache中的实现是危险的。其他服务器(nginx、node.js等)很可能没有这个问题,但这超出了我的专业领域。编写一个独立的服务器肯定可以解决这些问题,但我想不出有什么场景可以做到这一点,而使用websocket则不行。

您还应该注意浏览器实现了最大并发连接限制——尽管问题并没有那么严重。如果每个客户端使用多个SSE连接(你真的不应该),或者如果用户打开多个选项卡,那么可能会达到浏览器限制,根据浏览器的不同,这可能会阻止SSE连接工作,或者阻止任何后续请求完成(即,新页面加载、样式表、图像等)。

我以前从未听说过SSE,但这很有效:

session_start();
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
if(!isset($_SESSION['test'])) {
    $_SESSION['test'] = 0;
}
$_SESSION['test']++;
if (!isset($_SESSION['file_path'])) {
    $_SESSION['file_path'] = __DIR__ . '/updates.txt';
}
echo 'data: ';
if(is_file($_SESSION['file_path']))
{
    $time      = date('r');
    $file_path = $_SESSION['file_path'];
    echo file_get_contents($_SESSION['file_path']);
}
echo "'n'n";
flush();

首先,我看不出您在哪里设置了会话变量file_path,所以如果还没有设置,我会设置它。

但是,您一无所获(即使设置了file_path并且文件存在)的原因是,您需要在字符串前面加上data:,并在其后面写下换行符('n'n

可能不相关的评论:

据我所知,这与发送单独的GET请求类似,只是它不太兼容,因为它在IE 中不受支持

我的测试(使用firefox)大约每5秒显示一个单独的GET请求,这意味着你无论如何都不会实时得到任何东西。

如果我是你,我会简单地使用ajax请求(即$.get([...]))。。。或者websockets,如果这是一个选项,并且您确实需要实时的数据更新。

另一条评论

进一步阅读,你可以获得实时更新-但你可能需要克服很多障碍才能做到这一点。首先,你需要保持php脚本的活力,这是最简单的形式,意味着要做这样的事情(这将使脚本持续20秒):

for($i=0;$i<20;$i++) {
    echo 'data: ';
    echo 'read number ' . $i . ' :: ';
    if(is_file($_SESSION['file_path'])) {
        $time      = date('r');
        $file_path = $_SESSION['file_path'];
        echo file_get_contents($_SESSION['file_path']);
    }
    echo "'n";
    flush();
    sleep(1);
}
echo "'n";

然而,由于内部输出缓冲,这在"标准"apache中不起作用(即,即使您将内容告知php给flush(),apache仍会在实际将其发送到浏览器之前对其进行缓存)。我过去不得不处理这个问题,虽然对此的推理和讨论远远超出了这个答案的范围,但我只能说这并不容易,而且它需要一些效率极低且丑陋的技巧(比如发送几个KB的无用数据)。如果不是使用apache,而是一个可以更好地控制实际输出的Web服务器,甚至是一个可以完全控制发送数据的自定义Web服务器,则可以使用服务器端事件在浏览器中获取实时更新(前提是浏览器支持)。

所以TL;DR:虽然听说这种(对我来说)实时更新的新方法很有趣,但经过一些研究,我确信我永远不会使用它,我仍然建议在早期放弃它,采用更主流的方法

为了监视更改,您可以利用当前监视文件的上次修改时间。

<?php
// outputs e.g.  somefile.txt was last modified: December 29 2002 22:16:23.
$filename = 'somefile.txt';
if (file_exists($filename)) {
    echo "$filename was last modified: " . date ("F d Y H:i:s.", filemtime($filename));
}
?>

参考

我认为问题在于会话本身。

当您执行session_start()时,PHP将所有变量加载到$_SESSION变量中,考虑到这是一个单独的连接(第一个连接之后没有请求)。会话超级全局加载一次。如果您使用其他文件更改它,那就无关紧要了,因为(系统上的)会话文件不会被再次读取,因此$_session superglobal不会更改。

您可以通过删除对会话的依赖来确认这一点(就像w3所做的那样)。

相关文章: