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

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

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

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

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

312783 次浏览

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

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

服务器看起来像这样。

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

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

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

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

msgsrv.php

<?phpif(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的扭曲中编写一个“长轮询服务器”,它不依赖于每个请求一个线程。cometD是一个流行的(有几种语言可用),龙卷风是专门为此类任务制作的新框架(它是为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>

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

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

这是使用长轮询和晃动的完整基于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>

谢谢你的代码,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.pathPROJECT_DIR = os.path.dirname(__file__)TEMPLATE_DIRS = (os.path.join(PROJECT_DIR, 'templates'),)

urls.py中定义您的URL模式:

from django.views.generic.simple import direct_to_templatefrom 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 randintfrom time import sleepfrom 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应该与上述相同,并更正错别字。希望这有帮助。

这是一个关于如何使用PHP和jQuery进行长时间轮询的5分钟截屏:http://screenr.com/SNH

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

看看这篇博客文章,它有Python/Django/GEvent中简单聊天应用程序的代码。

龙卷风专为长轮询而设计,在/示例/聊天演示中包含一个非常小的(几百行Python)聊天应用程序,包括服务器代码和JS客户端代码。它的工作原理如下:

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

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

  • 客户端JS接收消息,将其添加到页面,然后要求更新此新消息ID。

WS-I小组发布了一个名为“可靠的安全配置文件”的东西,它有一条玻璃鱼,. NET实现显然是互操作

幸运的是,那里也有一个javascript实现。

还有一个Silverlight实现,它使用HTTP双工。 You can将javascript连接到Silverlight对象在推送发生时获取回调。

还有商业付费版本

下面是我为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 datawhile($data == NULL && $timeout < $this->timeoutTime) {$data = $this->loadData();if($data == NULL){
//No new orders, flush to notify php still aliveflush();
//Wait for new Messagessleep($this->sleepTime);$timeout += $this->sleepTime;}else{echo $data;flush();}}
}

protected abstract function loadData();
}

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

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

对于ASP.NET的MVC实现,请查看SignalR在NuGet上可用…请注意,NuGet通常与git源过时,这会导致非常频繁的提交。

阅读更多关于SignalR的信息作者:Scott Hanselman

这是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';

这里是一个demo:

http://dubbelboer.com/multipart.php

您可以尝试icomet(https://github.com/ideawu/icomet),这是一个用libEvent构建的C1000KC++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 pushalert(msg.content);}});

icomet支持广泛的浏览器和操作系统,包括Safari(iOS,Mac),IE(Windows),Firefox,Chrome等。

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

这是PHP是一个非常糟糕的选择的场景之一。如前所述,您可以非常快速地将所有Apache工作人员捆绑在一起执行这样的操作。PHP是为启动、执行、停止而构建的。它不是为启动、等待…执行、停止而构建的。您会很快陷入服务器,并发现您遇到了令人难以置信的扩展问题。

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

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

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

您会看到很多其他人提到其他语言库来完成长轮询,这是有充分理由的。PHP只是没有很好地构建这种类型的行为。

最简单的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 reallyfunction 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 infunction SomeVeryLongAction() {if ( responsesArray.length ) {let localResponse = responsesArray.shift();localResponse.end();}}
// simulate some action out of endpoint flowsetTimeout(SomeVeryLongAction, 10000);server.listen(8000);

如你所见,你真的可以响应所有连接,第一,做任何你想做的事情。每个请求都有id,所以你应该能够使用map和访问特定的out of api调用。