代码之家  ›  专栏  ›  技术社区  ›  dbr

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

  •  753
  • dbr  · 技术社区  · 16 年前

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

    我能找到的就是 cometd 它依赖于DojoJS框架和相当复杂的服务器系统。

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

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

    17 回复  |  直到 6 年前
        1
  •  499
  •   Minko Gechev    12 年前

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

    下面是一个非常基本的例子,它在2-10秒后发送一个简单的字符串。返回错误404的几率为1/3(在接下来的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的长轮询代码构建的)。但作为一个简单的例子,Apache已经足够了!这个脚本可以很容易地用任何语言编写(我选择了Apache/PHP,因为它们很常见,而且我碰巧在本地运行它们)。

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

    下面是这样一个页面的例子。加载页面时,它将发送 MSGSRV.PHP 文件。如果成功,我们将消息附加到 #messages DIV,然后在1秒后,我们再次调用waitformsg函数,这将触发等待。

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

    如果页面出错,它会将错误附加到 信息传递 DIV,等待15秒,然后重试(与我们在每条消息后等待1秒的方式相同)

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

    不管怎样, 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>
    
        2
  •  41
  •   j0k gauthamp    12 年前

    我有一个非常简单的聊天示例作为 slosh .

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

    这是使用长轮询和 晃动 . 这是一个 演示 关于如何进行调用,请忽略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>
    
        3
  •  31
  •   johndodo    11 年前

    Tornado 是为长轮询而设计的,并且包含非常少的(几百行python) chat app 中/ 示例/聊天演示 包括服务器代码和JS客户端代码。工作原理如下:

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

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

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

        4
  •  24
  •   Greg    16 年前

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

    服务器看起来是这样的。

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

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

        5
  •  17
  •   Community Bob Smith    7 年前

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

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

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

    代码与 DBR 上面的例子。

        7
  •  12
  •   dbr    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

        8
  •  11
  •   adam    16 年前

    我用过 this 为了掌握彗星,我还使用JavaGLASISFISH服务器建立了彗星,并通过订阅CeMeDayLy.com找到了许多其他例子。

        9
  •  9
  •   Ryan Henderson    13 年前

    下面是我为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

    赖安

    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();
    
    }
    
        10
  •  8
  •   xoblau    15 年前

    谢谢你的密码, DBR . 只是一个小的打字错误 龙眼 绕线

    1000 /* ..after 1 seconds */
    

    我想应该是

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

    为它工作。

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

    django-admin.py startproject lp
    

    调用应用程序 MSGSRV 对于消息服务器:

    python manage.py startapp msgsrv
    

    将下列行添加到 设置Py 有一个 模板 目录:

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

    在中定义URL模式 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/ VIEW 应该看起来像:

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

    最后,模板/ 龙眼 应与上述相同,并纠正打字错误。希望这有帮助。

        11
  •  8
  •   Denis    14 年前

    看一看 this blog post 在python/django中有一个简单的聊天应用程序的代码/ gevent .

        12
  •  7
  •   brightball    10 年前

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

    也就是说,您仍然可以使用PHP来完成这项工作,并且不要使用nginx httppushstreammodule来终止服务器: http://wiki.nginx.org/HttpPushStreamModule

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

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

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

        13
  •  4
  •   Community Bob Smith    7 年前

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

        14
  •  3
  •   Community Bob Smith    7 年前

    WS-I组发布了一个名为 "Reliable Secure Profile" 有一个玻璃鱼和 .NET implementation 显然 inter-operate 好。

    幸运的是 Javascript 在那里也可以实现。

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

    也有 commercial paid versions 也。

        15
  •  2
  •   makerofthings7    13 年前

    对于ASP.NET MVC实现,请查看signal which is available on NuGet …请注意,nuget通常从 Git source 这是非常频繁的承诺。

    阅读有关 blog on by Scott Hanselman

        16
  •  2
  •   ideawu    11 年前

    你可以试试iComet( https://github.com/ideawu/icomet )一个用LiBevor构建的C1000 K C++CeMET服务器。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)、IES(Windows)、Firefox、Chrome等。

        17
  •  -1
  •   sp3c1    6 年前

    最简单的节点

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

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

    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 对于每个请求,您应该能够使用map并访问特定的out-api调用。