首页 文章

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

提问于
浏览
751

我可以找到很多关于Long Polling如何工作的信息(例如,thisthis),但没有关于如何在代码中实现它的简单示例 .

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

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

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

17 回答

  • 17

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

    这是一个非常基本的例子,它在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服务器上运行它会快速占用所有"worker threads"并让它无法响应其他请求..有很多方法可以解决这个问题,但建议写一个"long-poll server"类似Python的twisted,它不依赖于每个请求一个线程 . cometD是一个流行的(有几种语言版本),Tornado是专门为这些任务制作的新框架(它是为FriendFeed的长轮询代码而构建的)......但作为一个简单的例子,Apache绰绰有余!这个脚本很容易用任何语言编写(我选择了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>
    
  • 4

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

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

    这是使用长轮询和slosh的完整的基于JSON的多用户聊天 . 这是一个如何进行调用的 demo ,所以请忽略XSS问题 . 没有人首先要对它进行消毒,就不应该进行部署 .

    请注意,客户端始终与服务器 Build 连接,并且只要有人发送消息,每个人都应该立即看到它 .

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

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

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

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

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

  • 496

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

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

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

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

  • 8

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

    • Controller :处理创建有效响应所需的操作(数据库操作等)

    • Processor :管理与网页的异步通信(本身)

    • IAsynchProcessor :服务流程实现此接口的实例

    • Sevice :处理实现IAsynchProcessor的请求对象

    • Request :包含响应的IAsynchProcessor包装器(对象)

    • Response :包含自定义对象或字段

  • 7

    这是一个很好的5分钟截屏视频,介绍如何使用PHP和jQuery进行长轮询:http://screenr.com/SNH

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

  • 8

    这是a simple long-polling example in PHP by Erik Dubbelboer使用 Content-type: multipart/x-mixed-replace Headers :

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

  • 24

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

  • 2

    下面是我为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();
    
    }
    
  • 11

    感谢您的代码, 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以具有 templates 目录:

    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应与上面相同,并更正拼写错误 . 希望这可以帮助 .

  • 2

    看一下this blog post,其中包含Python / Django / gevent中简单聊天应用的代码 .

  • -1

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

    也就是说,您仍然可以使用PHP执行此操作,并且不要使用nginx HttpPushStreamModule杀死您的服务器:http://wiki.nginx.org/HttpPushStreamModule

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

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

    你看到很多其他人提到其他语言库来完成长时间的轮询,这是有充分理由的 . PHP本身并不适合这种类型的行为 .

  • 41

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

  • 16

    WS-I小组发布了一个名为"Reliable Secure Profile"的东西,它有一条玻璃鱼和.NET implementation,显然很好 .

    运气好的话,那里也有Javascript实施 .

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

    还有commercial paid versions .

  • 31

    对于ASP.NET MVC实现,请查看SignalR which is available on NuGet ..请注意,NuGet经常会从Git source过时,这会非常频繁地提交 .

    blog on by Scott Hanselman上阅读有关SignalR的更多信息

  • 9

    你可以试试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等 .

  • 12

    最简单的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调用中特定的 Map 和访问权限 .

相关问题