我可以找到许多有关Long Polling工作原理的信息(例如this和this),但是没有简单的示例说明如何在代码中实现此功能。

我所能找到的全部是cometd,这依赖于在Dojo JS框架和相当复杂的服务器系统上。.

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

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

#1 楼

它比我最初想象的要简单。基本上,您只有一个页面不执行任何操作,直到要发送的数据可用(例如,收到新消息)。

这是一个非常基本的示例,它会在2-10秒后发送一个简单的字符串。 1/3的机会返回错误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的长轮询代码构建的)...但是作为一个简单的示例,Apache绰绰有余!该脚本可以很容易地用任何一种语言编写(我选择了Apache / PHP,因为它们很常见,而我碰巧是在本地运行它们的)

然后,用Javascript请求上述文件(msg_srv.php ),然后等待响应。当您得到一个时,就对数据进行操作。然后,您请求文件并再次等待,对数据进行操作(并重复)。

接下来是该页面的示例。当页面加载后,它将发送对msgsrv.php的初始请求文件。如果成功,则将消息附加到#messages div,然后在1秒后再次调用waitForMsg函数,从而触发等待。

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

如果页面错误,则会附加#messages div的错误,等待15秒钟,然后重试(与我们在每条消息后等待1秒钟的方式相同)。

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

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

 <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条消息专门发送给该客户端?

–DevDevDev
09年10月7日,下午3:36

大概。为了说明这一概念,这是一个非常简化的示例。要更好地执行此操作,您将需要更复杂的服务器端代码,在该代码中,它将存储该特定客户端的那1000条消息,并将它们发送为一个块。您还可以安全地减少waitForMsg超时

– dbr
09-10-7 13:32

nodejs是用于长轮询请求的另一个出色的服务器端解决方案,它的另一个优点(相对于Twisted)还可以用Javascript编写服务器代码。

–沙哑
2011-2-25在19:10

这只是一个间隔为1秒的简单的到服务器的周期性AJAX连接。这与“长时间轮询”无关。只要客户端超时,长轮询将使连接保持活动状态。

– Deele
2011年5月5日12:46

问题是真正的PHP脚本代替了sleep(rand(2,10))是做什么的; ?为了什么也不做,每100毫秒轮询一次数据库?它什么时候决定死?

–Luis Siquot
2011-09-28 3:29

#2 楼

作为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>
 


评论


我可以知道如何始终保持联系吗?对不起,如果我问傻话,但我想知道。

– Rocky Singh
2011年1月22日23:31

它执行HTTP GET,并且服务器阻止GET,直到有可用数据为止。当数据到达服务器时,服务器将数据返回给客户端,将其他可能进入的队列排队,然后客户端重新连接并拾取丢失的消息(如果有),否则再次阻塞。

–达斯汀
2011年1月22日23:46

乍一看可能并不明显,但是事情是负责“始终保持连接状态”的是ajaxStop和那里的getNewComments回调,因此它只是在每个ajax请求结束时不断触发

– Baldrs
16年1月20日在16:41



#3 楼

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


由于(最后一条消息的数量)服务器使用URL来请求更新,因此服务器URLHandler接收到这些更新,并添加了一个回调以响应客户端到队列。
服务器收到新消息时,onmessage事件将触发,循环遍历回调并发送消息。
客户端JS接收消息,将其添加到页面中,然后请求更新因为此新消息ID。


#4 楼

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

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

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

outputNewData();


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

评论


这是一个繁忙的等待,阻塞了您当前的线程。这根本无法扩展。

– Wouter Lievens
09年9月24日在9:41

不,usleep不是忙碌的等待。而“等待”的全部要点是阻塞您的线程一段时间。可能他的意思是50毫秒(usleep(50000)),而不是50微秒!但是无论如何,使用典型的Apache / PHP设置,还有其他方法可以做到这一点吗?

–马特
11年11月27日在20:30

好吧,从原则上讲,您不能在没有等待的情况下为聊天消息设置阻止功能。

–TomášZato-恢复莫妮卡
13年11月26日在20:06

真的很棒!我在服务器中构建了一个递归函数来检查新数据。但是,有效利用长轮询的最佳产品是什么?我使用普通的Apache,当我打开超过4/5个浏览器选项卡时,服务器没有响应:(寻找与PHP配合使用的功能

–现代派
2014年5月7日在11:25

#5 楼

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



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

处理器:管理与网页的异步通信(本身)

IAsynchProcessor:该服务处理实现此接口的实例

服务:处理实现IAsynchProcessor的请求对象

请求:包含您的响应(对象)的IAsynchProcessor包装器

响应:包含自定义对象或字段


评论


好吧...所以这为什么被否决了?这些类确实是长轮询的有效示例。

–囚犯零
2011-09-28 17:36

真正的长轮询不是(简单地)在您对资源进行普通轮询时增加间隔的做法。它是更大模式的一部分...“有些”需要解释...但是仅在整个实现的某些区域中。就是说...这些课程遵循上述模式!因此,如果您有理由对此表示反对,我真的会对这个原因感兴趣。

–囚犯零
2012年6月12日19:05



也许它被否决了,因为它没有直接解决简单代码示例的问题。当然我没有投票否决,所以我只能猜测。

–安德鲁(Andrew)
2012年6月15日15:22

#6 楼

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

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

评论


我认为您应该只将其作为长轮询的介绍,因为这种实现肯定会杀死具有许多并发用户的服务器。

–阿尔弗雷德
2010-2-22在17:58

我只是在了解所有这些信息...几个用户是否可靠...说10来回聊天?

–somdow
2012年4月4日下午14:12

#7 楼

这是Erik Dubbelboer在PHP中使用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 楼

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

#9 楼

看一下这篇博客文章,其中包含用于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

Ryan

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以具有模板目录:

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将整数作为其第二个参数。

–安德鲁·赫奇斯(Andrew Hedges)
2010年6月21日在22:48

这个答案需要工作。它是一个或多个评论以及一个或多个单独答案的最终结果。

– Brian Webster
2012年9月20日下午5:40

#12 楼

这是PHP是非常糟糕的选择的一种情况。如前所述,您可以快速捆绑所有Apache工作者,执行类似的操作。 PHP是为启动,执行,停止而构建的。它不是为启动而创建的,请稍等...执行,停止。您将很快停顿服务器,发现您遇到了难以置信的扩展问题。

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

在Apache(或其他任何工具)之前设置nginx,它将负责保持打开并发连接。您只需通过将数据发送到内部地址来响应负载,这可以通过后台作业来完成,或者只是在新请求到来时将消息发送给正在等待的人。这可以防止PHP进程在长时间轮询期间处于打开状态。

这不是PHP独有的,可以使用nginx和任何后端语言来完成。并发打开连接负载等于Node.js,因此最大的好处就是它可以使您摆脱NEEDING Node的负担。

您会看到很多其他人提到其他语言库来完成长时间轮询,这是有充分理由的。自然,PHP并不是很好地针对这种类型的行为构建的。

评论


这是Apache问题还是PHP问题?如果我的PHP代码直接在nginx或lighttpd上直接运行,我是否会遇到长时间轮询的问题?

–大卫
15年7月16日在3:39



这不是一个PHP问题,而是一个PHP滥用问题。在每个请求上,PHP都会从头开始运行脚本,根据需要加载库,执行其代码,然后在垃圾收集请求中开始的所有内容时关闭脚本。多年来,对PHP进行了许多修改,以最大程度地减少后期静态绑定,延迟加载,内存字节码缓存中的影响,以删除磁盘I / O等影响。问题仍然在于,PHP打算尽快启动和停止尽可能。每次引导时将加载一次并为请求打开线程的语言更适合于长时间轮询。

– Brightball
15年7月27日在18:38



但是要回答这个问题,是的,无论您使用的是Apache还是其他工具,您都将遇到此问题。这就是PHP的工作方式。我应该修正一下,如果您将拥有已知的最大流量负载,PHP将会很好。我看到使用PHP的嵌入式系统没有问题,因为只有几个连接。潜在地在公司Intranet上也可以通过。但是对于面向公众的应用程序,随着流量的增长,您将绝对杀死服务器。

– Brightball
15年7月27日在18:45

#13 楼

为什么不考虑使用Web套接字而不是长时间轮询?它们非常高效且易于设置。但是,仅现代浏览器支持它们。这是快速参考。

评论


我认为,一旦Websocket在所有地方(可能不会在未来几年内)实现,它们将成为此类应用程序的标准。不幸的是,目前,我们不能依靠它们来生产应用程序。

–理查德
14年4月13日在18:44

@Richard但是,您可以使用Socket.IO之类的东西,它提供自动回退传输,一直到IE 6都提供类似Web套接字的功能。

–布拉德
2014年6月3日,下午3:06

#14 楼

WS-I小组发布了一种名为“可靠的安全配置文件”的文件,该文件具有Glass Fish和.NET的实现方式,它们之间的互操作性很好。

如果运气好的话,也可以使用Javascript实现。

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

也有商业付费版本。

#15 楼

对于ASP.NET MVC实现,请查看NuGet上提供的SignalR。.请注意,NuGet通常是Git来源的过时版本,因为它经常提交。

阅读有关SignalR的更多信息Scott Hanselman撰写的博客

#16 楼

您可以尝试使用libevent构建的C1000K C ++彗星服务器icomet(https://github.com/ideawu/icomet)。 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支持多种浏览器和OS,包括Safari(iOS,Mac), IE(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或其他对象(对其他流可见)的范围,并在准备就绪时调用<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,因此您应该可以使用map并通过api调用访问特定对象。