关于php:我如何实现基本的“长轮询”?

How do I implement basic “Long Polling”?

我可以找到很多关于轮询工作时间的信息(例如,this和this),但没有简单的代码实现示例。

我所能找到的只是Cometd,它依赖于DojoJS框架和一个相当复杂的服务器系统。

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

这个例子不需要是可伸缩的、安全的或完整的,它只需要工作就可以了!


比我最初想象的简单……基本上,您有一个不做任何事情的页面,直到您想要发送的数据可用为止(例如,一条新消息到达)。

下面是一个非常基本的例子,它在2-10秒后发送一个简单的字符串。返回错误404的几率为1/3(在接下来的javascript示例中显示错误处理)

msgsrv.php

1
2
3
4
5
6
7
8
9
10
11
<?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的长轮询代码而构建的)。但作为一个简单的例子,Apache已经足够了!这个脚本可以很容易地用任何语言编写(我选择了Apache/PHP,因为它们很常见,而且我碰巧在本地运行它们)。

然后,在javascript中,您请求上述文件(msg_srv.php并等待响应。当你得到一个,你就根据数据来行动。然后您请求文件并再次等待,根据数据操作(并重复)

下面是这样一个页面的例子。加载页面时,发送对msgsrv.php文件的初始请求。如果成功,我们将消息附加到#messagesDIV,然后在1秒后再次调用waitformsg函数,从而触发等待。

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

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

这种方法的好处在于它具有很强的弹性。如果客户端的Internet连接断开,它将超时,然后尝试重新连接-这是轮询工作时间的固有特征,不需要复杂的错误处理

总之,使用jquery框架的long_poller.htm代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<html>
<head>
    BargePoller
    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.2.6/jquery.min.js" type="text/javascript" charset="utf-8">

    <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(
           ""+ msg +""
        );
    }

    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 */
    });
   
</head>
<body>
   
       
            BargePoll message requester!
       
   
</body>
</html>


我有一个非常简单的聊天例子作为Slosh的一部分。

编辑:(因为每个人都在这里粘贴他们的代码)

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

注意,客户机总是与服务器有连接,一旦有人发送消息,每个人都应该立即大致看到它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<?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>
    slosh chat
    <script type="text/javascript"
      src="http://code.jquery.com/jquery-latest.js">
    <link title="Default" rel="stylesheet" media="screen" href="style.css" />
  </head>

  <body>
    Welcome to Slosh Chat

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

    <form method="post" action="#">
      Nick: <input id='
from' type="text" name="from"/>
      Message:
      <textarea id='
msg' name="msg"></textarea>
      <input type="submit" value="Say it" id="submit"/>
    </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("<span class='from'>" + from +":</span>" +
           " <span class='msg'>" + msg +"</span>");
        });
        // 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();
      });
   
  </body>
</html>


Tornado是为长轮询而设计的,在/examples/chatdemo中包含一个非常小的(几百行python)聊天应用程序,包括服务器代码和JS客户机代码。工作原理如下:

  • 客户机使用JS请求自(最后一条消息的数量)以来的更新,服务器urlhandler接收这些更新,并添加回调以响应客户机对队列的响应。

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

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


我认为客户机看起来像一个普通的异步Ajax请求,但您希望它需要"很长时间"才能恢复。

服务器看起来是这样的。

1
2
3
4
while (!hasNewData())
    usleep(50);

outputNewData();

因此,Ajax请求将发送到服务器,可能包括上次更新的时间戳,以便您的hasNewData()知道您已经得到了什么数据。然后,服务器进入一个休眠循环,直到新数据可用。一直以来,Ajax请求都是连接的,只是挂在那里等待数据。最后,当新数据可用时,服务器会将其提供给Ajax请求并关闭连接。


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

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

  • 这是一个关于如何使用php&jquery进行长时间轮询的5分钟的精彩截屏:网址:http://screener.com/snh

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


    下面是Erik Dubelboer在PHP中使用Content-type: multipart/x-mixed-replace头的一个简单的长轮询示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    <?

    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


    我用它来对付彗星,我也用JavaGLAISFISH服务器建立了彗星,并通过订阅CeMeDayLycom发现了很多其他的例子。


    看看这个博客文章,它有一个简单的聊天应用程序代码,在python/django/gevent中。


    下面是我为Inform8Web开发的一个长轮询解决方案。基本上,重写类并实现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

    赖安

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    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();

    }

    谢谢你的代码,DBR。只是在long ou poller.htm中的一个小错误。

    1
    1000 /* ..after 1 seconds */

    我想应该是

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

    为它工作。

    对于那些感兴趣的人,我尝试了一个相当于姜戈的。启动一个新的Django项目,比如长轮询的lp:

    1
    django-admin.py startproject lp

    为消息服务器调用应用程序msgsrv:

    1
    python manage.py startapp msgsrv

    将以下行添加到settings.py以具有模板目录:

    1
    2
    3
    4
    5
    import os.path
    PROJECT_DIR = os.path.dirname(__file__)
    TEMPLATE_DIRS = (
        os.path.join(PROJECT_DIR, 'templates'),
    )

    在urls.py中定义URL模式,如下所示:

    1
    2
    3
    4
    5
    6
    7
    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应该如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    from random import randint
    from time import sleep
    from django.http import HttpResponse, HttpResponseNotFound

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

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


    这是一个非常糟糕的PHP选择场景。如前所述,您可以非常快地将所有Apache工作人员绑定在一起,执行类似的操作。PHP是为开始、执行、停止而构建的。它不是为开始而构建的,等等…执行,停止。你会很快陷入服务器的困境,发现你有难以置信的伸缩问题。

    也就是说,您仍然可以使用PHP来实现这一点,并且不必使用nginx httppushstreammodule:http://wiki.nginx.org/httppushstreammodule来终止服务器。

    您在Apache(或其他)前面设置nginx,它将负责保持并发连接的开放性。您只需将数据发送到一个内部地址,就可以使用后台作业进行响应,或者只需将消息发送给等待新请求进入的人。这可以防止在长时间轮询期间PHP进程处于打开状态。

    这不是PHP独有的,可以在任何后端语言中使用nginx。并发开放连接的负载等于node.js,所以最大的好处是它可以让您不需要这样的节点。

    你会看到很多人提到其他语言库是为了完成长时间的投票,这是有充分理由的。PHP并不是为这种行为自然构建的。


    为什么不考虑Web套接字而不是长轮询呢?它们效率高且易于安装。但是,它们仅在现代浏览器中受支持。这是一个快速参考。


    WS-I小组发布了一个名为"可靠的安全概要"的东西,它有一个GlassFish和.NET实现,显然可以很好地进行交互操作。

    幸运的是,这里也有一个JavaScript实现。

    还有一个使用HTTP双工的Silverlight实现。您可以将javascript连接到Silverlight对象,以便在发生推送时获取回调。

    也有商业付费版本。


    对于ASP.NET MVC实现,请查看Nuget上可用的信号器。请注意,nuget通常是从git源代码中过期的,而git源代码的提交非常频繁。

    在Scott Hanselman的博客上阅读更多关于信号器的信息


    您可以尝试ICOMET(http://Github.com/IdWaWu/ICOMET),一个用LiBevor构建的C100K C++CeMET服务器。icomet还提供了一个javascript库,它使用起来非常简单

    1
    2
    3
    4
    5
    6
    7
    8
    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)、IES(Windows)、Firefox、Chrome等。


    最简单的节点

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    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



    '
    );
    });

    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 for ExMaple中,您可以在中间件中获得response。您需要做什么,可以将所有长时间轮询的方法范围扩大到映射或其他流可见的内容,并在准备就绪时调用 response.end()。长轮询连接没有什么特别的。REST只是您通常构造应用程序的方式。

    如果你不知道我所说的范围界定是什么意思,这应该能让你知道

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    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



    '
    );
    });

    // 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);

    正如您所看到的,您可以真正地响应所有连接,一个,做任何您想要的。每个请求都有EDOCX1[2],因此您应该能够使用map和访问特定的out-api调用。