Как мне реализовать базовый "длинный опрос"?

Я могу найти много информации о том, как работает длинный опрос (например, это и это), но нет простых примеров того, как реализовать это в коде.

Все, что я могу найти, это Cometd, который опирается на среду Dojo JS, и довольно сложную серверную систему.

В основном, как бы я использовал Apache для обслуживания запросов, и как бы я написал простой скрипт (скажем, на PHP), который бы "долго опрашивал" сервер на наличие новых сообщений?

Пример не должен быть масштабируемым, безопасным или законченным, он просто должен работать!

17 ответов

Решение

Это проще, чем я думал вначале. По сути, у вас есть страница, которая ничего не делает, пока не будут доступны данные, которые вы хотите отправить (скажем, приходит новое сообщение).

Вот действительно простой пример, который отправляет простую строку через 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, который не полагается на один поток на запрос. 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>

У меня есть очень простой пример чата как часть болтовни.

Изменить: (так как каждый вставляет свой код здесь)

Это полный многопользовательский чат на основе 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>

Tornado предназначен для длинных опросов и включает в себя очень небольшое (несколько сотен строк Python) приложение чата в /examples / chatdemo, включая код сервера и код клиента JS. Это работает так:

  • Клиенты используют JS для запроса обновлений, поскольку (номер последнего сообщения) сервер URLHandler получает их и добавляет обратный вызов для ответа клиента в очередь.

  • Когда сервер получает новое сообщение, событие onmessage срабатывает, перебирает обратные вызовы и отправляет сообщения.

  • JS на стороне клиента получает сообщение, добавляет его на страницу, а затем запрашивает обновления, так как этот новый идентификатор сообщения.

Я думаю, что клиент выглядит как обычный асинхронный AJAX-запрос, но вы ожидаете, что для его возврата потребуется "много времени".

Сервер выглядит следующим образом.

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

outputNewData();

Итак, запрос AJAX отправляется на сервер, возможно, с отметкой времени последнего обновления, чтобы ваш hasNewData() знает, какие данные у вас уже есть. Затем сервер зацикливается, пока не будут доступны новые данные. Все это время ваш AJAX-запрос все еще подключен, просто висит там в ожидании данных. Наконец, когда новые данные доступны, сервер передает их на ваш AJAX-запрос и закрывает соединение.

Вот некоторые классы, которые я использую для длинного опроса в C#. Есть в основном 6 классов (см. Ниже).

  1. Контроллер: обрабатывает действия, необходимые для создания правильного ответа (операции с БД и т. Д.)
  2. Процессор: управляет асинхронной связью с веб-страницей (сама)
  3. IAsynchProcessor: служба обрабатывает экземпляры, которые реализуют этот интерфейс
  4. Sevice: обрабатывает объекты запроса, которые реализуют IAsynchProcessor
  5. Запрос: Оболочка IAsynchProcessor, содержащая ваш ответ (объект)
  6. Ответ: содержит пользовательские объекты или поля

Это хороший 5-минутный скринкаст о том, как сделать длинный опрос с использованием PHP и jQuery: http://screenr.com/SNH

Код очень похож на приведенный выше пример dbr.

Вот простой пример длинного опроса в 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

Я использовал это, чтобы разобраться с Comet, я также настроил Comet, используя сервер Java Glassfish, и нашел много других примеров, подписавшись на cometdaily.com.

Ниже приводится длинное решение для опроса, которое я разработал для 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();

}

Это один из сценариев, для которых PHP является очень плохим выбором. Как упоминалось ранее, вы можете очень быстро связать всех своих сотрудников Apache, выполнив что-то подобное. PHP построен для запуска, выполнения, остановки. Он не создан для запуска, подождите... выполните, остановите. Вы очень быстро отключите свой сервер и обнаружите, что у вас невероятные проблемы с масштабированием.

Тем не менее, вы все равно можете сделать это с помощью PHP, и он не убьет ваш сервер с помощью nginx HttpPushStreamModule: http://wiki.nginx.org/HttpPushStreamModule

Вы устанавливаете nginx перед Apache (или чем-то еще), и он позаботится о том, чтобы удерживать открытыми параллельные соединения. Вы просто отвечаете полезной нагрузкой, отправляя данные на внутренний адрес, что вы можете сделать с фоновой работой, или просто отправляете сообщения людям, которые ждали, когда поступают новые запросы. Это не позволяет процессам PHP оставаться открытыми во время длительного опроса.

Это не только для PHP и может быть сделано с помощью nginx с любым языком бэкэнда. Нагрузка на одновременные открытые соединения равна Node.js, поэтому самый большой плюс - это то, что он вытаскивает вас из узла NEEDING для чего-то подобного.

Вы видите много других людей, которые упоминают о других языковых библиотеках для проведения длинных опросов, и для этого есть все основания. PHP просто не очень хорошо подходит для такого поведения.

Посмотрите на это сообщение в блоге, в котором есть код для простого приложения чата в Python / Django / gevent.

Спасибо за код, дбр. Просто небольшая опечатка в 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'),
)

Определите ваши шаблоны URL в 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)))

Наконец, templates /long_poller.htm должен быть таким же, как и выше, с исправленной опечаткой. Надеюсь это поможет.

Вот пример node.js, который поставляется с клиентом jquery. Там же инструкция по настройке на героку.

Почему бы не рассмотреть веб-сокеты вместо длинного опроса? Они очень эффективны и просты в настройке. Однако они поддерживаются только в современных браузерах. Вот краткий справочник.

Группа WS-I опубликовала нечто, называемое "Надежный безопасный профиль", в котором реализована реализация Glass Fish и .NET, которая, по - видимому, хорошо взаимодействует.

Если повезет, есть также реализация Javascript.

Существует также реализация Silverlight, которая использует HTTP Duplex. Вы можете подключить javascript к объекту Silverlight, чтобы получать обратные вызовы, когда происходит push.

Есть также коммерческие платные версии.

Вы можете попробовать icomet ( https://github.com/ideawu/icomet), кометный сервер C1000K C++, созданный с libevent. 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 и т. д.

Для реализации ASP.NET MVC посмотрите на SignalR, который доступен на NuGet. Обратите внимание, что NuGet часто устарел из источника Git, который получает очень частые коммиты.

Узнайте больше о SignalR в блоге Скотта Хансельмана

Хорошо , я не знаю, что они думают, но я могу предложить вам только чистое, естественное и блестящее мнение.

1) выполните ajax за 5 секунд для каждого пользователя, чтобы вызвать текстовый файл, значение которого иногда равно 0, а иногда - 1 2) если кто-то отправит ваше клиентское сообщение, создайте файл с именем id + username.txt с помощью php (это файл, который вызывается внутри первого шага) 3) когда сообщение отправляется в базу данных, он также вставляет значение 1 в текстовый файл 4) если значение текстового файла равно 1, клиентская сторона обращается к стороне сервера для получения сообщения. 5) В конце концов, клиент вызывает функцию, которая вставляет 1 в свой текстовый файл.

Простейший 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.

Другие вопросы по тегам