При знакомстве с веб-сокетами, возникает первый вопрос, а на чём же сделать этот веб-сокет сервер. На сегодняшний день выбор разнообразен: это Socket.IO (написан на NodeJS), Tornado (написан на Phyton), phpDaemon (написан на PHP) и другие. Я же буду описывать фреймворк PHPDaemon, т.к. он написан на PHP, который я знаю лучше. Для тех кто решил написать свой сервер WebSocket на PHP могут многое взять с этого фреймворка.
Оглавление
- О PHPDaemon
- Недостатки
- Установка
- Компоненты: приложения, серверы, клиенты
- Настройка
- Запуск и другие команды в консоли
- Логирование
- Пример простого echo WebSocket сервера
О 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-страницы или через свою программу. Должно всё заработать.
Комментарии:
Добавить комментарий