我想使用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所做的那样)。