@kirsan_vlz read-only
Пользователь
16 сентября 2011 в 12:41

Разработка → Простая реализация long polling механизма на PHP

Сейчас довольно популярно использование Comet-технологии, «когда при которой постоянное HTTP-соединение позволяет веб-серверу отправлять (push) данные браузеру, без дополнительного запроса со стороны браузера», согласно википедии.
Реализаций этой технологии есть много разных, но я сейчас хочу остановиться на одной из них, которая называется Long Polling. В статье я разберу, что это такое и с чем его едят.
Ну а тем, кто знает, что это, возможно, будетинтересно посмотреть на реализацию, которая не использует сторонний софт для своей работы — только PHP. Зачем это нужно, если есть специальные comet-сервера, которые выдержат гораздо более высокие нагрузки, чем скрипт на PHP? Это может пригодиться, если нужно сделать небольшой проект без высоких нагрузок, а на хостинге не дают ставить сторонний софт. Ну и если мне в рамках одного задания, где как раз нельзя было использовать сторонние приложения, пришлось реализовать данный функционал, то почему бы не поделиться.

Теория

Итак, что же из себя представляет Long Polling?
Выглядит это примерно следующим образом:
1) Клиент отсылает на сервер обычный ajax-запрос
2) Сервер, вместо того, чтобы быстро обработать этот запрос и отправить ответ клиенту, запускает цикл, в каждой итерации которого следит за возникновением событий (другой клиент добавил запись или удалил).
3) При возникновении события сервер генерирует ответ и отсылает его клиенту, таким образом завершая запрос.
4) Клиент, получив ответ от сервера, запускает обработчик события и параллельно отправляет очередной «длинный» запрос серверу.

То есть всё довольно просто и понятно. Однако, при реализации возникает несколько вопросов, которые нужно решить.

Шаги к практике


В первую очередь возникает вопрос о том, как скрипты будут взаимодействовать друг с другом. Ведь на каждый запрос клиента к серверу создаётся независимая копия PHP-скрипта. То есть нужна какая-то общая память для хранения данных о событиях.
Традиционный вариант — запускается демон, который оперирует событиями и держит соединение с клиентами. Использование демона естественным образом решает проблему памяти. Клиенты обращаются для получения событий не на веб-сервер, а к демону. В то же время, клиент, инициализирующий событие, также сообщает демону, что произошло событие. И таким образом всё крутится в памяти этого демона. который вообще может быть написан на чём угодно.
Но нам, в виду шила в одном месте и желания обойтись без сторонних приложений, демон на чём угодно не подойдёт. А писать демона на PHP, на мой вкус, не слишком интересно. У хостера могут быть проблемы с изменением максимального времени работы скрипта, а ещё продумывать интерфейсы взаимодействия клиентов с этим демоном… Нет, мы пойдём другим путём.
А остаётся только решить, как реализовать общую память в самом PHP. В голову приходят варианты хранения событий в файлах и БД, но хочется-то, чтобы всё было поближе — в памяти. Memcache нам, увы недоступен, а что тогда делать? А есть в php такое понятие как shared memory, точнее понятие-то не является особенностью этого языка, но нам дают возможность этим пользоваться. Механизмы этой технологии позволяют нам создать ячейку в оперативной памяти и использовать её в своих целях. Вот её я и буду использовать.

Практика

Попробуем представить интерфейс класса, который будет реализовывать нужный нам функционал.
Какие нужны методы?
1) Естественно, метод «прослушки», который будет вызываться, когда клиенты будут посылать polling-запросы, и следить за возникающими событиями. Я назвал этот метод listen.
2) Кроме того нам понадобится метод, который будет создавать событие. Этот метод носит имя push.
3) Вполне вероятно, что мы захотим обработать данные о событии, прежде чем отправить результат клиенту. Поэтому я добавил метод, который регистрирует событие и связывает с ним обработчик. Называется метод registerEvent.

В итоге получаем такой интерфейс класса:
class Polling
{
    /**
     * Генерация события
     * @param string $event Имя события
     * @param array $data Параметры события
     */
     public function push($event, $data = null);
	
    /**
     * "Слежение"за возникновением событий. Время работы зависит от опции max_execution_time
     * @param int $lastQueryTime Время завершения последнего запроса на "прослушивание".
     * Используется для того ,чтобы при медленном соединении не терялись события, возникшие
     * между запросами. Если не указан, то события отслеживаются с момента начала запроса
     * @return array Массив с результатами выполнения обработчиков для возникших событий
     */
     public function listen($lastQueryTime = null);

    /**
     * Регистрация события и назначение обработчика. Поддерживается по одному обработчику на событие.
     * @param string $event Имя события
     * @param callback $callback Функция-обработчик
     */
     public function registerEvent($event, $callback);
}


Несколько примечаний:
1) В методе registerEvent поддерживается только по одному обработчику на событие по причине того, что возвращаемое обработчиком значение возвращается клиенту как данные события. В одном обработчике можно вызвать несколько функций и из результата их работы собрать ответ клиенту. Впрочем переделать код для поддержки нескольких обработчиков можно без особых затруднений.
2) В методе listen используется параметр lastQueryTime, который позволяет обработать события, возникшие раньше, чем поступил запрос на прослушку. Это может пригодиться когда нагрузка на сеть высока и между завершением одного polling-запроса от клиента и началом другого может пройти заметный промежуток времени, в который возникают события.
3) В приведённом коде не учитывается одновременный доступ к памяти. А вообще. для более надёжной работы желательно использовать семафоры.
4) В методе listen используется вызов функции sleep(1). Это нужно для того, чтобы уменьшить количество холостых итераций. Частоты обновления раз в секунду вполне достаточно для имитации реалтайма.

Ну и на клиенте всё также предельно просто. Нам нужен метод, который будет посылать polling-запрос серверу, ждать ответ, запускать обработчики событий и заново посылать запрос. Ну и метод, регистрирующий сами обработчики событий.

Исходники:
Описанный класс Polling на PHP.
Если кому-нибудь понадобится, приведу в порядок и выложу клиентскую часть.
@kirsan_vlz
карма
35,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (23)

  • +1
    > В приведённом коде не учитывается одновременный доступ к памяти.
    Т.е. приведенный код на самом деле нерабочий?
    • 0
      Рабочий, но при больших нагрузках может возникнуть ситуация, что два клиента одновременно станут писать данные в память. Результат непредсказуем, но, скорее всего, событие в итоге потеряется.
      Вообще. тут код приведён как иллюстрация к статье, а не готовое решение. Я его взял из тестового задания, которое недавно делал, для тех задач его хватило, на серьёзных приложениях понадобится немного дописать, о чём я предупредил.
      • 0
        Стоп, под большие нагрузки такой бред никто ставить не будет — отдельный инстанс php на каждого клиента — это даже не для малых нагрузок. Речь о том, что оно рано или поздно рванет вообще без нагрузки.
        • 0
          Ну большие — это такие, при которых вероятность возникновения двух событий в один момент времени хоть сколько-нибудь ощутима )
          У меня чат крутился на этом коде неделю, проблем с поллингом не было, но и сидели там 5 человек.
          Тут же статья не о том. что я сделал готовое универсальное решение, а в том, чтобы показать, что поллинг — это не так сложно, как многим кажется, и если приспичит, ничего сложного в написании такого механизма нет.
          • 0
            Тут еще момент потребления памяти и процессора каждым клиентов, ведь для каждого запроса подымается вся эта машина из вебсервера, пхп, и всего остального.
    • 0
      Поставьте туда семафоры, для этого надо дописать пять новых строчек.

      Но так вы избежите одновременного доступа к памяти.
      • 0
        Я про семафоры в статье писал.
        • 0
          Прошу прощения, действительно, не заметил.

  • +1
    Есть предложение уменьшить вложеность конструкций в методе Polling::listen, потому что читаемость кода страшно падает. Сделать это можно добавив дополнительную точку выхода к первому условию и поменяв на обарное второе, чтобы получилось нечто подобное:
    if ($shmid) {
        return $result;
    }
    while (time() < $endTime) {
        if ($storage['_updated'] < $lastQueryTime) {
            continue;
        }
         .......
        break;     
    }
    
    • 0
      Ну да, обычно так и делаю, тут пропустил, а потом внимание не обратил. Спасибо, что заметили.
  • 0
    Как же органично для этого подходит Node.JS!
    Создаём сервер, но не отправляем конечный ответ, только заголовки для использования chunked-режима. Далее — делаем сокет-сервер, слушающий какой-либо порт и, при поступлении события в сокет (справится обычный fsockopen() в PHP) просто дописываем отбивку 1024 байт (для осла) + данные. Итого — не переустанавливается соединение и в ифрейм постоянно сыпятся данные.
    • 0
      Да тут много чего более органично подойдёт. Ниже вот про realplexor справедливо заметили.
      Но красивые решения требуют сторонний софт и node.js тут, увы, не исключение.
  • 0
    Зачем свой велосипед, когда есть dklab_realplexor?
    • 0
      Когда я задание получил, первым делом про реалплексор вспомнил, но в моём случае мне нельзя было использовать сторонний софт. Пришлось самому писать, впрочем это было интересно.
      А вообще, тут цель статьи не дать готовое решение, а показать, как это работает. Многие почему-то считают, что это совсем сложно, хотя это не так.
      • 0
        Соглачен, это принцип работы совсем простой. Да и работает довольно прозрачно, это я про realplexor.
    • 0
      Я работал и с реалплектором и с Нодой. Вердикт — реалплексор кажется (только кажется) надёжнее, но Нода быстрее. Тяжёлый траффик они держат одинаково, но в top нет процесса перла, отжирающего 15-20% 4-х ядерного процессора.
      ИМХО — мой выбор — Node.JS
    • 0
      лучше даже ape, имхо. прост в установке и «hello, world» с пулом примерно в 10 строк.
      • 0
        прошу прощения, ссылка: www.ape-project.org/
      • 0
        Ape очень любит процессор и память, даже в режиме простоя.
        На 2х процессорном сервере он ел 15% в простое.
        • 0
          вы его, видимо, как-то неправильно настроили. Облако на selectel'е потребляет 230 мб оперативки и 0.03% (!) CPU в простое.
          Установлены и работают: nginx, mongod, php-cgi, aped, memcached
      • 0
        Я выбирал между ape и realplexor. Остановился на realplexor — он полностью готов к работе с php из коробки.
  • 0
    А вообще, мне кажется, что такой код должен решаться не на уровне PHP точно…
  • 0
    Вообще, проблема начнется в тот момент, когда количество онлайнеров превысит количество воркеров веб-сервера или количество fastcgi процессов. При нынешних ценах на vps можно себе позволить пользоваться ими для реальных задач.

    Ну а в качестве proof-of-concept такой пример прокатит, да.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.