Сегодня речь пойдет о
phpDaemon — асинхронном модульном демоне-фреймворке, который берёт на себя обработку I/O (libevent) и другие низкоуровневые задачи, присущие демонам. С его помощью легко писать правильные сетевые приложения с блэкджеком и шлюхами.
Из коробки идут сервера FastCGI, HTTP, CGI, FlashPolicy, Telnet,
WebSocket (!) — да-да тот самый
волшебный пендаль новый протокол от Google. И клиенты mysql, memcached, mongodb… И многое другое, полный список под катом. Работать с сетью действительно просто. Программист средней руки может написать, к примеру, IRC-бота за считанные часы.
В качестве наглядного примера я реализовал
вот этот чат на
phpDaemon +
WebSocket +
MongoDB +
jQuery. Он наглядно демонстрирует преимущества этой технологии: доставка сообщений мгновенна, накладные расходы при обмене данными минимальны, высока производительность, приложение масштабируется горизонтально.
Исходники серверной части этого чата (в данный момент 17 кб). Прошу заметить, чат тестировался и работает в Chrome, FF, IE6+, Iron, Safari.
Где это лучше применить?
Область применения phpDaemon очень широка как в Веб-разработке, так и за её пределами. С его помощью хорошо делать real-time многопользовательские игры (например на Flash), сервисы с мгновенным взаимодействием, чаты, IM-гейты… Также хорошо использовать необходимые в хозяйстве полезности, такие как сервер flashpolicy.
Самое первое реальное применение проекта в production — мгновенная доставка личных сообщений: на страницах сайта был невидимый swf-файл (Flash) в несколько килобайт, он подключался к серверу и ждал команд. Как только пользователю приходило сообщения, Flash посылал команду странице (Javascript), который в свою очередь сразу же отображал всплывающую иконку и «пиликал». Мы с коллегами были поражены скоростью реакции, звук от соседнего компьютера был слышен еще до того как отправитель успевал оторвать палец от левой кнопки мыши.
А совсем недавно по заказу был реализован сервер предоставляющий API к Asterisk'у (для IP-телефонии), прекрасно держит нагрузку.
Архитектура и возможности
Приложения в
phpDaemon содержат лишь логику обработки, а все низкоуровневые вызовы происходят автоматически. Проект написан исключительно на PHP, и вы, вероятно уже задались вопросом производительности. I/O происходит через libevent, библиотеку хорошо зарекомендовавшую себя, которая используется в memcached и в других известных проектах. При этом, скорость выполнения инструкций в PHP весьма велика, он превосходит в этом многие другие языки своей группы. В привычных всем синхронных скриптах львиная доля времени уходит на запуск среды и на блокировку при вводе-выводе, например при запросе к базе данных обычный скрипт не продолжит выполнение пока ответ не будет получен. В phpDaemon таких блокировок нет: отослали запрос, повесили если нужно callback-функцию на ответ, и пошли по своим делам.
phpDaemon — это 1 мастер-процесс и множество рабочих процессов, каждый из которых представляет собой среду выполнения приложений. Приложение является классом-наследником AppInstance, который описывает его реакцию на различные события. При запуске рабочих процессов, они создают экземпляры классов-приложений и выполняют метод init(). В методе init() приложение может забиндить сокет, куда-то подключиться, или открыть локальный дескриптор (при этом одно не исключает другого). Приложение также может взаимодействовать с другими приложениями в рабочем процессе, объявляя в них необходимые параметры: например, прописать роут в WebSocketServer.
После того как оно забиндило сокет, при поступлении подходящих соединений будет вызвано событие onAccepted(connId,addr). Пример этого метода из WebSocketServer:
public function onAccepted($connId,$addr)
{
$this->sessions[$connId] = new WebSocketSession($connId,$this);
$this->sessions[$connId]->clientAddr = $addr;
}
* This source code was highlighted with Source Code Highlighter.
Сервер отдающий политику crossdomain по 843 порту весит всего 1,67 кб —
FlashPolicy.php. Между тем, его явное преимущество перед множеством аналогов очевидно: его нельзя положить открыв десяток-другой телнет-сессий. Многие популярные реализации flashpolicyd DoS-ятся открытием даже одной сессии, которая ничего не шлет, поскольку в них идёт синхронная обработка, и процесс ждет у моря погоды.
Для обработки HTTP-запросов (в том числе по FastCGI) существует отдельная сущность — классы-наследники Request. Эта сущность имеет входные параметры (get,post,cookie,server...) и состояния — run/sleep/dead. Запросы висят в очереди, и диспетчер вызывает их по порядку. Если запросу не нужно прерываться, он может сделать всю работу и вернуть код завершения, чтобы диспетчер удалил его из очереди. Если запрос производит операции, требующие ожидания (например делает запрос к MongoDB), то ему необходимо сделать $this->sleep(30) чтобы заснуть максимум на 30 на секунд. А в callback-функции к запросу MongoDB достаточно указать $request->wakeup(), чтобы немедленно прогнать сон. Тогда диспетчер обратится к нему незамедлительно. Запрос сможет продолжить своё выполнение, имея ответ от MongoDB. Если же ответ по каким-то причинам не получен, запрос может вывести «We're sorry, try again shortly later». Для полного завершения запроса в методе run() вызывается либо return 1, либо $this->terminate().
После того как заголовки запроса приняты приложения-сервера (коробочные — FastCGI, HTTP) обращаются к appResolver'у, который определяет к кому приложению поступает запрос и вызывает у приложения метод beginRequest. А он, в свою очередь, создает и возвращает экземпляр класса-наследника Request. Затем объект запроса кладется в очередь (queue). Приложение может по желанию добавлять в очередь и собственные «запросы» посредством метода pushRequest, это необходимо чтобы вызывать определенный код через задаваемые промежутки времени (например, это делается в MongoNode для опроса курсора).
Запросами в полной мере поддерживаются POST-данные (и multipart), Upload'ы, и прочее (
страничка-пример), более того — больше нет необходимости вручную обрабатывать UCS-2 кодировку в запросах (%uFFFF) — это происходит автоматически.
Поддерживаются X-Sendfile (запись ответа на запроса веб-сервера в файл) и Request-Body-File (чтение body-часть запроса из файла). Можно начать выполнение запроса до того как тело полностью принято, и можно программировать обработку upload'а (например, кидать сразу в memcached, а не во временный файл на диске).
Предвижу просьбы дать конкретные цифры показывающие выигрыш в производительности, но некорректно сравнивать синхронные и асинхронные фреймворки. Понятно что последние быстрее. Однако, разница будет напрямую зависеть от приложения, а именно — от количества и времени блокировок в синхронном варианте. Добавлю лишь субъективный бенчмарк странички-примера… Requests per second: 4784.80 [#/sec] (mean).
В данный момент используется последняя стабильная версия libevent (1.4.10-stable), скоро будет выпущена libevent 2.0, это даст возможность рабочим процессам принимать соединения через epoll_wait и принесет дополнительный выигрыш в производительности перед текущим вариантом. Как только так сразу.
Управляемость, администрирование, конфигурирование
Встроенный управляющий скрипт:
# phpd
usage: phpd (start|(hard)stop|update|reload|(hard)restart|fullstatus|status|configtest|help)…
# phpd fullstatus
[STATUS] phpDaemon 0.2 is running (/var/run/phpdaemon.pid).
State of workers:
Total: 1
Idle: 1
Busy: 0
Shutdown: 1
Pre-init: 0
Wait-init: 0
Init: 0
Рабочими процессами поддерживается команды suid, sgid, chroot. Они задаются на уровне конфигурации. Встроенный динамический MPM (Multi-Process Manager) определяет загруженность рабочих процессов и запускает новые в рамках настроек. Используя API, алгоритм MPM можно переопределить в конфигурационном файле. При разработке проекта делается упор как на расширяемость и заменяемость компонентов, так и на простоту использования.
Искаропки
На данный момент в дистрибутив включены следующие модули:
- FastCGI — позволяет подключаться к приложениям по протоколу FastCGI.
- HTTP — позволяет подключаться к приложениям по HTTP.
- CGI — позволяет пускать обычные CGI-приложения по FastCGI, HTTP и любым другим транспортам.
- Flashpolicy — раздает политику crossdomain для Flash по 843 порту.
- WebSocketServer — обслуживает WebSocket-сеансы.
- MongoClient — драйвер MongoDB.
- MySQLClient — драйвер MySQL (через него также можно подключаться к SphinxQL).
- MySQLProxy — проксирующий MySQL-сервер.
- MongoNode — реализация slave-ноды MongoDB, позволяет вешать события на изменения объектов в базе.
- MemcacheClient — драйвер Memcache.
- LockServer — распределенный сервис блокировок.
- LockClient — клиент для собственного сервиса блокировок.
- TelnetHoneypot — простейший сервер telnet.
- RTEPServer/RTEPClient — реализация Real-Time Events Protocol, позволяющая клиентам подписываться на события и анонсировать их.
- BitTorrentTracker — трекер BitTorrent с использованием MongoDB.
- DebugConsole — сервер интерактивной отладочной консоли a-la telnet, который позволяет выполнять код в запущенном процессе.
Также в поставку включены инструменты для асинхронной работы с процессами и дескрипторами (asyncProcess, asyncStream).
Помимо этого в комплекте идут приложения-примеры.
Будем рады вашим модулям в дистрибутиве!
Баранов в стоило, холодильник в дом
Лицензия — LGPL. Проект относительно свежий, в чем-то сыроватый, но стабильный, и используется в production. Разработка ведется активно. В случае отсутствия форс-мажора, фиксы выходят от нескольких минут до двух суток после репорта.
Web:
http://phpdaemon.googlecode.com/
Группа:
http://groups.google.com/group/phpdaemon
IRC: irc.freenode.org #phpdaemon
E-Mail мне лично — kak.serpom.po.yaitsam@gmail.com
Спасибо за внимание!
P.S. Если вам подобная архитектура импонирует — возможна поддержка, даже в принципе написание модулей на заказ.
комментарии (94)
Брызги шампанского, визги поетесс, хлопушки и фейрверки.
Ура, товарищи!
и я бы на вашем месте для создания обьектов пользовался бы fabric method — ибо public function onHandshake($client) {return $this->sessions[$client->connId] = new ChatSession($client,$this);}
это не дело как мне кажется.
* Подключение прошло успешно.
* Упс! Произошло отключение от сервера!
* Пытаюсь переподключиться…
* Идёт подключение…
Перл комментатора выше о том, что PHP быстрее Java только лишь подтверждает это.
Тоже не хочу холивара, но тот факт, что сборка мусора появилась в пхп совсем недавно, ставит под сомнение попытки написания демонов на этом языке. Имхо, слишком велик риск нарваться на неожиданные проблемы. В конце-концов, у каждого языка и технологии есть свои задачи.
Те же руби и пайтон мне понравились как языки, но вот как средство зарабатывать, не владея языком в совершенстве… Just for fun :( Слишком велика ступень между «hello word» (утрирую) и редкими вакансиями Junior Python/Ruby Developer. Да и «пропихнуть» свое предложение, когда язык не задан, куда проще, если можно добавить, что скрипты будут работать на подавляющем большинстве хостингов, включая бесплатные, а не «радовать» заказчика тем, что для сайта-визитки какого-нить ООО «Рога и копыта» ему понадобится выделенный сервер.
И почему PHP? Особенно через FastCGI. который он так и не поддерживает нормально.
Может легче всеже через RabbitMQ серевер гонять сообщения, все же именно для этого Эрланг и был создан? (например)
public function onReady() { $this->errlogfile = dirname(__FILE__).'/cgi-error.log'; if (!isset(Daemon::$settings[$k = 'modcgiallowoverridebinpath'])) {Daemon::$settings[$k] = TRUE;} if (!isset(Daemon::$settings[$k = 'modcgiallowoverridecwd'])) {Daemon::$settings[$k] = TRUE;} if (!isset(Daemon::$settings[$k = 'modcgiallowoverridechroot'])) {Daemon::$settings[$k] = TRUE;} if (!isset(Daemon::$settings[$k = 'modcgiallowoverrideuser'])) {Daemon::$settings[$k] = TRUE;} if (!isset(Daemon::$settings[$k = 'modcgiallowoverridegroup'])) {Daemon::$settings[$k] = TRUE;} if (!isset(Daemon::$settings[$k = 'modcgioutputerrors'])) {Daemon::$settings[$k] = TRUE;} }Почему бы не сделать массив со значениями по умолчанию и не прибавить его к имеющемуся массиву? Это не единственный пример, там полно такого же.
'modcgiallowoverridebinpath'=> true,
'modcgiallowoverridecwd'=> true,
'modcgiallowoverridechroot'=> true,
'modcgiallowoverrideuser'=> true,
'modcgiallowoverridegroup'=> true,
'modcgioutputerrors'=> true,
), Daemon::$settings);
(а табуляция по вкусу)
и так каждый раз. мучаюсь.
Во-вторых, мой вариант быстрее и кушает меньше памяти. При определенных условиях мой код вообще потребует записи, только проверит по порядку наличие всех переменных. Когда как Ваш в любом случае построит лишние два хеша, прогоняя проверки.
Стиль, на мой взгляд — воплощение логики. Не понимаете код — разбирайтесь, учитесь, это пойдет на пользу. Поверьте, у нас с Вами не только одна планета, но и одна страна и один город. По поводу кода — советую посмотреть вот этот кусочек кода (начиная с 1005 строки, в самом низу). Когда разберетесь и научитесь подобное писать, думаю, к Вам придет просветление и Вы сможете писать правильный код столь же легко как разговаривать. Уверяю, я этот код написал довольно быстро и читаю его как русский текст или код на любом другом языке который знаю, также легко и редактирую.
и мой дорогой друг, я никоим образом не оспариваю тот факт, что вы знаете регэкпы (да и, вероятно, не только их) гораздо лучше меня.
я никоим образом не говорю, что код плохой. просто он непонятен. это не плохо и не хорошо. просто вы так пишете, а я этому удивляюсь.
и я совершенно согласен, что стиль — воплощение логики. я понимаю ваш код (кроме квики, да), но сквозь него нужно продираться, а читать легко и быстро не получается. возможно, дело в том, что я — посредственный программист, но ведь на это вам показал не только я, но и другие люди в этом топике. у нас у всех не получается это просто читать.
вы написали этот топик и решили поделилиться с нами, посредственными программистами, своими лучезарными регэкспами. наша ли вина в том, что регэкспы непонятны, а нативная php-реализация mysql-протокола вызывает недоумение?
и вот за то, что вы решили этим всем с нами поделиться, мы за это очень благодарны (ну, во всяком случае я. тут я без без всякого ёрничанья говорю уже)
Позвольте поинтересоваться, а почему достаточная понятная реализация протокола на PHP вызывает недоумение, а код mysqlnd к примеру не вызывает? И его просто используют. Он на порядок сложнее для понимания, можете убедиться. Вот я и предлагаю просто использовать мой код, не обязательно разбираться как он написан. Ничтожное малый процент программистов лезут в исходники реализации языка на котором пишут.
не обижайтесь, но такое ощущение, как будто вы много лет программировали на фортране для какой-нибудь ес эвм, потом столько же лет программировали микроконтроллеры, а потом встали не с той ноги и решили перейти на php.
ваш код очень отличается от кода всех php-проектов, которые я видел до сих пор. а на сишный код он очень-очень похож, только доллары везде расставлены. не то, чтоб это являлось проблемой само по себе, но, если вы вдруг серьёзно зададитесь целью популяризировать ваш фреймворк, то стиль кода может стать проблемой, на мой взгляд.
Думаю, язык не столь важен. Мне честно говоря всё равно на чем программировать, на PCRE многокилобайтные выражение выписывать, на PHP писать, на Java 2SE, микроконтроллеры или загрузчик ОС, или на ANSI C что-нибудь. Это всего лишь элементарные машинные языки со своими правилами, которые помогают заставить компьютер делать то что от него требуется, в рамках приемлемых ресурсов.
Чтоб понять откуда что вызвано — в PHP рулит debug_backtrace() ;-) Хотя например я и так с ходу читаю любой код на языке который знаю.
А в заключение я вам открою, на мой взгляд, справедливую мудрость. Код и есть логика, надо прослеживать логику кода, а не программиста который его писал. Код это и есть объективное выражение этой логики, остальное — фантазия. Автора кода может уже и не быть на свете, и логики никакой нигде нет, кроме той что в коде который Вы читаете. Это же не стихи, и не живопись, чтобы там сокральный смысл ловить. Это алгоритм действий, который преобразуется в весьма ограниченный набор процессорных инструкций.
Как Вы считаете, написание доки решит проблему популяризации?
создать массив из констант
получить массив из Daemon::$settings
смерджить эти два массива
положить результат в Daemon::$settings
а у вас:
положить в k строку
получить массив Daemon::$settings
проверить существование и ненулль элемента с индексом $k в массиве
если элемента нет, то добавить в этот массив элемент с таким индексом и значением true
положить в k другую строку
получить массив Daemon::$settings
проверить существование и ненулль элемента с индексом $k в массиве
если элемента нет, то добавить в этот массив элемент с таким индексом и значением true
и так много раз
samoval ~ # php file.php
5.8901879787445
5.1516609191895
W:\>php file.php
5.4503819942474
4.234375
(проверил два раза. на разных компьютерах)
samoval.ru/file.php.html
ВОТ ВИДИТЕ, МОЙ ВАРИАНТ БЫСТРЕЕ!!!
<? class Daemon { public static $settings = array(); public static function reset() { Daemon::$settings = array( 'modcgiallowoverridecwd'=> false, 'modcgiallowoverridegroup'=> false, ); } public static function asd() { if (!isset(Daemon::$settings[$k = 'modcgiallowoverridebinpath'])) {Daemon::$settings[$k] = TRUE;} if (!isset(Daemon::$settings[$k = 'modcgiallowoverridecwd'])) {Daemon::$settings[$k] = TRUE;} if (!isset(Daemon::$settings[$k = 'modcgiallowoverridechroot'])) {Daemon::$settings[$k] = TRUE;} if (!isset(Daemon::$settings[$k = 'modcgiallowoverrideuser'])) {Daemon::$settings[$k] = TRUE;} if (!isset(Daemon::$settings[$k = 'modcgiallowoverridegroup'])) {Daemon::$settings[$k] = TRUE;} if (!isset(Daemon::$settings[$k = 'modcgioutputerrors'])) {Daemon::$settings[$k] = TRUE;} } public static function qwe() { Daemon::$settings = array_merge(array( 'modcgiallowoverridebinpath'=> true, 'modcgiallowoverridecwd'=> true, 'modcgiallowoverridechroot'=> true, 'modcgiallowoverrideuser'=> true, 'modcgiallowoverridegroup'=> true, 'modcgioutputerrors'=> true, ), Daemon::$settings); } } $ts1 = microtime(true); for ($i=0;$i<100000;$i++) { Daemon::reset(); Daemon::asd(); } $ts2 = microtime(true); echo ($ts2-$ts1)."\n"; $ts1 = microtime(true); for ($i=0;$i<100000;$i++) { Daemon::reset(); Daemon::qwe(); } $ts2 = microtime(true); echo ($ts2-$ts1)."\n"; ?>Скорость варианта TravisBickle:
0.71443605423
Скорость моего:
0.367926836014
1.1654109954834
я запускал её 5 раз подряд и каждый раз разница была во втором знаке. т.е. в пределах полутора процентов.
тем не менее, я проверил свой тест, поменяв местами вызовы. ничего не изменилось.
Не понял реплику про isset.
$keys = array('modcgiallowoverridebinpath', 'modcgiallowoverridecwd' ,'modcgiallowoverridechroot', 'modcgiallowoverrideuser', 'modcgiallowoverridegroup', 'modcgioutputerrors');
foreach($keys as $v)
if (!isset(Daemon::$settings[$v])) { Daemon::$settings[$v] = TRUE; }
$keys = array('modcgiallowoverridebinpath', 'modcgiallowoverridecwd' ,'modcgiallowoverridechroot', 'modcgiallowoverrideuser', 'modcgiallowoverridegroup', 'modcgioutputerrors');
foreach($keys as $v)
if (!isset(Daemon::$settings[$v])) { Daemon::$settings[$v] = TRUE; }
а что, слабо было через массив параметров сделать?
к сожалению, применения в своих проектах пока не нашел…
Хотя доклад, думаю, провальный получился. Опыта нет в докладах…
доклад был не плохой, я его смотрел в онлайне, только ты очень уж разволновался и это было видно.
Вот что значит грамотный пиар. Гугл первыми сделали реализацию вебсокетов, и теперь все думают что гугл придумал вебсокеты.
Но этот код…
Хотя, это ещё вопрос, что лучше: суровый код, вокруг которого человек не поленился написать статью, либо «правильный» код, про который думаешь «ну вот, осталось ещё чуть-чуть подпилить, тесты дописать, чтобы не стыдно было, и можно будет писать статью на Хабр»… и так уже пару лет :)
> функционал… с другой стороны, вот этот код вгоняет меня в ступор. xor eax,eax какой-то.
+1
Думаю, что еще наберется +100 от других людей — я слышал как минимум от троих такое же мнение.
ИМХО — что автору (который — для меня совершенно очевидно — весьма талантливый программист) стоит перестать упираться рогом с его неповторимым стилем кодирования и перейти-таки на PEAR/ZF Coding Standards. На это уйдет примерно неделя (у меня в начале 2000-х ушло в сумме столько), после чего карма автора резко возрастет (а заодно и email сменить бы еще :).
Исходники PEAR того хуже, одних собак в коде хватит на целый питомник.
У меня убеждение что большинство людей — ламеры, и не важно программисты они, режиссеры, или мясники — ламером может быть каждый. Хороших специалистов очень мало. И в то же время большинство людей очень не любят шевелить мозгами, им будто бы больно думать. Они хотят поделать что-нибудь привычное в своем привычном ламерском стиле, и пойти в пятницу напиться алкоголем. У меня другой менталитет, и я не хочу добиваться всеобщего признания и популяризации плодов моей работы путем собственного оглупления и дибилизацией собственного кода. Есть много людей которые с радостью используют мои open source продукты годами и полностью довольно, иногда пишут багрепорты, всегда быстро правлю.
Документацию, комменты и больше примеров — напишу, а код дибилизовать не стану. Наоборот кто хочет пусть смотрит код и комменты и учится на этом примере.
Никого обидеть не хочу, у нас у всех ДНК очень похожее.
dklab.ru/lib/JsHttpRequest/demo/lib/JsHttpRequest/JsHttpRequest.js — это файл после DOJO-минимизации (если Вы его смотрели).
А chat.php.spb.ru — это не мой, а Дмитрия Бородина, Вы перепутали. :-)
В общем, мы Вам тут сигнализируем о проблеме и от чистого сердца советуем придерживаться задокументированных стандартов кодирования, которыми пользуются тысячи человек. А Вы уж сами решайте. Поставьте «плюс» все, кто думает так же. :-)
Хорошо читаемый, легко понятный код. Ну а уж что именно он использует или как пишет это же его дело, верно? Да, мне неприятно читать в данном случае ифы, но ведь это моя проблема, согласитесь?
P.S. Меня не покидает ощущение, что отторжение кода имярека у сообщества произошло из-за его подчёркнутой самоуверенности. Да, меня тоже это бесит, но при чём тут код?
Правильный у Вас взгляд. Но мир не переделаешь, большинство хочет либо просто либо никак. А почему уверенные в себе люди Вас бесят? Это же хорошо. Если человек действительно знает всё дело, он уверен в себе и в своих силах… Я ведь никого этим не хочу задеть, это личное дело каждого как он относится к себе лично =)
Меня бесит то, что Вы подчёркнуто самоуверенны. То, как Вы иногда строите фразы, вызывает у меня ощущение самолюбования. Вот именно оно и бесит.
Ваша уверенность в своих силах и знаниях, на мой взгляд, объективна, и она даже близко не вызывает отторжения :)
Главное иногда не зарываться и уметь перешагивать через свои ошибки :)
Спасибо за совет, Дмитрий, я постараюсь, правда. Извините еще раз за ошибку.
про чистоту кода ему раз сто
говорили, когда он тусовался на PHPClub…
но, что ни говорите, а голова у него на плечах…
молодец, осваиваешь NoSQL
и комментарии мне твои понравились…
желаю успехов!