基本的な「ロングポーリング」を実装するにはどうすればよいですか?


776

ロングポーリングのしくみに関する多くの情報(たとえば、thisthis)を見つけることができますが、これをコードに実装する簡単な例はありません。

私が見つけることができるのは、Dojo JSフレームワークに依存するcometdと、かなり複雑なサーバーシステムだけです。

基本的に、リクエストを処理するためにApacheをどのように使用し、新しいメッセージに対してサーバーを「ロングポーリング」する単純なスクリプト(たとえば、PHP)をどのように記述しますか?

この例は、スケーラブル、安全、または完全である必要はありません。動作する必要があるだけです。

回答:


512

最初に思ったよりも簡単です。基本的に、送信したいデータが利用可能になるまで(たとえば、新しいメッセージが到着するまで)、ページは何も実行しません。

これは本当に基本的な例で、2〜10秒後に単純な文字列を送信します。エラー404を返す3分の1の確率(次の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サーバーでこれを実行すると、すべての「ワーカースレッド」がすぐに拘束され、他の要求に応答できなくなります。これを回避する方法はいくつかありますが、リクエストごとに1つのスレッドに依存しない、Pythonのtwistedなどの「ロングポーリングサーバー」。cometDは人気のあるもの(複数の言語で利用可能)であり、トルネードはそのようなタスク(FriendFeedのロングポーリングコード用に作成された)のために特別に作られた新しいフレームワークです...しかし、簡単な例として、Apacheは十分すぎるほどです!このスクリプトは任意の言語で簡単に作成できます(非常に一般的なため、Apache / PHPを選択しましたが、たまたまローカルで実行しています)。

次に、JavaScriptで上記のファイル(msg_srv.php)を要求し、応答を待ちます。取得すると、データに基づいて行動します。次に、ファイルを要求して再度待機し、データを操作します(そして繰り返します)。

このようなページの例を次に示します。ページが読み込まれると、msgsrv.phpファイルの初期リクエストが送信されます。成功した場合は、メッセージを#messagesdivに追加し、1秒後にもう一度waitForMsg関数を呼び出します。これは待機をトリガーします。

1秒setTimeout()は本当に基本的なレートリミッターです。これがなくても正常に機能しますが、msgsrv.php 常に(たとえば、構文エラーで)瞬時に戻る場合、ブラウザーをフラッディングすると、すぐにフリーズする可能性があります。これには、ファイルに有効なJSON応答が含まれているかどうかを確認したり、1分あたり1秒あたりのリクエスト数の合計を維持したり、適切に一時停止したりすることをお勧めします。

ページにエラーが発生した場合、エラーを#messagesdivに追加し、15秒待機してから再試行します(各メッセージの後に1秒待機する方法と同じ)

このアプローチの良い点は、非常に弾力性があることです。クライアントのインターネット接続が停止すると、タイムアウトしてから再接続します。これは、ポーリングの動作時間に固有であり、複雑なエラー処理は必要ありません。

とにかく、long_poller.htmjQueryフレームワークを使用したコード:

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

7
このアイデアを使用して、いくつかのメッセージが抜けることができませんでしたか?その1秒のタイムアウトで、1000のチャットメッセージが送信されたとすると、サーバーは1000のメッセージをそのクライアントに送信することをどのようにして知るのでしょうか。
DevDevDev 2009年

15
多分。これは、概念を示すために非常に単純化された例です。これをより適切に行うには、より複雑なサーバー側コードが必要です。このコードでは、特定のクライアントの1000メッセージを格納し、それらを1つのチャンクで送信します。また、waitForMsgタイムアウトを安全に減らすこともできます
dbr

21
nodejsは、長いポーリング要求のためのもう1つの優れたサーバー側ソリューションであり、サーバーコードをJavaScriptでも記述できるという(Twistedに勝る)追加の利点があります。
ハスキー、

8
これは、1秒間隔のサーバーへの単純な再帰AJAX接続です。これは「ロングポーリング」とは関係ありません。長いポーリングは、クライアントのタイムアウトが発生する限り、接続を維持する必要があります。
Deele

6
問題は、実際のP​​HPスクリプトが代わりにsleep(rand(2,10));何を行うかです。何もしないために、100ミリ秒ごとにデータベースをポーリングしますか?いつ死ぬのか?
Luis Siquot、2011

41

私はsloshの一部として本当に簡単なチャットの例を持っています。

編集:(誰もがここにコードを貼り付けているため)

これは、ロングポーリングと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>

1
これが常に接続されている方法を知っていてもよいですか?ばかげた質問をしている場合は申し訳ありませんが、それを知りたいのですが。
ロッキーシン

4
これはHTTP GETを実行し、サーバーはデータが利用可能になるまでGETをブロックします。データがサーバーに到着すると、サーバーはデータをクライアントに返し、受信する可能性のあるものをすべてキューに入れ、クライアントは再接続して欠落しているメッセージがあればそれを取得し、そうでない場合は再びブロックします。
ダスティン

4
最初は明らかではないかもしれませんが、「常に接続された状態」の原因ajaxStopでのgetNewCommentsコールバックであり、すべてのajaxリクエストの終わりに無限に発火するだけです
1

32

Tornadoはロングポーリング用に設計されており、/ examples / chatdemoに最小限の(数百行のPythonの)チャットアプリが含まれています(サーバーコードとJSクライアントコードを含む)。それはこのように動作します:

  • クライアントはJSを使用して(最後のメッセージの数)以降の更新を要求し、サーバーURLHandlerはこれらを受信して​​、クライアントに応答するためのコールバックをキューに追加します。

  • サーバーが新しいメッセージを取得すると、onmessageイベントが発生し、コールバックをループしてメッセージを送信します。

  • クライアント側のJSはメッセージを受信して​​ページに追加し、この新しいメッセージID以降の更新を要求します。


25

クライアントは通常の非同期AJAXリクエストのように見えますが、戻ってくるまでに「長い時間」かかると思います。

サーバーは次のようになります。

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

outputNewData();

したがって、AJAX要求はサーバーに送信され、おそらく最後に更新されたときのタイムスタンプが含まれてhasNewData()いるため、すでに取得しているデータがわかります。サーバーは、新しいデータが利用可能になるまでループスリープ状態になります。その間、AJAXリクエストは接続されたままで、データを待っているだけです。最後に、新しいデータが利用可能になると、サーバーはそれをAJAXリクエストに渡し、接続を閉じます。


10
これは、現在のスレッドをブロックするビジー待機です。それはまったくスケーリングしません。
Wouter Lievens、

10
いいえ、usleepは忙しい待機ではありません。そして、「待機」の全体のポイントは、しばらくの間スレッドをブロックすることです。おそらく彼は50ミリ秒(usleep(50000))を意味していましたが、50マイクロ秒ではありませんでした!しかし、とにかく、典型的なApache / PHPセットアップで、これを行う他の方法はありますか?
マット

ええと、原理上、待たないとチャットメッセージのブロック機能は作れません。
トマーシュザト-モニカを復活させる'11

本当に素晴らしい!新しいデータをチェックするために、サーバーに再帰関数を作成しました。しかし、ロングポーリングを効率的に使用するのに最適な製品は何ですか。私は以上の4/5のブラウザタブを開いたとき、私は、通常のApacheを使用して、サーバが応答しません:( PHPで使用するために何かを探して
現代人

17

C#でのロングポーリングに使用するいくつかのクラスを次に示します。基本的に6つのクラスがあります(以下を参照)。

  1. Controller:有効な応答を作成するために必要なアクションを処理します(db操作など)
  2. プロセッサ:Webページ(それ自体)との非同期通信を管理します
  3. IAsynchProcessor:サービスは、このインターフェイスを実装するインスタンスを処理します
  4. サービス:IAsynchProcessorを実装するプロセス要求オブジェクト
  5. リクエスト:レスポンス(オブジェクト)を含むIAsynchProcessorラッパー
  6. 応答:カスタムオブジェクトまたはフィールドが含まれています

2
わかりました...だから、なぜこれが投票されたのですか?これらのクラスは確かにロングポーリングの有効な例です。
囚人ZERO

実際のロングポーリングは、(単純に)通常のポーリング(リソースに対して)を実行する間隔を増やすことではありません。これはより大きなパターンの一部です...これは「ある程度」解釈の対象となりますが、実装全体の特定の領域でのみです。つまり、これらのクラスは上記のパターンに従います!だから、これに投票する理由があるなら...私は本当にその理由に興味があります。
囚人ZERO

単純なコード例の問題に直接対応していないため、おそらくそれは否決されました。もちろん投票はしなかったので、推測しかできません。
Andrew

16

これは、PHPとjQueryを使用してロングポーリングを行う方法に関する5分間の素晴らしいスクリーンキャストです。http//screenr.com/SNH

コードは、上記のdbrの例と非常に似ています。


3
この実装は確かに多くの同時ユーザーでサーバーを強制終了するため、これはロングポーリングの概要としてのみ表示する必要があると思います。
アルフレッド、

私はこのすべてについて学ぶだけです...信頼できるかどうか、それは少数のユーザーと...それは10人が戻ってチャットしていると言いますか?
somdow 2012

12

以下は、ヘッダーを使用した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


11

私はこれを使ってCometに慣れるために、Java Glassfishサーバーを使用してCometをセットアップし、cometdaily.comにサブスクライブすることで他の多くの例を見つけました



9

以下は、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();

}

8

コードをありがとう、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は、2番目のパラメーターとして整数を取ります。
Andrew Hedges 2010年

この答えは仕事が必要です。これは、1つ以上のコメントと個別の回答の集大成です。
ブライアンウェブスター

8

これは、PHPが非常に悪い選択であるシナリオの1つです。前述のように、Apacheワーカーのすべてを非常に迅速に拘束して、このようなことを行うことができます。PHPは、起動、実行、停止用に構築されています。起動、待機、実行、停止用に構築されていません。サーバーをすぐに停止して、信じられないほどのスケーリングの問題があることに気づくでしょう。

そうは言っても、PHPでこれを行うことができ、nginx HttpPushStreamModule:http ://wiki.nginx.org/HttpPushStreamModuleを使用してサーバーを強制終了しないでください。

nginxをApache(またはその他)の前にセットアップすると、同時接続を開いたままにしておくことができます。バックグラウンドジョブで実行できる内部アドレスにデータを送信してペイロードで応答するか、新しいリクエストが着信するたびに待機していたユーザーにメッセージを送信します。これにより、PHPのプロセスが長時間のポーリング中に開いたままにならないようにします。

これはPHPに限定されるものではなく、任意のバックエンド言語でnginxを使用して実行できます。同時オープン接続の負荷はNode.jsと等しいため、最大の特典は、このようなもののためにノードを必要としないことです。

他の多くの人々がロングポーリングを達成するために他の言語ライブラリに言及しているのを見ていますが、それはもっともな理由があります。PHPは自然にこのタイプの振る舞いのためにうまく構築されていません。


これはApacheの問題ですか、それともPHPの問題ですか?PHPコードがnginxまたはlighttpdで直接実行された場合、ロングポーリングで問題が発生しますか?
デビッド

これは、PHPの問題ではなく、PHPの誤用です。すべてのリクエストで、PHPはスクリプトを最初から実行し、必要に応じてライブラリをロードし、そのコードを実行し、リクエストで開始されたすべてをガベージコレクションしながらシャットダウンします。遅い静的バインディング、遅延読み込み、メモリバイトコードキャッシュでのディスクI / Oの削除などの影響を最小限に抑えるため、長年にわたってPHPに多くの変更が加えられています。問題は、PHPがすぐに起動および停止することですできるだけ。1回/ブートでロードしてリクエストのスレッドを開く言語は、ロングポーリングに適しています。
ブライトボール

しかし、質問に答えるために、はい、Apacheを使用しているかどうかに関係なく、問題が発生するでしょう。これがPHPの動作方法です。これを修正して、最大トラフィック負荷がわかっている場合は、PHPで問題ないことをお伝えします。接続が2つしかないため問題のないPHPを使用した組み込みシステムを見てきました。これは、企業のイントラネットでは潜在的には無難かもしれません。ただし、一般向けのアプリケーションの場合、トラフィックの増加に応じてサーバーを完全に停止します。
ブライトボール2015

4

ロングポーリングの代わりにWebソケットを検討してみませんか?彼らは非常に効率的で簡単にセットアップできます。ただし、最新のブラウザでのみサポートされています。こちらがクイックリファレンスです。


WebSocketがどこにでも実装されると(おそらく今後数年はそうではないでしょう)、この種のアプリケーションの標準になると思います。残念ながら、今のところ、本番アプリではそれらに依存することはできません。
リチャード

3
ただし、すべての方法IE 6までのウェブソケットのような機能を提供する、自動フォールバックトランスポートを提供しSocket.IOのようなものを使用することができます@Richard
ブラッド・



2

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は、Safari(iOS、Mac)、IE(Windows)、Firefox、Chromeなど、幅広いブラウザーとOSをサポートしています。


0

最も単純な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);
}

たとえばresponseミドルウェアを使用する場合のExpressの運用に関するシナリオ。あなたがする必要があることを行い、長いポーリングされたすべてのメソッドを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の特定のout呼び出しにアクセスできるはずです。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.