从服务器向用户浏览器推送数据

21

我希望从服务器向浏览器推送数据。我已经了解了php函数ob_flush(),它可以发送输出缓冲区。我需要一点逻辑帮助。我正在使用Facebook实时API,所以我想在每次Facebook访问我的网站时向用户推送数据。

这是我试图向浏览器推送数据的代码,但它没有起作用。

<?php
header('Access-Control-Allow-Origin: *');
header('Content-Type: text/event-stream');
ini_set("log_errors", 1);
ini_set("error_log", "php-error.log");
error_log( "LOGS STARTS FROM HERE" );
if(isset($_GET['hub_challenge'])){
    echo $_GET['hub_challenge'];    
}
if($_SERVER['REQUEST_METHOD'] == "POST"){
    $updates = json_decode(file_get_contents("php://input"), true); 
    // Replace with your own code here to handle the update 
    // Note the request must complete within 15 seconds.
    // Otherwise Facebook server will consider it a timeout and 
    // resend the push notification again.
    print_r($updates);
    ob_flush();
    flush();
    //file_put_contents('fb.log', print_r($updates,true), FILE_APPEND);     
    //error_log('updates = ' . print_r($updates, true));              
}
?>

我想使用官方的PHP套接字库http://php.net/manual/en/book.sockets.php,但如何在浏览器中实现呢? - Skyyy
我认为你要找的是关于PHP中WebSockets的教程。为什么不先搜索一下,然后根据你的发现来提问呢? - som
1
http://www.sanwebe.com/2013/05/chat-using-websocket-php-socket - som
https://dev59.com/5WUq5IYBdhLWcg3waPzr - som
让我们在聊天中继续这个讨论。点击此处进入聊天室 - Skyyy
显示剩余4条评论
9个回答

8
如@som所建议的,您可以简单地使用请求和它们之间的时间间隔,您不需要使用套接字。
但问题是,您正在尝试一次性从API接收数据并立即将其传递给浏览器。最好将这两个步骤分开。
在接收来自Facebook的数据的脚本中,将该数据存储在数据库或其他地方:
if($_SERVER['REQUEST_METHOD'] == "POST"){
    $updates = json_decode(file_get_contents("php://input"), true); 

    insertDataToDatabase($updates); // you'll have to implement this.
}

然后设置一个监控页面: monitor.php
<script>
lastId = 0;

$(document).ready(function() {
    getNewDataAtInterval();
});

function getNewDataAtInterval() {
    $.ajax({
        dataType: "json",
        url: "getData.php",
        data: {lastId: lastId}
    }).done(function(data) {
        for(i=0; i<data.messages.length; i++) {
            $("#messages").append("<p>" + data.messages[i]['id'] + ": " + data.messages[i]['message'] + "</p>");
            if (data.messages[i]['id'] > lastId) lastId = data.messages[i]['id'];
        }

        setTimeout(getNewDataAtInterval, 10000);
    }).fail(function( jqXHR, textStatus ) {
        alert( "Request failed: " + jqXHR.responseText );
    });
}
</script>

<div id="messages"></div>

最后,创建一个服务器端脚本,从数据库中加载新消息并返回JSON格式数据。 getData.php
$lastId = $_GET['lastId'];
$newMessages = getUpdatesFromDatabase($lastId);

exit(json_encode(array("messages"=>$newMessages)));

function getUpdatesFromDatabase($lastId) {
    // I'm using this array just as an example, so you can see it working.
    $myData = array(
        array("id"=>1,"message"=>"Hi"),
        array("id"=>2,"message"=>"Hello"),
        array("id"=>3,"message"=>"How are you?"),
        array("id"=>4,"message"=>"I'm fine, thanks")
    );

    $newMessages = array();
    foreach($myData as $item) {
        if ($item["id"] > $lastId) {
            $newMessages[] = $item;
            $newLastId = $item["id"];
        }
    }

    return $newMessages;
}

我不想存储任何数据。 - Skyyy
你可以使用任何类型的存储吗?比如临时文件?在这种情况下,“insertDataToDatabase”将会写入到服务器上的文本文件中。每当有人访问“monitor.php”时,“getUpdatesFromDatabase”函数将会读取该文件,将内容发送回客户端并删除该文件。 - Marcos Dimitrio
我可以访问存储数据库和临时文件。但是我不想使用它,因为有时Facebook数据会延迟一分钟,存储和提取数据会增加更多的处理时间。我想立即将数据推送给用户。 - Skyyy
数据量大约为10-20个用户(可能会有变化),每个用户在单个请求中的数据大小为460字节。然后我将把这些数据推送给一个单独的用户(也就是我),以进一步处理数据并在大屏幕上显示。 - Skyyy
这是当今任何优秀的DBMS服务器都可以轻松处理的情况,只要您拥有良好的硬件并正确设计索引。 您可以增加监视器刷新速率,比如每5秒,3秒甚至1秒,只需微调即可满意。 - Marcos Dimitrio
显示剩余3条评论

3
使用Comet或Prototype将成为这里的最佳实践。Ajax会增加服务器负载,它会频繁地轮询服务器。以下是使用Comet的一些必要信息。
这里是一个关于如何使用php实现Comet以发送实时数据的例子:链接

3

传统的HTTP协议中,Web浏览器总是向服务器请求响应。一旦响应被发送,服务器就会关闭连接。

真正的持久连接技术,例如WebSocket,是其中一种例外情况,浏览器会向服务器建立特定类型的连接,并且在了解意图的情况下,服务器将保持连接打开。这样,只要有数据需要“推送”到浏览器,连接就始终准备就绪。使用这种方法,无需暂时保存数据,因为您只需“传递它”即可。

轮询技术(包括长轮询,其中服务器不断向客户端发送“心跳”,就好像一个无意义的响应正在缓慢地传输)可以用作解决方法,但总会有一段时间间隔,在此期间连接不再打开,直到下一个循环发生。当连接不存在时,您唯一的选择是暂时保存数据,以便在浏览器回来时可以推送未处理的数据。如果您考虑将其存储在临时变量中,请注意,在PHP脚本中,一旦执行完成,该范围内分配的所有数据都将被垃圾收集。


2

我在寻找最佳解决方案时,发现了Pusher和Redis,用于从服务器向浏览器推送数据。

Pusher

您可以使用Pusher的JavaScript SDK方便地消费使用Pusher驱动程序广播的事件。

this.pusher = new Pusher('pusher-key');

this.pusherChannel = this.pusher.subscribe('reference_id');

this.pusherChannel.bind('SomeEvent', function(message) {
    console.log(message.user);
});

Redis

如果您正在使用Redis广播器,您需要编写自己的Redis pub/sub消费者来接收消息,并使用您选择的websocket技术进行广播。例如,您可以选择使用流行的Node.js库Socket.io。

使用socket.io和ioredis Node库,您可以快速编写事件广播器,以发布应用程序广播的所有事件:

var app = require('http').createServer(handler);
var io = require('socket.io')(app);

var Redis = require('ioredis');
var redis = new Redis();

app.listen(6001, function() {
    console.log('Server is running!');
});

function handler(req, res) {
    res.writeHead(200);
    res.end('');
}

io.on('connection', function(socket) {
    //
});

redis.psubscribe('*', function(err, count) {
    //
});

redis.on('pmessage', function(method, channel, message) {
    message = JSON.parse(message);
    io.emit(channel + ':' + message.event, message.data);
});

1
为什么需要推动呢?也许我漏掉了一些线索,但是这是解决问题的唯一方法吗?你想要设置一个名为statusUpdate的div元素的文本,以显示从Facebook发布新状态?那么你可以:

将该过程分解为一个作为守护进程运行的状态收集线程,该线程不断尝试从FB API获取(我不知道任何FB API的规格或有关知识,但我只能想象是否存在查找新状态的调用)。

API是否流式传输或者我们需要每隔X秒连接一次都无所谓,因为我们可以考虑到这一点。我会在php中设置一个守护进程,然后使用SSH命令nohup php daemon.php来启动一个永不结束的循环脚本,如下所示:

Define('SLEEP', 1);  // loop every second

while (true) {  

   // do your thing, dont exit

   if( $fbMonkey->checkNewStatus() ){
        $fbMonkey->toDatabase(new_statuses);
  }

   if( some_reason_to_exit() == TRUE ){
      exit;
   }

    sleep(SLEEP);  
}
// While ends with break

然后,也许可以将一个JavaScript函数包含到目标用户的HTML中(即进程的浏览器端),该函数从守护程序填充的状态表中读取,并返回那些尚未被标记为已查看(由用户或类似物)的未读状态到浏览器。如果我们在浏览器中为此创建一个无限循环,并让它更新div statusUpdate和新内容(html或tekst都可以?)。我会让这样的调用挂起并每20秒左右检查一次并更新div。

http://www.w3schools.com/ajax/tryit.asp?filename=tryajax_xml2

function loadXMLDoc(url)
{
var xmlhttp;
var txt,xx,x,i;
if (window.XMLHttpRequest)
  {// code for IE7+, Firefox, Chrome, Opera, Safari
  xmlhttp=new XMLHttpRequest();
  }
else
  {// code for IE6, IE5
  xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
  }
xmlhttp.onreadystatechange=function()
  {
  if (xmlhttp.readyState==4 && xmlhttp.status==200)
    {
    txt="<table border='1'><tr><th>Title</th><th>Artist</th></tr>";
    x=xmlhttp.responseXML.documentElement.getElementsByTagName("CD");
    for (i=0;i<x.length;i++)
      {
      txt=txt + "<tr>";
      xx=x[i].getElementsByTagName("TITLE");
        {
        try
          {
          txt=txt + "<td>" + xx[0].firstChild.nodeValue + "</td>";
          }
        catch (er)
          {
          txt=txt + "<td>&nbsp;</td>";
          }
        }
    xx=x[i].getElementsByTagName("ARTIST");
      {
        try
          {
          txt=txt + "<td>" + xx[0].firstChild.nodeValue + "</td>";
          }
        catch (er)
          {
          txt=txt + "<td>&nbsp;</td>";
          }
        }
      txt=txt + "</tr>";
      }
    txt=txt + "</table>";
    document.getElementById('txtCDInfo').innerHTML=txt;
    }
  }
xmlhttp.open("GET",url,true);
xmlhttp.send();
}

“或者我完全错了?”(保留HTML标签)

0
如果您只需要一个简单的解决方案,并且不担心旧浏览器的兼容性,并且您的流量较低,服务器发送事件可能是个好选择。
如果您的推送消息生成脚本在同一台服务器上,则可以使用一行代码来实例化它。
var evtSource = new EventSource("messages.php");

接下来是处理传入消息的函数。

    evtSource.onmessage = function(e) {

       console.log(e.data);
    }

messages.php 需要设置头部

header("Content-Type: text/event-stream\n\n");

然后进行一个无限循环,根据需要设置时间间隔。

例如:

header("Content-Type: text/event-stream\n\n");
date_default_timezone_set("Pacific/Auckland"); // as required
$sleepTime = 8; // # of seconds delayed between notifications

while (1) 
{   

   // Send a message at $sleepTime second intervals.
   echo 'data: The time is ' . date("H:i:s") . "\n\n";

   ob_end_flush();
   flush();
   sleep($sleepTime);
}

显然,您需要从某个地方读取消息并按要求呈现,但至少此示例为您提供了创建事件流的想法,而无需涉及WebSocket的相对复杂性。

免责声明:我对PHP不是非常有经验,但这个解决方案似乎对我有效。如果有任何问题,请告诉我。


0

不错的讨论。我喜欢:D @andy:太棒了,对于我来说,第一次有人能够准确地解释区别,我也理解了:D @Marcos Dimitrio 我同意

我自己运行了一个相当可怕的线程池,用于 Twitter API 守护进程,除了来自 Facebook 的 $_POST 推送之外,如果我理解正确的话,它们完全可以做到这一点。通过 Firehose 数据流 API 监控实时推文,以获取数百 / 数千个关键字群集的数组。在我看来,这是正确的方法,否则将遭受可怕的失败:D 当然,这是两个守护进程 getTweets 和 parseTweets 的一半。

<?php
ob_start();
require_once('config/phirehose-config.php');
require_once('lib.php');
$oDB = new db;

// run as a daemon aka background process
while (true) {

  // Process all statuses
  $query = 'SELECT cache_id, raw_tweet ' .
    'FROM json_cache';
  $result = $oDB->select($query);
  while($row = mysqli_fetch_assoc($result)) {

    $cache_id = $row['cache_id'];
//    $status = unserialize(base64_decode($row['raw_tweet']));
    $tweet_object = json_decode($row['raw_tweet'],false);


    // JSON payload for statuses stored in the database  
    // serialized base64 raw data

      // Delete cached copy of tweet
      //    $oDB->select("DELETE FROM json_cache WHERE cache_id = $cache_id");

        // Limit tweets to a single language,
        // such as 'en' for English
        //if ($tweet_object->lang <> 'nl') {continue;}

    // Test status update before inserting
    $tweet_id = $tweet_object->id_str;

    if ($oDB->in_table('tweets','tweet_id=' . $tweet_id )) {continue;}

    $tweet_text = $oDB->escape($tweet_object->text);    
    $created_at = $oDB->date($tweet_object->created_at);
    if (isset($tweet_object->geo)) {
      $geo_lat = $tweet_object->geo->coordinates[0];
      $geo_long = $tweet_object->geo->coordinates[1];
    } else {
      $geo_lat = $geo_long = 0;
    } 
    $user_object = $tweet_object->user;
    $user_id = $user_object->id_str;
    $screen_name = $oDB->escape($user_object->screen_name);
    $name = $oDB->escape($user_object->name);
    $profile_image_url = $user_object->profile_image_url;


    // Add a new user row or update an existing one
    $field_values = 'screen_name = "' . $screen_name . '", ' .
      'profile_image_url = "' . $profile_image_url . '", ' .
      'user_id = ' . $user_id . ', ' .
      'name = "' . $name . '", ' .
      'location = "' . $oDB->escape($user_object->location) . '", ' . 
      'url = "' . $user_object->url . '", ' .
      'description = "' . $oDB->escape($user_object->description) . '", ' .
      'created_at = "' . $oDB->date($user_object->created_at) . '", ' .
      'followers_count = ' . $user_object->followers_count . ', ' .
      'friends_count = ' . $user_object->friends_count . ', ' .
      'statuses_count = ' . $user_object->statuses_count . ', ' . 
      'time_zone = "' . $user_object->time_zone . '", ' .
      'last_update = "' . $oDB->date($tweet_object->created_at) . '"' ;     

    if ($oDB->in_table('users','user_id="' . $user_id . '"')) {
      $oDB->update('users',$field_values,'user_id = "' .$user_id . '"');
    } else {            
      $oDB->insert('users',$field_values);
    }

    // percist status to database

    $field_values = 'tweet_id = ' . $tweet_id . ', ' ....


    //... Somethings are to be for da cook alone, its hard work          

            foreach ($entities->hashtags as $hashtag) {

      $where = 'tweet_id=' . $tweet_id . ' ' .
        'AND tag="' . $hashtag->text . '"';     

      if(! $oDB->in_table('tweet_tags',$where)) {

        $field_values = 'tweet_id=' . $tweet_id . ', ' .
          'tag="' . $hashtag->text . '"';   

        $oDB->insert('tweet_tags',$field_values);
      }
    }
    foreach ($entities->urls as $url) {

      if (empty($url->expanded_url)) {
        $url = $url->url;
      } else {
        $url = $url->expanded_url;
      }

      $where = 'tweet_id=' . $tweet_id . ' ' .
        'AND url="' . $url . '"';       

      if(! $oDB->in_table('tweet_urls',$where)) {
        $field_values = 'tweet_id=' . $tweet_id . ', ' .
          'url="' . $url . '"'; 

        $oDB->insert('tweet_urls',$field_values);
      }
    }       
  } 

  if(DEBUG){ 
     echo ob_get_contents();
     ob_clean();
  }else{
     ob_clean();
  }

  // Longer sleep equals lower server load
  sleep(1);
}
?>

这也非常适用于蜘蛛和爬虫,我有自己的团队来处理。如果有更好的方法,请告诉我,考虑到资源和可扩展性,作为一个持续连接的网站小部件,用于FB状态更新,就像再次使用Echelon作为电视遥控器一样。


0

0
只有在从服务器获取数据后才发送请求。在服务器上休眠,直到发现有新内容要发送并将响应发送回去。
保持以上状态,直到页面存活,您将看到数据被推送。 这避免了以固定间隔ping服务器。
我不确定他们是否在这里引用了https://en.wikipedia.org/wiki/Comet_(programming)

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