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

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

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

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

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

Ответ 1

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

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

Ответ 2

У меня очень простой пример чата как часть 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>

Ответ 3

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

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

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

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

Ответ 4

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

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

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

outputNewData();

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

Ответ 5

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

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

Ответ 6

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

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

Ответ 7

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

Ответ 8

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

Ответ 10

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

}

Ответ 11

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

Ответ 12

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

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

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

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

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

Ответ 13

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

Ответ 14

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

Ответ 15

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

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

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

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

Ответ 17

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

Ответ 18

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

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

Ответ 19

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