如何实施基本的“长轮询”?

时间:2008-12-02 11:14:48

标签: php http comet

我可以找到很多有关长轮询工作方式的信息(例如,thisthis),但没有简单的示例,说明如何在代码中实现此功能

所有我能找到的是cometd,它依赖于Dojo JS框架,以及一个相当复杂的服务器系统..

基本上,我将如何使用Apache来处理请求,以及如何编写一个简单的脚本(例如,在PHP中),它将“长时间轮询”服务器以获取新消息?

该示例不必是可扩展的,安全的或完整的,它只需要工作!

18 个答案:

答案 0 :(得分:502)

它比我最初的想法简单。基本上你有一个什么都不做的页面,直到你想要发送的数据可用(比如,一条新消息到达)。

这是一个非常基本的例子,它在2-10秒后发送一个简单的字符串。三分之一的机会返回错误404(在即将到来的Javascript示例中显示错误处理)

msgsrv.php

<?php
if(rand(1,3) == 1){
    /* Fake an error */
    header("HTTP/1.0 404 Not Found");
    die();
}

/* Send a string after a random number of seconds (2-10) */
sleep(rand(2,10));
echo("Hi! Have a random number: " . rand(1,10));
?>

注意:对于一个真实的站点,在像Apache这样的常规Web服务器上运行它会快速占用所有“工作线程”并使其无法响应其他请求。有很多方法可以解决这个问题,但它是建议在Python的twisted之类的东西中编写一个“长轮询服务器”,它不依赖于每个请求的一个线程。 cometD是一种流行的版本(有多种语言版本可供选择),而Tornado是专门为此类任务制作的新框架(它是为FriendFeed的长轮询代码而构建的)...但是作为一个简单的例子,A​​pache绰绰有余!这个脚本很容易用任何语言编写(我选择了Apache / PHP,因为它们很常见,我碰巧在本地运行它们)

然后,在Javascript中,您请求上述文件(msg_srv.php),并等待响应。当你得到一个,你就会对数据采取行动。然后你请求文件并再次等待,对数据采取行动(并重复)

以下是此类页面的示例..加载页面时,它会发送msgsrv.php文件的初始请求。如果成功,我们会将消息附加到#messages div,然后在1秒后我们再次调用waitForMsg函数,这会触发等待。

1秒setTimeout()是一个非常基本的速率限制器,没有它可以正常工作,但如果msgsrv.php 总是立即返回(例如语法错误,例如) - 你淹没浏览器,它可以迅速冻结。最好检查文件是否包含有效的JSON响应,和/或保持每分钟/秒的运行总计请求,并进行适当的暂停。

如果页面出错,它会将错误附加到#messages div,等待15秒,然后再次尝试(与每条消息后等待1秒的方式相同)

这种方法的好处是它非常有弹性。如果客户端互联网连接中断,它将超时,然后尝试重新连接 - 这是轮询工作多长时间所固有的,不需要复杂的错误处理

无论如何,long_poller.htm代码,使用jQuery框架:

<html>
<head>
    <title>BargePoller</title>
    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.2.6/jquery.min.js" type="text/javascript" charset="utf-8"></script>

    <style type="text/css" media="screen">
      body{ background:#000;color:#fff;font-size:.9em; }
      .msg{ background:#aaa;padding:.2em; border-bottom:1px #000 solid}
      .old{ background-color:#246499;}
      .new{ background-color:#3B9957;}
    .error{ background-color:#992E36;}
    </style>

    <script type="text/javascript" charset="utf-8">
    function addmsg(type, msg){
        /* Simple helper to add a div.
        type is the name of a CSS class (old/new/error).
        msg is the contents of the div */
        $("#messages").append(
            "<div class='msg "+ type +"'>"+ msg +"</div>"
        );
    }

    function waitForMsg(){
        /* This requests the url "msgsrv.php"
        When it complete (or errors)*/
        $.ajax({
            type: "GET",
            url: "msgsrv.php",

            async: true, /* If set to non-async, browser shows page as "Loading.."*/
            cache: false,
            timeout:50000, /* Timeout in ms */

            success: function(data){ /* called when request to barge.php completes */
                addmsg("new", data); /* Add response to a .msg div (with the "new" class)*/
                setTimeout(
                    waitForMsg, /* Request next message */
                    1000 /* ..after 1 seconds */
                );
            },
            error: function(XMLHttpRequest, textStatus, errorThrown){
                addmsg("error", textStatus + " (" + errorThrown + ")");
                setTimeout(
                    waitForMsg, /* Try again after.. */
                    15000); /* milliseconds (15seconds) */
            }
        });
    };

    $(document).ready(function(){
        waitForMsg(); /* Start the inital request */
    });
    </script>
</head>
<body>
    <div id="messages">
        <div class="msg old">
            BargePoll message requester!
        </div>
    </div>
</body>
</html>

答案 1 :(得分:41)

我有一个非常简单的聊天示例,作为slosh的一部分。

修改 :(因为每个人都在这里粘贴代码)

这是使用长轮询和slosh的完整的基于JSON的多用户聊天。这是如何进行调用的演示,因此请忽略XSS问题。没有人应该在没有首先消毒的情况下进行部署。

请注意,客户端总是与服务器有连接,只要有人发送消息,每个人都应该立即看到它。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<!-- Copyright (c) 2008 Dustin Sallings <dustin+html@spy.net> -->
<html lang="en">
  <head>
    <title>slosh chat</title>
    <script type="text/javascript"
      src="http://code.jquery.com/jquery-latest.js"></script>
    <link title="Default" rel="stylesheet" media="screen" href="style.css" />
  </head>

  <body>
    <h1>Welcome to Slosh Chat</h1>

    <div id="messages">
      <div>
        <span class="from">First!:</span>
        <span class="msg">Welcome to chat. Please don't hurt each other.</span>
      </div>
    </div>

    <form method="post" action="#">
      <div>Nick: <input id='from' type="text" name="from"/></div>
      <div>Message:</div>
      <div><textarea id='msg' name="msg"></textarea></div>
      <div><input type="submit" value="Say it" id="submit"/></div>
    </form>

    <script type="text/javascript">
      function gotData(json, st) {
        var msgs=$('#messages');
        $.each(json.res, function(idx, p) {
          var from = p.from[0]
          var msg = p.msg[0]
          msgs.append("<div><span class='from'>" + from + ":</span>" +
            " <span class='msg'>" + msg + "</span></div>");
        });
        // The jQuery wrapped msgs above does not work here.
        var msgs=document.getElementById("messages");
        msgs.scrollTop = msgs.scrollHeight;
      }

      function getNewComments() {
        $.getJSON('/topics/chat.json', gotData);
      }

      $(document).ready(function() {
        $(document).ajaxStop(getNewComments);
        $("form").submit(function() {
          $.post('/topics/chat', $('form').serialize());
          return false;
        });
        getNewComments();
      });
    </script>
  </body>
</html>

答案 2 :(得分:31)

Tornado专为长轮询而设计,包含一个非常小的(几百行Python){/ 3}} / examples / chatdemo ,包括服务器代码和JS客户代码。它的工作原理如下:

  • 客户端使用JS来请求更新(上一条消息的数量),服务器URLHandler接收这些更新并添加回调以响应客户端到队列。

  • 当服务器收到新消息时,onmessage事件将触发,循环回调并发送消息。

  • 客户端JS接收消息,将其添加到页面,然后请求自此新消息ID以来的更新。

答案 3 :(得分:24)

我认为客户端看起来像是一个普通的异步AJAX请求,但是你希望它需要“很长时间”才能回来。

然后服务器看起来像这样。

while (!hasNewData())
    usleep(50);

outputNewData();

因此,AJAX请求进入服务器,可能包括上次更新时间的时间戳,以便hasNewData()知道您已经获得了哪些数据。 然后服务器处于休眠状态,直到新数据可用。一直以来,你的AJAX请求仍然是连接的,只是挂在那里等待数据。 最后,当有新数据可用时,服务器会将其提供给您的AJAX请求并关闭连接。

答案 4 :(得分:17)

Here是我在C#中用于长轮询的一些类。基本上有6个类(见下文)。

  1. 控制器:处理创建有效响应所需的操作(数据库操作等)。
  2. 处理器:管理与网页(本身)的异步通信
  3. IAsynchProcessor :服务处理实现此接口的实例
  4. 服务:处理实现IAsynchProcessor的请求对象
  5. 请求:包含响应(对象)的IAsynchProcessor包装器
  6. 响应:包含自定义对象或字段

答案 5 :(得分:16)

这是一个很好的5分钟截屏视频,介绍如何使用PHP&amp; jQuery的: http://screenr.com/SNH

代码与上面 dbr 的示例非常相似。

答案 6 :(得分:12)

以下a simple long-polling example in PHP by Erik Dubbelboer使用Content-type: multipart/x-mixed-replace标题:

<?

header('Content-type: multipart/x-mixed-replace; boundary=endofsection');

// Keep in mind that the empty line is important to separate the headers
// from the content.
echo 'Content-type: text/plain

After 5 seconds this will go away and a cat will appear...
--endofsection
';
flush(); // Don't forget to flush the content to the browser.


sleep(5);


echo 'Content-type: image/jpg

';

$stream = fopen('cat.jpg', 'rb');
fpassthru($stream);
fclose($stream);

echo '
--endofsection
';

这是一个演示:

http://dubbelboer.com/multipart.php

答案 7 :(得分:11)

我使用this来掌握Comet,我还使用Java Glassfish服务器设置了Comet,并通过订阅cometdaily.com找到了许多其他示例

答案 8 :(得分:9)

以下是我为Inform8 Web开发的长轮询解决方案。基本上,您重写该类并实现loadData方法。当loadData返回一个值或操作超时时,它将打印结果并返回。

如果您的脚本处理时间超过30秒,您可能需要将set_time_limit()调用更长时间。

Apache 2.0许可证。 github上的最新版本 https://github.com/ryanhend/Inform8/blob/master/Inform8-web/src/config/lib/Inform8/longpoll/LongPoller.php

赖安

abstract class LongPoller {

  protected $sleepTime = 5;
  protected $timeoutTime = 30;

  function __construct() {
  }


  function setTimeout($timeout) {
    $this->timeoutTime = $timeout;
  }

  function setSleep($sleep) {
    $this->sleepTime = $sleepTime;
  }


  public function run() {
    $data = NULL;
    $timeout = 0;

    set_time_limit($this->timeoutTime + $this->sleepTime + 15);

    //Query database for data
    while($data == NULL && $timeout < $this->timeoutTime) {
      $data = $this->loadData();
      if($data == NULL){

        //No new orders, flush to notify php still alive
        flush();

        //Wait for new Messages
        sleep($this->sleepTime);
        $timeout += $this->sleepTime;
      }else{
        echo $data;
        flush();
      }
    }

  }


  protected abstract function loadData();

}

答案 9 :(得分:9)

在Python / Django / this blog post中查看包含简单聊天应用程序代码的gevent

答案 10 :(得分:8)

感谢您的代码 dbr 。只是 long_poller.htm 中的一个小错误

1000 /* ..after 1 seconds */

我认为应该是

"1000"); /* ..after 1 seconds */

让它发挥作用。

对于那些感兴趣的人,我尝试了一个Django等价物。开始一个新的Django项目,比如 lp 进行长轮询:

django-admin.py startproject lp

为消息服务器调用应用 msgsrv

python manage.py startapp msgsrv

将以下行添加到 settings.py 以获得 模板 目录:

import os.path
PROJECT_DIR = os.path.dirname(__file__)
TEMPLATE_DIRS = (
    os.path.join(PROJECT_DIR, 'templates'),
)

urls.py 中定义您的网址格式:

from django.views.generic.simple import direct_to_template
from lp.msgsrv.views import retmsg

urlpatterns = patterns('',
    (r'^msgsrv\.php$', retmsg),
    (r'^long_poller\.htm$', direct_to_template, {'template': 'long_poller.htm'}),
)

msgsrv / views.py 应如下所示:

from random import randint
from time import sleep
from django.http import HttpResponse, HttpResponseNotFound

def retmsg(request):
    if randint(1,3) == 1:
        return HttpResponseNotFound('<h1>Page not found</h1>')
    else:
        sleep(randint(2,10))
        return HttpResponse('Hi! Have a random number: %s' % str(randint(1,10)))

最后,模板/ long_poller.htm 应与上面相同,并更正拼写错误。希望这会有所帮助。

答案 11 :(得分:7)

这是PHP的一个非常糟糕的选择。如前所述,您可以非常快速地绑定所有Apache工作人员。 PHP是为启动,执行,停止而构建的。它不是为了开始而构建的,等待......执行,停止。你很快就会陷入困境,发现你有令人难以置信的扩展问题。

那就是说,你仍然可以用PHP做到这一点并且不要使用nginx HttpPushStreamModule杀死你的服务器:http://wiki.nginx.org/HttpPushStreamModule

你在Apache(或其他任何东西)面前设置nginx,它将负责保持打开并发连接。您只需通过将数据发送到您可以使用后台作业执行的内部地址来响应有效负载,或者只是将消息发送给在新请求进入时等待的人员。这可以防止PHP进程在长轮询期间保持打开状态。

这不是PHP独有的,可以使用任何后端语言的nginx来完成。并发开放连接负载等于Node.js,因此最大的好处是它可以让你离开NEEDING Node这样的事情。

你看到很多其他人提到其他语言库来完成长时间的民意调查,这是有充分理由的。 PHP本身并不适合这种行为。

答案 12 :(得分:4)

为什么不考虑网络套接字而不是长轮询?它们非常高效且易于设置。但是它们仅在现代浏览器中受支持。这是quick reference

答案 13 :(得分:3)

WS-I小组发布了名为"Reliable Secure Profile"的内容,其中有一条Glass Fish和.NET implementation显然inter-operate

运气好的话还有Javascript实施。

还有一个使用HTTP Duplex.的Silverlight实现。当发生推送时,你可以connect javascript to the Silverlight反对获取回调。

还有commercial paid versions

答案 14 :(得分:2)

对于ASP.NET MVC实现,请查看SignalR which is available on NuGet。请注意,NuGet通常会过于频繁提交的Git source

blog on by Scott Hanselman

上阅读有关SignalR的更多信息

答案 15 :(得分:2)

你可以试试icomet(https://github.com/ideawu/icomet),一个用libevent构建的C1000K C ++彗星服务器。 icomet还提供了一个JavaScript库,它很容易使用,就像

一样简单
var comet = new iComet({
    sign_url: 'http://' + app_host + '/sign?obj=' + obj,
    sub_url: 'http://' + icomet_host + '/sub',
    callback: function(msg){
        // on server push
        alert(msg.content);
    }
});

icomet支持各种浏览器和操作系统,包括Safari(iOS,Mac),IE(Windows),Firefox,Chrome等。

答案 16 :(得分:0)

好吧,我不知道他们的想法,但我可以为您提供纯正,本土化和光彩的意见。

1)每用户每5秒执行一次ajax调用文本文件,该值有时为0,有时为1 2)如果有人发送您的客户消息,请通过php创建一个名为id + username.txt的文件(该文件在第一步中称为) 3)当邮件正在数据库中发送时,它还会在文本文件中插入值1 4)如果文本文件值等于1,则客户端击中服务器端以获取消息。 5)毕竟,客户端调用函数在其文本文件中插入1。

答案 17 :(得分:-1)

最简单的NodeJS

const http = require('http');

const server = http.createServer((req, res) => {
  SomeVeryLongAction(res);
});

server.on('clientError', (err, socket) => {
  socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
});

server.listen(8000);

// the long running task - simplified to setTimeout here
// but can be async, wait from websocket service - whatever really
function SomeVeryLongAction(response) {
  setTimeout(response.end, 10000);
}

Express中的生产智能方案,您可以在中间件中获得response。你需要做什么,可以将所有长轮询方法的范围扩展到Map或其他东西(其他流可见),并在你准备好时调用<Response> response.end()。长轮询连接没有什么特别之处。休息就是你正常构建应用程序的方式。

如果你不知道我的意思是什么,这应该给你一个想法

const http = require('http');
var responsesArray = [];

const server = http.createServer((req, res) => {
  // not dealing with connection
  // put it on stack (array in this case)
  responsesArray.push(res);
  // end this is where normal api flow ends
});

server.on('clientError', (err, socket) => {
  socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
});

// and eventually when we are ready to resolve
// that if is there just to ensure you actually 
// called endpoint before the timeout kicks in
function SomeVeryLongAction() {
  if ( responsesArray.length ) {
    let localResponse = responsesArray.shift();
    localResponse.end();
  }
}

// simulate some action out of endpoint flow
setTimeout(SomeVeryLongAction, 10000);
server.listen(8000);

如你所见,你可以真正回应所有的联系,一个,随心所欲。每个请求都有id,因此您应该能够使用api调用中特定的地图和访问权限。