Як я реалізую базовий "Довгий опитування"?


776

Я можу знайти багато інформації про те, як працює довге опитування (наприклад, це і це ), але немає простих прикладів того, як це реалізувати в коді.

Все, що я можу знайти, - це cometd , який спирається на систему Dojo JS, і досить складну серверну систему ..

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

Приклад не повинен бути масштабованим, надійним або повним, він просто повинен працювати!

Відповіді:


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, та / або підтримує загальну кількість запитів за хвилину / секунду та робить відповідну паузу.

Якщо сторінка помилка, вона додає помилку до #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
Не могли б деякі повідомлення пропустити за допомогою цієї ідеї? У той час, коли тайм-аут вийшов, скажімо, було надіслано 1000 повідомлень чату, як сервер знав би надіслати 1000 повідомлень саме цьому клієнту?
DevDevDev

15
Мабуть. Це дуже спрощений приклад, щоб продемонструвати концепцію. Щоб зробити це краще, вам знадобиться більш досконалий код на стороні сервера, де він би зберігав ці 1000 повідомлень для цього конкретного клієнта, і надсилав їх в один шматок. Ви також можете безпечно скоротити час очікування
forFormsg

21
nodejs - це ще одне чудове серверне рішення для тривалих запитів на опитування, з додатковою перевагою (над Twisted), що ви можете писати код сервера і в Javascript.
Хаскі

8
Це просто звичайні періодичні підключення AJAX до сервера з інтервалом 1 секунди. Це не має нічого спільного з "довгим опитуванням". Тривале опитування повинно підтримувати зв’язок живим до тих пір, поки клієнт не вистачить часу.
Діле

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>

1
Чи можу я знати, як це завжди пов'язано? Вибачте, якщо я прошу щось дурне, але я хочу це знати.
Рокі Сінгх

4
Це робить HTTP GET і сервер блокує GET, поки не будуть доступні дані. Коли дані надходять на сервер, сервер повертає дані клієнтові, чергає все, що ще може надходити, і тоді клієнт знову підключається і підбирає відсутні повідомлення, якщо такі є, інакше він блокується знову.
Дастін

4
Спочатку це може бути не очевидно, але справа в тому, що відповідальним за "завжди з'єднаний стан" є ajaxStop із getNewCommentsзворотним викликом там, тому він просто запускає його в кінці кожного запиту на ajax
1616

32

Tornado призначений для тривалого опитування і включає в себе дуже мінімальну (кілька сотень рядків Python) програми чату в / прикладах / chatdemo , включаючи код сервера та код клієнта JS. Це працює так:

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

  • Коли сервер отримує нове повідомлення, подія onmessage запускається, проходить цикл зворотних дзвінків та надсилає повідомлення.

  • JS на стороні клієнта отримує повідомлення, додає його на сторінку, а потім просить оновити з моменту цього нового ідентифікатора повідомлення.


25

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

Потім сервер виглядає приблизно так.

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

outputNewData();

Отже, запит AJAX надходить на сервер, ймовірно, включаючи часову позначку, коли було останнє оновлення, щоб ви hasNewData()знали, які дані у вас вже є. Потім сервер сидить у циклі сплячого, поки не з’являться нові дані. Весь час ваш запит AJAX все ще підключений, просто висить там, чекаючи даних. Нарешті, коли з’являються нові дані, сервер передає їх вашому запиту AJAX і закриває з'єднання.


10
Це зайняте очікування, яке блокує поточну нитку. Це зовсім не масштабується.
Wouter Lievens

10
Ні, сон - це не зайняте очікування. І вся суть "очікування" - блокувати свою нитку на деякий час. Ймовірно, він мав на увазі 50 мілісекунд (usleep (50000)), хоча, а не 50 мікросекунд! Але все одно, із типовою установкою Apache / PHP, чи є ще якийсь спосіб зробити це?
Метт

Ну, з принципу, ви не можете зробити функцію блокування для повідомлення в чаті без очікування.
Томаш Зато - Відновити Моніку

Чудово дійсно! Я створив рекурсивну функцію на сервері, щоб перевірити наявність нових даних. Але який найкращий продукт для ефективного використання тривалого опитування? Я використовую звичайний Apache, і сервер не відповідає, коли я відкриваю більше 4/5 вкладок браузера :( Шукаю щось для використання з PHP
модернізується

17

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

  1. Контролер : обробляє дії, необхідні для створення дійсної відповіді (db-операції тощо)
  2. Процесор : Керує асинхронним зв’язком із веб-сторінкою (сама)
  3. IAsynchProcessor : Сервіс обробляє екземпляри, які реалізують цей інтерфейс
  4. Сервіс : Процеси запитують об'єкти, які реалізують IAsynchProcessor
  5. Запит : обгортка IAsynchProcessor, що містить вашу відповідь (об'єкт)
  6. Відповідь : Містить власні об'єкти або поля

2
Добре ... так ЧОМУ це було проголосовано? Ці класи справді є дійсними прикладами тривалого опитування.
В'язень ЗЕРО

Справжнє тривале опитування - це не (просто) практика збільшення інтервалу, коли ви проводите звичайне опитування (на ресурсі). Вона є частиною більш масштабної моделі ... яка "дещо" піддається тлумаченню ... але лише в певних сферах загальної реалізації. Це сказало ... ці класи слідують зазначеній схемі! Тож, якщо у вас є причина проголосувати це рішення, я справді був би зацікавлений у цій причині.
В'язень ЗЕРО

Можливо, це було проголошено, оскільки це не стосується прямо питання простий приклад коду. Звичайно, я його не проголосував, тому можу лише здогадуватися.
Андрій

16

Це хороший 5-хвилинний скріншот про те, як проводити довге опитування за допомогою PHP & jQuery: http://screenr.com/SNH

Код досить схожий на приклад dbr вище.


3
Я думаю, ви повинні бачити це лише як вступ до тривалого опитування, тому що ця реалізація точно вб'є ваш сервер із багатьма одночасними користувачами.
Альфред

я просто дізнаюсь про все це ... наскільки надійним чи ні, це з кількома користувачами ... скажімо, 10 чатів назад вперед?
somdow

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

Нижче наведено довге рішення для опитування, яке я розробив для Інформ8 Веб. В основному ви переосмислюєте клас та реалізуєте метод 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, скажімо, 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)))

Нарешті, шаблони / long_poller.htm повинні бути такими ж, як вище, з виправленими помилками . Сподіваюсь, це допомагає.


Власне, "15000"це синтаксична помилка. setTimeout приймає ціле число як свій 2-й параметр.
Ендрю Хеджес

Ця відповідь потребує роботи. Це кульмінація одного чи декількох коментарів та окремої відповіді чи відповідей.
Брайан Вебстер

8

Це один із сценаріїв, для яких PHP - дуже поганий вибір. Як було сказано раніше, ви можете зв’язати всіх своїх працівників 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 запускає скрипт з нуля, завантажуючи бібліотеки за потребою, виконуючи його код, а потім вимикаючи, поки сміття збирає все розпочате в запиті. Протягом багатьох років PHP було внесено багато змін, щоб мінімізувати вплив, як, наприклад, пізні статичні прив’язки, ледача завантаження, кеші байт-коду пам'яті для видалення вводу-виводу диска тощо. Проблема залишається, що PHP призначений для запуску та зупинки якнайшвидше якомога. Мови, які завантажуються один раз / завантажуються та відкривають нитку для запиту, набагато краще підходять для тривалого опитування.
яскравий бал

Але, щоб відповісти на питання, так, ви відчували б це питання незалежно від того, використовували ви Apache чи щось інше. Просто так працює PHP. Я повинен змінити це, щоб сказати, що, якщо ви будете мати відоме максимальне завантаження трафіку, PHP буде добре. Я бачив вбудовані системи, що використовують PHP, які не мають проблем, оскільки є лише пара підключень. Потенційно в інтранеті компанії це також може бути прохідним. Для публічних додатків, однак, ви абсолютно вбиватимете свої сервери у міру зростання трафіку.
яскравий бал

4

Чому б не розглянути веб-розетки замість тривалого опитування? Вони набагато ефективніші та прості в налаштуванні. Однак вони підтримуються лише в сучасних браузерах. Ось коротка довідка .


Я думаю, що коли веб-розетки будуть впроваджені всюди (можливо, не на довгі роки), вони стануть стандартом для такого роду додатків. На жаль, поки що ми не можемо розраховувати на них у виробничих додатках.
Річард

3
@Richard Однак ви можете використовувати щось на кшталт Socket.IO, яке забезпечує автоматичні резервні перевезення, забезпечуючи функціональність, що нагадує веб-сокет, аж до IE 6.
Бред

3

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

При будь-якій удачі, є також реалізація Javascript .

Існує також реалізація Silverlight, яка використовує HTTP Duplex. Ви можете підключити javascript до об'єкта Silverlight, щоб отримувати зворотні дзвінки, коли відбувається натискання.

Також є комерційні платні версії .



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 тощо.


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.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.