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

776

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

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

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

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

DBR
источник

Ответы:

512

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

Вот действительно простой пример, который отправляет простую строку через 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файла. Если это удается, мы добавляем сообщение в #messagesdiv, затем через 1 секунду мы снова вызываем функцию waitForMsg, который вызывает ожидание.

1 секунда setTimeout()- это действительно простой ограничитель скорости, без него он работает нормально, но если он msgsrv.php всегда возвращается мгновенно (например, с синтаксической ошибкой), вы загружаете браузер и он может быстро зависнуть. Это лучше сделать, проверяя, содержит ли файл правильный ответ JSON, и / или сохраняя общее количество запросов в минуту / секунду, и соответствующим образом останавливая.

Если страница ошибается, она добавляет ошибку в #messagesdiv, ждет 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>
DBR
источник
7
Не могли ли некоторые сообщения проскользнуть через эту идею? Скажем, в течение 1 секунды было отправлено 1000 сообщений чата, как сервер узнает, что нужно отправить 1000 сообщений конкретно этому клиенту?
DevDevDev
15
Вероятно. Это очень упрощенный пример, демонстрирующий концепцию. Чтобы сделать это лучше, вам понадобится более сложный код на стороне сервера, где он будет хранить эти 1000 сообщений для этого конкретного клиента и отправлять их в одном блоке. Вы также можете безопасно уменьшить время ожидания
waitForMsg
21
nodejs - это еще одно превосходное решение на стороне сервера для длинных запросов на опрос, с дополнительным преимуществом (по сравнению с Twisted) в том, что вы также можете писать код сервера в Javascript.
Хаски
8
Это просто обычные регулярные AJAX-соединения с сервером с интервалом в 1 секунду. Это не имеет ничего общего с "длинным опросом". Длительный опрос должен поддерживать соединение, пока не истечет время ожидания клиента.
Deele
6
вопрос в том, что делает настоящий PHP-скрипт вместо sleep(rand(2,10));? чтобы ничего не делать, опрашивать базу каждые 100 милисек? когда он решает умереть?
Луис Сиквот
41

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

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

Это полный многопользовательский чат на основе 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>
Dustin
источник
1
Могу ли я знать, как это всегда связано? Извините, если я спрашиваю что-то глупое, но я хочу это знать.
Рокки Сингх
4
Он выполняет HTTP GET, и сервер блокирует GET, пока не будут доступны данные. Когда данные поступают на сервер, сервер возвращает данные клиенту, ставит в очередь все, что еще может поступить, а затем клиент повторно подключается и забирает пропущенные сообщения, если таковые имеются, в противном случае он снова блокируется.
Дастин
4
Поначалу это может быть неочевидно, но дело в том, что за «всегда подключенное состояние» отвечает ajaxStop с getNewCommentsобратным вызовом, поэтому он просто бесконечно запускает его в конце каждого ajax-запроса
baldrs
32

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

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

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

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

mikemaccana
источник
25

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

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

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

outputNewData();

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

Greg
источник
10
Это занятое ожидание, которое блокирует ваш текущий поток. Это совсем не масштабируется.
Ваут Ливенс
10
Нет, уснуть - это не напряженное ожидание. И весь смысл "ожидания" заключается в том, чтобы заблокировать ваш поток на некоторое время. Вероятно, он имел в виду 50 миллисекунд (usleep (50000)), а не 50 микросекунд! Но в любом случае, с типичной настройкой Apache / PHP, есть ли другой способ сделать это?
Мэтт
Ну, из принципа, вы не можете сделать функцию блокировки для сообщения чата без ожидания.
Томаш Зато - Восстановить Монику
Отлично, правда! Я построил рекурсивную функцию на сервере для проверки новых данных. Но какой продукт лучше всего использовать для длительного опроса? Я использую обычный Apache, и сервер не отвечает, когда я открываю более 4/5 вкладок браузера :( Ищу что-то, что можно использовать с PHP
moderns
17

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

  1. Контроллер : обрабатывает действия, необходимые для создания правильного ответа (операции с БД и т. Д.)
  2. Процессор : управляет асинхронной связью с веб-страницей (сама по себе)
  3. IAsynchProcessor : служба обрабатывает экземпляры, которые реализуют этот интерфейс
  4. Sevice : обрабатывает объекты запроса, которые реализуют IAsynchProcessor
  5. Запрос : Оболочка IAsynchProcessor, содержащая ваш ответ (объект)
  6. Ответ : содержит пользовательские объекты или поля
Узник ноль
источник
2
Хорошо ... так ПОЧЕМУ это было отклонено? Эти классы действительно являются действительными примерами длинных опросов.
Заключенный НОЛЬ
Реальный длительный опрос - это не (просто) практика увеличения интервала, когда вы проводите обычный опрос (на ресурсе). Это часть более широкой модели ... которая "несколько" подлежит интерпретации ... но только в определенных областях общей реализации. Тем не менее ... эти классы следуют указанному шаблону! Так что, если у вас есть причина для голосования, я действительно был бы заинтересован в причине.
Заключенный НОЛЬ
Возможно, он был отвергнут, так как он напрямую не затрагивает вопрос простого примера кода. Конечно, я не проголосовал, поэтому могу только догадываться.
Андрей
16

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

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

Шон О
источник
3
Я думаю, что вы должны рассматривать это только как введение в длительный опрос, потому что эта реализация наверняка убьет ваш сервер со многими одновременными пользователями.
Альфред
я просто узнаю обо всем этом ... насколько надежным или нет, это с несколькими пользователями ... скажем, 10 болтать обратно?
Сомдов
12

Вот простой пример длинного опроса в 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, я также настроил Comet, используя сервер Java Glassfish, и нашел много других примеров, подписавшись на 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

Спасибо за код, дбр . Просто небольшая опечатка в 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 должен быть таким же, как указано выше, с исправленной опечаткой. Надеюсь это поможет.

xoblau
источник
На самом деле, "15000"это синтаксическая ошибка. setTimeout принимает целое число в качестве второго параметра.
Эндрю Хеджес
Этот ответ требует работы. Это кульминация одного или нескольких комментариев и отдельного ответа или ответов.
Брайан Уэбстер,
8

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

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

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

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

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

brightball
источник
Это проблема Apache или проблема PHP? Будут ли у меня проблемы с длительным опросом, если мой PHP-код будет работать непосредственно на nginx или lighttpd?
Дэвид
Это меньше проблема PHP и больше злоупотребление PHP. При каждом запросе PHP запускает скрипт с нуля, загружая библиотеки по мере необходимости, выполняет свой код и затем завершает работу во время сбора мусора, запущенного в запросе. За прошедшие годы в PHP было сделано множество модификаций для минимизации таких последствий, как поздние статические привязки, отложенная загрузка, кэширование байт-кода в памяти для удаления дискового ввода-вывода и т. Д. Проблема остается в том, что PHP предназначен для запуска и остановки так же быстро насколько это возможно. Языки, которые будут загружаться один раз / boot и открывать поток для запроса, гораздо лучше подходят для длительного опроса.
бейсбол
Но чтобы ответить на вопрос, да, вы столкнетесь с проблемой независимо от того, используете ли вы Apache или что-то еще. Это просто, как работает PHP. Я должен изменить это, чтобы сказать, что, если вы собираетесь иметь известную максимальную нагрузку трафика, PHP будет в порядке. Я видел встраиваемые системы, использующие PHP, у которых нет проблем, потому что есть только пара соединений. Потенциально во внутренней сети компании это также может быть сносно. Для общедоступных приложений вы, безусловно, убьете свои серверы по мере роста трафика.
светлый мяч
4

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

Шаси Кант
источник
Я думаю, что как только веб-сокеты будут внедрены повсеместно (вероятно, не на долгие годы), они станут стандартом для такого рода приложений. К сожалению, на данный момент мы не можем полагаться на них в производственных приложениях.
Ричард
3
@Richard Однако вы можете использовать что-то наподобие Socket.IO, который обеспечивает автоматические резервные транспорты, обеспечивая функциональность, подобную веб-сокетам, вплоть до IE 6.
Брэд,
3

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

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

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

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

goodguys_activate
источник
2

Вы можете попробовать 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 и т. д.

ideawu
источник
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);
}

Мудрый сценарий производства в 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.

sp3c1
источник