Веб-сокеты на PHP это нормально: анонимный чат

Чуть более недели назад мне удалось разработать неплохое экономичное решение webockets демоны на PHP хостинге которое может работать на хостинге за 2 доллара в месяц. Оно не требует доступа консоли, оно не требует библиотеки работы с сигналами и форками. И даже удалось сделать панель управления, позволяющую управлять демоном из браузера. И это большой успех, т.к. до этого я считал что вменяемая работа с сокетами невозможна на дешевом хостинге. Еще несколькими неделями раньше я писал о том, что такое веб-сокеты и с чего начать при работе с этой технологией. Настало время для написания полноценных простых приложений. Сегодня я разработаю простейший чат, как это обычно делают все кто начинает работать с этой технологией.

Т.к. в моём распоряжении обычный PC под управлением ОС Windows и для разработки чата на ws, мне всё же понадобится система управления процессом под windows (под *nix то же самое только правильнее называть демоном). Возьму за основу описанную в прошлой статье панель управления (Downloads, ws server admin panel pre-alpha) и модифицирую её для работы под ОС Windows с установленным Денвером.

Вначале приведу результат работы.

Анонимный ролевой флейм-чат

Правило: позовите друзей и поиграйте в ролевую игру, пытаясь угадать кто есть кто.
Используйте команды /me, /to «Имя».
Пользователей он-лайн: Только вы.

Сообщение:

Далее о том, как это было разработано.

Фоновый процесс из PHP в ОС Windows

Решение оказалось не однозначным — изначально везде где нужно была добавлена проверка версии ОС на которой запущен PHP скрипт, и в зависимости от ОС следовало выполнение команды запуска ws сервера (демона).

if (strtoupper(substr(PHP_OS,0,3)) === 'WIN') {  //Действия под виндой
exec("w:\usr\local\php5\php.exe -q w:\home\localhost\www\ws\ws.php");
} else exec("php -q ws.php &");

Всё заработало. Обратите внимание, что пришлось переименовать проект и все файлы, т.к. сейчас разрабатывается проект не echo сервера, а более общий случай — ws сервер. Ради интереса проверил перечень программ которые появляются в диспетчере задач Windows. Оказалось целых три программы:

process

И это логично: консоль для исполнения команд, php интерпретатор, и окно консоли узла (правда, не совсем понимаю что это).

В процессе я опасался того, что под ОС Windows всё будет сложнее и запасся готовыми решениями и советами с php.net. Спешил уже написать о том, какое простое решение оказалось и что эти советы не понадобились. Однако, по непонятным причинам через несколько дней предложенное выше решение начало зависать при выполнении команды exec в PHP скрипте запуска.
И одним из советов с php.net воспользоваться всё таки пришлось.

This will execute $cmd in the background (no cmd window) without PHP waiting for it to finish, on both Windows and Unix.

function execInBackground($cmd) {
if (substr(php_uname(), 0, 7) == "Windows"){
pclose(popen("start /B ". $cmd, "r"));
}
else {
exec($cmd . " > /dev/null &");
}
}

В результате код запуска выглядит следующим образом.

if (strtoupper(substr(PHP_OS,0,3)) === 'WIN') {  //Действия под виндой
pclose(popen("start /B w:\usr\local\php5\php.exe -q w:\home\localhost\www\ws\ws.php", "r"));
} else exec("php -q ws.php &");

Примечательно то, что в диспетчере задач теперь появляется только одна дополнительная строка — php.exe.

Второй совет с сайта php.net оказался невостребованным, но обязательно его приведу на случай, если у кого-то не будет запускаться по причине нехватки прав.

If you try to use the psexec from Sysinternals on your Windows Server for background-processes that need special user-rights and get an «Access denied» oder «Wrong user or password» notice, although your username and password is right, this could help you getting around this bug.

$command = "c:\directory1\psexec.exe \\127.0.0.1 -u username -p password c:\directory2\commandtoexecute.exe";
exec($command);

Хотя, уверен что в 90% случаев права администратора не понадобятся.

ws сервер + панель управления улучшенная

Всё же знал, что придётся столкнуться с тем чтобы разработать полноценную систему управления ws сервером, хотя и не хотел. Пользуйтесь.

Features

Бонусом идёт ws echo клиент и ws чат клиент, которые шлют AJAX запросы включающие ws сервер, перед попыткой каждого соединения по протоколу ws. Не забывайте править конфиг: данные авторизации, порты и команду запуска ws в ОС Windows, которая требует указания точного пути к файлу ws.php.

В ОС Windows медленно работает запуск и завершение работы (до 7 секунд), так что наберитесь терпения. Я проверял работу в ОС Windows 7 под Денвером.

Конечно, разработанное ПО под ОС Windows нельзя назвать удобным, ведь если что-то упало или Windows была перезагружена, то для последующего запуска нужно удалить pid-файл в ручную, т.к. панель управления работая под Windows не проверяет наличие процесса в памяти а проверяет только pid-файл и если он есть Windows будет думать что процесс запущен.
Но при этом разработанное ПО может оказаться незаменимым для разработки ws приложения. Например, вы разрабатываете программу, изменяете код класса websocketserver и для того чтобы изменения вступили в силу, вам нужно погасить демона и запустить его заново, в этом вам и поможет данное ПО, которое позволит это сделать в два клика.

ws приложение чат

Теперь, когда все инструменты готовы, можно заняться и творческими вещами, например, написанием простенького чата на ws. Код сервера пришлось значительно переписать и вы это увидите скачав архив ws сервера предложенный несколькими абзацами выше. Причиной для такой переработки послужила не реализация логики чата, которая очень проста, а приведение кода в порядок и необходимость реализации устойчивой работы под ОС Windows.

Фактически, ws сервер чата такой же echo сервер, за исключением того, что он отправляет сообщение всем подключенным ws клиентам. А как мы знаем — у нас есть метод вызываемый при получении сообщения от пользователя в который и нужно добавить код рассылки сообщений всем пользователям.

protected function onMessage($connect, $data) {	
$uid = array_search($connect, $this->connects); //по переменной $connect определяем user id отправителя

$f = $this->decode($data); //расшифровываем сообщение

if($f['payload'] == "" || $f['payload'] == " ") return; //Если сообщение пустое, ничего не делаем

$msgtosend = $this->users[$uid]['id']."(".$this->users[$uid]['ip'].":".$this->users[$uid]['port']."): ".htmlspecialchars($f['payload']); //Иначе формируем сообщение для рассылки всем пользователям

foreach($this->connects as $connect) //обрабатываем все соединения и рассылаем сообщение
fwrite($connect, $this->encode($msgtosend));
}

Массив $this->users наполняется данными о пользователях, которые поступают при установлении соединения и рукопожатия. Реализация чата действительно очень проста, но её нельзя назвать промышленной т.к. для доведения её до такого статуса требуется реализация полноценной системы защиты от лишних соединений и решение других вопросов безопасности.

Борьба с утечками памяти

Т.к. демон может быть долгоиграющим а проект находится в стадии разработки и активно дописывается, то, к сожалению, от утечек памяти никуда не убежать. Очевидно, что бороться с утечками можно и нужно логируя все действия демона и фиксируя затраченную память. Дополнительно для решения проблем утечек памяти я хочу предложить некоторый неплохой инструмент сборки мусора, который может быть весьма полезен (работает в PHP 5.3+).

// Somewhere before the endless loop

$last_gc_cycle = time() - (24 * 3600);
// Some more code
while (true) {
// The main code here

if (function_exists('gc_collect_cycles')) {
$time = time();
if ($time - $last_gc_cycle > 300) {
$last_gc_cycle = $time;
gc_collect_cycles();
}
}
}

upd 2014.08.04: следующая статья, обновленная панель и немного об опыте разработки и отладки.

Специально для тех, кто ищет хостинг для запуска своего проекта на веб-сокетах обсуждение по ссылке.

upd 2016.09.15: Долго ломал голову над проблемой — почему не работает exec на *nix после перезагрузки сервера пока не залогинюсь по ssh. Ответ прост: php нужно запускать из-под root пользователя.

Мои статьи про PHP демонов и веб-сокеты