Главная > Блог > Веб-сокет сервер на PHPDaemon или как приручить демона

Веб-сокет сервер на PHPDaemon или как приручить демона

phpDaemon

При знакомстве с веб-сокетами, возникает первый вопрос, а на чём же сделать этот веб-сокет сервер. На сегодняшний день выбор разнообразен: это Socket.IO (написан на NodeJS), Tornado (написан на Phyton), phpDaemon (написан на PHP) и другие. Я же буду описывать фреймворк PHPDaemon, т.к. он написан на PHP, который я знаю лучше. Для тех кто решил написать свой сервер WebSocket на PHP могут многое взять с этого фреймворка.

Оглавление

О PHPDaemon

PHPDaemon (далее просто «phpd») - это асинхронный многопоточный фреймворк написанный на PHP, предназначен для обработки большого количества одновременных соединений. Помимо веб-сокета сервера, на нём можно реализовать свой HTTP, FastCGI, Socks4/5 и другие сервера. Для работы в асинхронном режиме реализованы клиенты MySQL, PostgreSQL, Redis, HTTP, Memcache и другие. После запуска «phpd» можно узнать о состоянии его работы через консоль, также он ведёт подробный лог своей работы. Сервер является многопоточным и количество потоков (далее «воркеры»), которые будут запускаться зависит от нагрузки, также со временем воркеры перезагружаются, чтобы не забивать оперативную память. Воркер тоже отключается по умному: если на нём «висят» несколько соединений допустим websocket-соединения, воркер отсылает этим соединениям фрейм «close» и соединения отключаются «чисто». Если соединений будет слишком много, они выстроятся в очередь в режиме ожидания и никто не останется без ответа.

Преимущества асинхронного сервера в том что обладая небольшим количеством ресурсом, можно одновременно обслуживать большое кол-во соединений. Если в классическом синхроном веб-приложении чтобы обслужить 100 запросов необходимо допустим: открыть соединение с СУБД, сделать запросы к БД, дождаться из выполнения, закрыть соединения с БД, выдать ответ, начать обслуживать следующий запрос, то в асинхронном приложении это выглядело бы так: сделать запрос к БД и при выполнении ответить, начать обслуживать следующющий запрос. Можно использую всего одно соединение к БД выполнить множество запросов, а в синхронном пришлось бы сделать 100 соединений и другие запросы ждали бы когда обслужили других. От этого такая высокая скорость выполнения.

Работая с «phpd» не нужно думать про низкоуровневые операции (допустим разбор протокола и фреймов WebSocket-а), а сразу начинать писать логику вашего приложения.

Недостатки

Сразу скажу что на первый взгляд «phpd» покажется вам «избыточным», «тяжёлым», интерфейс неудобным и разработка приложений сложным (именно такие мысли посещали меня при изучении). Это потому что «phpd» является асинхронным, многопоточным (реализовано через eio (http://php.net/eio)) сервером, и поэтому всё что работает для синхронного приложения, возможно не будет работать для асинхронного приложения. Код будет чем то немного напоминать JavaScript со своими «калбеками» . Например вот так:

/* Обычное последовательное выполнение функций в синхронном приложении */
func1("a");
func2("b");

/* Обычное последовательное выполнение функций в асинхронном приложении */
func1("a", function()
{
	func2("b");
});

Ещё один большой минус «phpd» - скудная документация, официальная докуменация (https://daemon.io/docs/ru/#intro) в большинстве своём устарела и обновляется не часто. Поэтому основные источники знания по «phpd» у вас будет поисковик, исходный код и файлы в папке «Examples». В папке «Examples», лежат действительно рабочие примеры использования какой либо функциональности.

Установка

Для работы необходим PHP версии 5.6. и выше (на PHP 7 тоже запускается, но нужно правильно собрать php7 и загрузить Pecl-расширения). Все операции выполняю под root-ом в Linux Debian 8.4.

aptitude install gcc make libcurl4-openssl-dev libevent-dev git libevent pkg-config
aptitude install php5-cli php5-dev php-pear

Установка PHP Pecl

pecl install event eio
echo "extension=event.so" > /etc/php5/mods-available/event.ini
echo "extension=eio.so" > /etc/php5/mods-available/eio.ini
ln -s /etc/php5/mods-available/event.ini /etc/php5/cli/conf.d/event.ini
ln -s /etc/php5/mods-available/eio.ini /etc/php5/cli/conf.d/eio.ini

Далее создадим пользователя example:example, под которым будет работать PHPDaemon и папку для него. В эту папку зальём PHPDaemon.

groupadd example
useradd example -g example -s /usr/sbin/nologin
mkdir /home/example/phpdaemon
cd /home/example/phpdaemon 
git clone https://github.com/kakserpom/phpdaemon.git .
chown -R example:example /home/example/phpdaemon

Удалим ненужные файлы

rm -rf .git .gitignore .scrutinizer.yml composer.json LICENSE README.md 

Компоненты: приложения, серверы, клиенты.

Чтобы правильно настроить конфигурационный файл, необходимо разобраться в таких понятиях как «Приложения», «Серверы», «Клиенты».

Приложение - это класс, в котором реализуется основная логика вашей программы. Класс приложения наследуется от класса «\PHPDaemon\Core\AppInstance», сам файл должен лежать в папке «PHPDaemon\Applications» (в конфигурации можно сменить эту папку), название файла должно совпадать с названием класса, регистр должен совпадать.

Сервер - это запускаемый процесс, который слушает соединения по сети. При поступления данных на порт «Сервера» парсит приходящие потоки байт, в необходимые объекты и передаёт приложениям. Так называемые точки входа для приложений. Список доступных серверов можно посмотреть в папке «PHPDaemon\Servers». В папке сервера есть класс «Pool» наследник от «\PHPDaemon\Network\Server», а методе «getConfigDefaults» можно узнать список всех доступных параметров для сервера.

Клиенты - классы, которые реализуют функции асинхроного взаимодействия по сети с другими серверами, допустим MySQL, PosgreSQL, Redis и другие. Если использовать стандартные функции для работы допустим с MySQL, процесс будет висеть пока не дождётся ответа по всем запросам, то приложение перестанет быть асинхронным. Чтобы реализовать полноценное асинхронное приложение необходимо использовать эти клиенты, т.к. процесс отправить все запросы и ответы вернёт только по мере их прибытия от сервера MySQL. У многих клиентов реализован только минимальный функционал (не разгуляешься), но этого должно хватить. Все клиенты лежат в папке «PHPDaemon\Clients». В папке клиента есть класс «Pool» наследник от класса «\PHPDaemon\Network\Client», в нём по методу «getConfigDefaults» можно узнать список всех параметров для клиента.

«phpd» работает примерно так: сначала «Серверы» перехватывают запрос, обрабатывает его и далее отправляют «Приложениям», а приложения в свою очередь могут использовать «Клиенты» для обработки запросов.

Настройка

Когда запускается «phpd» он просматривает файл «conf/phpd.conf», основной конфигурационный файл. (можно сменить при указании параметра --config-file во время запуска). Полный список параметров и комментарии к ним можно посмотреть в файле «PHPDaemon\Config\Object», где переменные класса «Object» означают наименование параметра. Допустим $this->maxworkers в конфиге можно будет написать как: «max-workers», «maxWorkers» или «Max-Workers», т.к. наименование параметров не учитывают регистр и тире, а используется лишь для удобочитаемости. Описания к параметрам тут. Создадим для примера файл настройки, в котором «phpd» будет у нас обслуживать WebSocket запросы на порту «3333», приложение будет называться «Notice» и использоваться СУБД MySQL.

# Пользователь
user example;
group example;

# Воркеры
max-workers 8;
min-workers 2;

# Сервер WebSocket
Pool:Servers\WebSocket
{
	listen 'tcp://0.0.0.0';
	port 3333;
}

# Клиент MySQL
Pool:Clients\MySQL
{
	server "tcp://example:password@127.0.0.1/example";
}

# Приложение
Notice
{
	enable 1;
}

Запуск и другие команды в консоли

После того как вы создали конфигурационный файл его нужно запустить. Запускать «phpd» нужно через консоль. Запускаемый файл находится тут «phpdaemon/bin/phpd», это обычный php-файл, в котором прописан интерпертатор PHP, т.е. обычный Unix скрипт. Запускать его можно двумя способами (находимся в папке «phpdaemon/bin»):

./phpd start

или если хотите запускать через свою версию PHP

/usr/local/bin/php phpd start

После каждого изменения файла приложения или других файлов необходимо перезапускать «phpd». Другие команды:

./phpd --help		# Справка по всем командам
./phpd start        # Старт
./phpd stop         # Остановка
./phpd restart      # Перезагрузка
./phpd status       # Показать статус, запушен или нет
./phpd fulllstatus  # Показать статус по воркерам, время работы и др.

Полный список команд тут.

Логирование

По умолчанию при запуске «phpd» скидывает отчёт в файл «/var/log/phpdaemon.log» (параметр log-storage в конфиге), но можно отчёт вывести напрямую в консоль, для этого нужно добавить к команде аргумент «--verbose-tty=1». Пример:

./phpd start --verbose-tty=1
./phpd restart --verbose-tty=1
./phpd stop --verbose-tty=1

Если запросов на «phpd» много, консоль будет загромождена сообщениями и выполнить команду в консоли не представляется возможным. Я предпочитаю запуск сервера во время отладки запускать так:

./phpd start
tail -f -n 100 /var/log/phpdaemon.log

Сообщения от «phpd» не очень информативны, обычный «PHP Notice» будет выглядить вот-так:

[Fri, 14 Jun 2016 20:01:29.382215 +0300] Notice: Trying to get property of non-object in /home/example/phpdaemon/PHPDaemon/Network/IOStream.php:545
#1  PHPDaemon\Core\Daemon::errorHandler() called at [/home/example/phpdaemon/PHPDaemon/Network/IOStream.php:545]
#2  PHPDaemon\Network\IOStream->getInputLength() called at [/home/example/phpdaemon/PHPDaemon/Servers/WebSocket/Protocols/V13.php:164]
#3  PHPDaemon\Servers\WebSocket\Protocols\V13->onRead() called at [/home/example/phpdaemon/PHPDaemon/Network/IOStream.php:735]
#4  PHPDaemon\Network\IOStream->onReadEv()
#5  EventBase->dispatch() called at [/home/example/phpdaemon/PHPDaemon/Thread/Worker.php:253]
#6  PHPDaemon\Thread\Worker->run() called at [/home/example/phpdaemon/PHPDaemon/Thread/Generic.php:127]
#7  PHPDaemon\Thread\Generic->__invoke() called at [/home/example/phpdaemon/bin/phpd:69]

поэтому, не ленитесь данные проверять на наличие функцией empty() или isset(), чтобы избежать захламление лога.

Лог бывает полезен, когда нужно во время работы вывести свои сообщения, для этого используйте метод «\PHPDaemon\Core\Daemon::log()» или функцию «D()». Функция «D()» выводит в лог «var_dump». Не используйте конструкцию «echo» или аналогичные ей функции (print, printf и т.д.), они ничего не выведут в консоль.

Со временем файл лога будет обрастать полезными и бесполезными сообщениями, поэтому необходимо иногда делать ротацию логов. Сам «phpd» не занимается ротацией лога, но в файле «phpdaemon/conf/logrotate» есть примерный конфигурационный файл для стандартной программы в Linux «logrotate».

Пример простого echo WebSocket сервера

Создадим приложение, которое будет принимать соединения на 3333 порт по веб-сокету от браузера и отправлять обратно то же сообщение браузеру (эхо). Предположим что вы уже установили «phpd» в папку «/home/example/phpdaemon». Правим конфигурационный файл «/home/example/phpdaemon/conf/phpd.conf»:

user			example;
group			example;
log-errors		1;
max-workers		8;
min-workers		2;
start-workers	1;
max-idle		0;

Pool:Servers\WebSocket
{
	listen 'tcp://0.0.0.0';
	port 3333;
}

WSEcho
{
	enable		1;
}

Далее создадим файл «/home/example/phpdaemon/PHPDaemon/Applications/WSEcho.php».

namespace PHPDaemon\Applications;

class WSEcho extends \PHPDaemon\Core\AppInstance
{
	public function onReady() 
	{
		$appInstance = $this;

		\PHPDaemon\Servers\WebSocket\Pool::getInstance()->addRoute("/", function ($client) use ($appInstance)
		{
			return new WsEchoRoute($client, $appInstance);
		});
	}
}

class WsEchoRoute extends \PHPDaemon\WebSocket\Route
{
	public function onHandshake()
	{
		\PHPDaemon\Core\Daemon::log("Соединение установлено.");
	}
	
	public function onFrame($data, $type)
	{
		if ($type === "STRING")
		{
			\PHPDaemon\Core\Daemon::log("Пришли данные «{$data}».");
			$this->client->sendFrame($data);
		}
	}
	
	public function onFinish() 
	{
		\PHPDaemon\Core\Daemon::log("Соединение разорвано.");
	}
}

Теперь запускаем phpd, так чтобы все сообщения phpd выкидывал в консоль.

cd /home/example/phpdaemon/bin
./phpd start --verbose-tty=1

Протестируем с помощью моей html-страницы или через свою программу. Должно всё заработать.

Источники

phpdaemon, вебсокет сервер, websocket server, php, javascript

Комментарии:

Роман
от
14.02.2017 - 14:14:23
А как же ratchet? (который socketo me). С нодой пришлось повозиться чтобы настроить, рэтчет тоже вроде работает, но вот как со стабильностью дела обстоят не понятно. Нода, js и в частности socket.io созданы для асинхронщины и работы с сокетами, рэтчет тоже не дураки писали, вроде... )
Комментировать

Добавить комментарий

x

Добавить