【问题标题】:How do I implement basic "Long Polling"?如何实现基本的“长轮询”?
【发布时间】:2010-09-24 22:08:14
【问题描述】:

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

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

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

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

【问题讨论】:

    标签: php http comet


    【解决方案1】:

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

    这是一个非常基本的示例,它会在 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 这样的常规网络服务器上运行它会很快占用所有“工作线程”并使其无法响应其他请求。有一些方法可以解决这个问题,但它是建议在 Python 的 twisted 之类的东西中编写一个“长轮询服务器”,每个请求不依赖一个线程。 cometD 是一种流行的框架(有多种语言版本),Tornado 是专门为此类任务设计的新框架(它是为 FriendFeed 的长轮询代码构建的)......但作为一个简单的例子,A​​pache绰绰有余!这个脚本可以很容易地用任何语言编写(我选择了 Apache/PHP,因为它们很常见,而且我碰巧在本地运行它们)

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

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

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

    如果页面出错,它会将错误附加到#messages 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>
    

    【讨论】:

    • 不能用这个想法让一些消息溜走吗?在那 1 秒的时间里,假设发送了 1000 条聊天消息,服务器如何知道将 1000 条消息专门发送给该客户端?
    • 可能。这是一个非常简化的示例,用于演示这个概念。为了更好地做到这一点,您需要更精细的服务器端代码,它将为该特定客户端存储这 1000 条消息,并将它们以一个块的形式发送。您还可以安全地减少 waitForMsg 超时
    • nodejs 是另一个出色的长轮询请求服务器端解决方案,具有额外的优势(优于 Twisted),您也可以用 Javascript 编写服务器代码。
    • 这只是一个简单的周期性 AJAX 连接到服务器,间隔为 1 秒。这与“长轮询”无关。只要客户端超时,长轮询应该保持连接活动。
    • 问题是真正的 PHP 脚本做什么而不是 sleep(rand(2,10)); ?为了什么都不做,每 100 毫秒轮询一次数据库?它什么时候决定死?
    【解决方案2】:

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

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

    这是使用长轮询和slosh 的完整的基于 JSON 的多用户聊天。这是一个如何调用的demo,请忽略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>
    

    【讨论】:

    • 我可以知道这是如何始终连接的吗?对不起,如果我问了一些愚蠢的问题,但我想知道。
    • 它执行 HTTP GET 并且服务器阻止 GET 直到有可用数据。当数据到达服务器时,服务器将数据返回给客户端,将可能进入的任何其他内容排队,然后客户端重新连接并拾取丢失的消息(如果有),否则再次阻塞。
    • 一开始可能并不明显,但负责“始终连接状态”的是 ajaxStopgetNewComments 回调,所以它只是在每个 ajax 请求结束时无休止地触发它
    【解决方案3】:

    Tornado 是为长轮询而设计的,在 /examples/chatdemo 中包含一个非常小的(几百行 Python)chat app,包括服务器代码和 JS 客户端代码。它的工作原理是这样的:

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

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

    • 客户端 JS 接收到消息,将其添加到页面中,然后从这个新消息 ID 开始请求更新。

    【讨论】:

      【解决方案4】:

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

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

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

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

      【讨论】:

      • 这是一个阻塞当前线程的繁忙等待。这根本无法扩展。
      • 不,usleep 不是忙等待。而“等待”的全部意义在于将你的线程阻塞一段时间。可能他的意思是 50 毫秒(usleep(50000)),而不是 50 微秒!但无论如何,对于典型的 Apache/PHP 设置,是否有任何其他方法可以做到这一点?
      • 嗯,从原理上讲,你不能不等待就对聊天消息进行屏蔽。
      • 真的很棒!我在服务器中构建了一个递归函数来检查新数据。但是,有效使用长轮询的最佳产品是什么?我使用普通的 Apache,当我打开超过 4/5 个浏览器选项卡时服务器没有响应 :( 寻找与 PHP 一起使用的东西
      【解决方案5】:

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

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

      【讨论】:

      • 好的...为什么这被否决了?这些类确实是长轮询的有效示例。
      • 真正的长轮询不是(简单地)在您进行正常轮询时增加间隔的做法(在资源上)。它是一个更大模式的一部分……它“有点”需要解释……但仅限于整体实施的某些领域。也就是说......这些课程遵循上述模式!因此,如果您有理由对此投反对票……我真的会对这个原因感兴趣。
      • 也许它被否决了,因为它没有直接解决简单代码示例的问题。当然我没有投反对票,所以我只能猜测。
      【解决方案6】:

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

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

      【讨论】:

      • 我认为您应该只将其视为对长轮询的介绍,因为这种实现肯定会杀死您的服务器,同时拥有许多并发用户。
      • 我只是了解这一切...有多可靠,或者不可靠,是否有少数用户...比如说 10 人来回聊天?
      【解决方案7】:

      这里是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】:

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

        【讨论】:

          【解决方案9】:

          看看this blog post,它有一个简单的Python/Django/gevent聊天应用程序代码。

          【讨论】:

            【解决方案10】:

            以下是我为 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】:

              这是 PHP 非常糟糕的选择之一。如前所述,您可以非常快速地捆绑所有 Apache 工作人员来执行类似的操作。 PHP 是为启动、执行、停止而构建的。它不是为启动、等待...执行、停止而构建的。你会很快让你的服务器陷入瘫痪,并发现你有令人难以置信的扩展问题。

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

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

              这不是 PHP 独有的,可以使用带有任何后端语言的 nginx 来完成。并发打开的连接负载等于 Node.js,所以最大的好处是它让你摆脱了类似这样的需要节点。

              您看到很多其他人提到其他语言库来完成长轮询,这是有充分理由的。 PHP 天生就不能很好地适应这种行为。

              【讨论】:

              • 这是 Apache 问题还是 PHP 问题?如果我的 PHP 代码直接在 nginx 或 lighttpd 上运行,我会遇到长轮询问题吗?
              • 这不是 PHP 问题,而是 PHP 滥用。对于每个请求,PHP 从头开始​​运行脚本,根据需要加载库,执行其代码,然后在垃圾收集请求中启动的所有内容时关闭。多年来对 PHP 进行了许多修改,以尽量减少后期静态绑定、延迟加载、内存字节码缓存以删除磁盘 I/O 等影响。问题仍然是 PHP 旨在尽快启动和停止尽可能。将加载一次/启动并为请求打开线程的语言更适合长轮询。
              • 但是要回答这个问题,是的,无论您使用的是 Apache 还是其他东西,您都会遇到这个问题。这就是 PHP 的工作方式。我应该修改这个说,如果你有一个已知的最大流量负载 PHP 会很好。我见过使用 PHP 的嵌入式系统没有问题,因为只有几个连接。可能在公司 Intranet 上,这也可以通过。但是,对于面向公众的应用程序,随着流量的增长,您绝对会杀死您的服务器。
              【解决方案12】:

              感谢代码,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 中定义您的 URL 模式,如下所示:

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

              最后,templates/long_poller.htm 应该和上面的一样,并纠正了错字。希望这会有所帮助。

              【讨论】:

              • 其实"15000"是语法错误。 setTimeout 将整数作为其第二个参数。
              • 这个答案需要工作。它是一个或多个 cmets 和一个或多个单独答案的高潮。
              【解决方案13】:

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

              【讨论】:

              • 我认为一旦 websockets 在所有地方实现(可能不会在未来几年内),它们将成为此类应用程序的标准。不幸的是,目前我们不能依赖它们来生产应用程序。
              • @Richard 但是,您可以使用 Socket.IO 之类的东西,它提供自动回退传输,提供类似 web-socket 的功能,一直到 IE 6。
              【解决方案14】:

              WS-I 小组发布了一个名为 "Reliable Secure Profile" 的东西,其中有一条玻璃鱼,.NET implementation 显然是 inter-operate

              运气好的话,还有一个Javascript 实现。

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

              还有commercial paid versions

              【讨论】:

                【解决方案15】:

                对于 ASP.NET MVC 实现,请查看 SignalR which is available on NuGet。请注意,NuGet 与 Git source 相比通常已过时,Git source 提交非常频繁。

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

                【讨论】:

                  【解决方案16】:

                  你可以试试 icomet(https://github.com/ideawu/icomet),一个用 libevent 构建的 C1000K C++ comet 服务器。 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】:

                    最简单的 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 或其他东西(对其他流可见),并在你准备好时调用&lt;Response&gt; 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调用。

                    【讨论】:

                      猜你喜欢
                      • 2017-09-12
                      • 2020-04-26
                      • 2014-01-08
                      • 2016-03-10
                      • 2012-04-01
                      • 2013-02-17
                      • 2012-12-10
                      相关资源
                      最近更新 更多