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

744

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

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

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

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

Теги:
http
comet

18 ответов

515
Лучший ответ

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

Вот действительно простой пример, который отправляет простую строку через 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 более чем достаточен! Этот script может быть легко написан на любом языке (я выбрал Apache/PHP, поскольку они очень распространены, и я случайно их запускал)

Затем в Javascript вы запрашиваете вышеуказанный файл (msg_srv.php) и ждите ответа. Когда вы его получите, вы действуете на данные. Затем вы запрашиваете файл и ждете еще раз, действуйте на данные (и повторяйте)

Далее следует пример такой страницы. Когда страница загружается, она отправляет исходный запрос для файла msgsrv.php. Если это удается, мы добавляем сообщение в div #messages, затем после 1 секунду мы снова вызываем функцию waitForMsg, которая вызывает ожидание.

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

Если ошибка страницы, она добавляет ошибку в div #messages, ждет 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>
  • 7
    Не могли ли некоторые сообщения проскользнуть через эту идею? Скажем, за 1 секунду, например, было отправлено 1000 сообщений чата, как сервер узнает, что нужно отправить 1000 сообщений конкретно этому клиенту?
  • 15
    Наверное. Это очень упрощенный пример, демонстрирующий концепцию. Чтобы сделать это лучше, вам понадобится более сложный код на стороне сервера, где он будет хранить эти 1000 сообщений для этого конкретного клиента и отправлять их в одном блоке. Вы также можете безопасно уменьшить время ожидания waitForMsg
Показать ещё 15 комментариев
41

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

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

Это полный многопользовательский чат на основе JSON, использующий длительный опрос и slosh. Это демонстрация того, как выполнять вызовы, поэтому, пожалуйста, игнорируйте проблемы 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 <[email protected]> -->
<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, пока не будут доступны данные. Когда данные поступают на сервер, сервер возвращает данные клиенту, ставит в очередь все, что еще может прийти, а затем клиент повторно подключается и забирает пропущенные сообщения, если таковые имеются, в противном случае он снова блокируется.
Показать ещё 1 комментарий
30

Tornado предназначен для длительного опроса и включает в себя очень минимальное (несколько сотен строк Python) chat app в / examples/chatdemo, включая код сервера и код клиента JS. Он работает следующим образом:

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

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

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

25

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

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

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

outputNewData();

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

  • 10
    Это занятое ожидание, которое блокирует ваш текущий поток. Это совсем не масштабируется.
  • 10
    Нет, уснуть - это не занятое ожидание. И весь смысл "ожидания" заключается в том, чтобы заблокировать ваш поток на некоторое время. Вероятно, он имел в виду 50 миллисекунд (usleep (50000)), а не 50 микросекунд! Но в любом случае, с типичной настройкой Apache / PHP, есть ли другой способ сделать это?
Показать ещё 3 комментария
17

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

  • Контроллер: обрабатывает действия, необходимые для создания допустимого ответа (операции db и т.д.).
  • Процессор: управляет асинхронной связью с веб-страницей (сам)
  • IAsynchProcessor. Службы обрабатывают экземпляры, реализующие этот интерфейс.
  • Sevice: обрабатывает объекты запроса, которые реализуют IAsynchProcessor
  • Запрос: оболочка IAsynchProcessor, содержащая ваш ответ (объект)
  • Ответ: содержит пользовательские объекты или поля
  • 2
    Хорошо ... так ПОЧЕМУ это было отклонено? Эти классы действительно являются действительными примерами длинных опросов.
  • 0
    Реальный длительный опрос - это (не просто) практика увеличения интервала, когда вы проводите обычный опрос (на ресурсе). Это часть более широкой модели ... которая "несколько" подлежит интерпретации ... но только в определенных областях общей реализации. Тем не менее ... эти классы следуют указанному шаблону! Так что, если у вас есть причина для голосования это ... Я действительно был бы заинтересован в причине.
Показать ещё 1 комментарий
16

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

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

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

Вот простой пример длинного опроса в PHP от Erik Dubbelboer с помощью заголовка 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

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

9

Ниже приведен длинный опрос, который я разработал для Inform8 Web. В основном вы переопределяете класс и реализуете метод loadData. Когда loadData возвращает значение или время выполнения операции, оно будет печатать результат и возвращать.

Если обработка вашего script может занять более 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

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

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, чтобы иметь каталог templates:

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

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

  • 0
    На самом деле, "15000" является синтаксической ошибкой. setTimeout принимает целое число в качестве второго параметра.
  • 0
    Этот ответ требует работы. Это кульминация одного или нескольких комментариев и отдельного ответа или ответов.
7

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

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

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

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

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

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

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

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

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

3

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

В любом случае существует Javascript реализация.

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

Существуют также коммерческие платные версии.

2

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

2

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

Подробнее о SignalR в блоге от Скотта Гензельмана

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 для exmaple вы получите response в промежуточном программном обеспечении. Вы, что вам нужно сделать, можете охватить все методы с длинным опросом на карте или что-то (что видно на другие потоки) и вызвать <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.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню