Веб-сокет сервер на PHP, запуск демона на PHP

В предыдущей статье о WebSocket-ах для самых начинающих я рассказывал больше о том, как настроить Денвер и убедиться в том, что всё работает, лишь чуть-чуть уделив время реализации простого ws echo сервера. Но если вы скачали архивы исходников, то, уверен, вы с лёгкостью разобрались в получении и отправке сообщений, чего вполне достаточно для использования технологии.
Рад тому, что в моём пока не раскрученном блоге пошла реакция и это побудило к тому, чтобы написать продолжение и раскрыть вопрос о том, как всё же заставить PHP скрипт ws сервера работать на хостинге, как следить за тем, что процесс запущен и PHP скрипт выполняется и не был закрыт по таймауту или при перезагрузке Apache, как избежать запуска дублирующего процесса PHP и ответы на другие необходимые вопросы, чтобы получить гарантированно работающий ws сервер. Обязательно создам Интернет ws echo сервер с функцией чата и размещу его на своём посредственном хостинге. Далее в этой статье PHP скрипт, который непрерывно выполняется на сервере и обеспечивает работу с ws я буду называть ws сервер, хотя фактически это процесс на стороне сервера. И самое важное, дам готовое решение даже для тех у кого нет SSH доступа к консоли хостингового сервера или виртуальной машины.

Сегодня будет много работы

Начнем с простого.

Размещение и запуск веб-сокет сервера на хостинге

Простота состоит в том, что именно с запуском никаких проблем нет. Вы можете взять PHP скрипт echows.php из прошлой статьи, совершенно ничего в нём не меняя, закачать на хостинг и обратиться к файлу из браузера, единственным отличием от выполнения в Денвере может быть то, что настройки Apache и кэширующих механизмов хостинга запрещают выводить информацию незавершенного PHP скрипта, в таком случае вы не увидите никакой информации в окне браузера, а страница продолжит грузиться в браузере. Но если попробуете подключиться к ws серверу из нашего ws клиента, то вы увидите что подключение прошло успешно и на ws echo сервер отвечает на все передаваемые запросы. В качестве адреса ws сервера в ws клиенте, естественно, нужно указать ws://yourdomain.com:8889. Как видите, всё в порядке. Единственная проблема с которой вы можете столкнуться это настройки файрволлов хостинга и занятость портов другими сервисами. Если у вас несколько доменов на одном IP адресе, то вы можете обращаться к ws серверу по адресу этого домена ws://anotheromain.com:8889 что, согласитесь не очень хорошо, особенно для хостинга где сотни веб-сайтов висят на одном домене, теоретически кто-то может использовать те же порты что и вы и это может привести к неработоспособности ws сервера, поэтому еще раз настоятельно рекомендую аккуратно выбирать порты и следить за тем, чтобы по прекращению работы ws сервер всегда закрывал все соединения и корректно закрывал сокеты. Теперь что касается самого PHP скрипта, как вы могли заметить, наш PHP скрипт живет только 100 секунд, после чего при попытке подключения или отправке сообщения, он закрывает все соединения и завершает своё выполнение. Как сделать так, чтобы он жил “вечно”? Есть достаточное количество методов. Но проблема не в том, что PHP скрипт должен выполняться бесконечно, а проблема в том, как обеспечить корректное завершение работы скрипта в различных ситуациях и затем коректно возобновлять его работу. Например, при выключении сервера и последующем его включении PHP скрипт сам не запустится, а отслеживать работает ли ws сервер и если не работает в ручную его запускать, очень плохая идея.

Метод бесконечного выполнения PHP скрипта запуск из браузера

Первое что нужно сделать, это в самом скрипте указать безлимитное время жизни PHP сценария set_time_limit(0); и игнорирование отключения браузера ignore_user_abort(true); чтобы PHP скрипт выполнялся после того, как вы закроете окно браузера. Когда вы произвели необходимые действия можете запускать его из браузера. Проблема в том, что запустив его однажды, мы не сможем проверить, всё ли в порядке с процессом, не возникло ли ошибок по ходу выполнения, а продолжает ли скрипт работу или нет можно убедиться только подключившись к нему ws клиентом. Конечно, можно добавить в этот скрипт функционал ведения лог-файла, в котором будут зафиксирована история его работы. Также при перезагрузке Apache на хостинге процесс со скриптом 100% будет выключен. Этот способ может подойти только в случае, когда мы закачиваем на сервер гарантированно работающий отлаженный ws сервер PHP скрипт и нам важно чтобы он работал только короткое обозримое время в зависимости от надежности хостинг провайдера, но этот метод совершенно не применим для работы полноценного промышленного ws сервера из-за своей крайней ненадёжности и невозможности отключения в тот момент когда нам это нужно. Представьте ситуацию, что администратор хостинг сервера решил перезагрузить Apache с целью обновления, а вы не проверяете постоянно работает ваш ws сервер или нет, а пользователи тем временем допустим сидят в вашем чате, и внезапно всё ложится, пользователи негодуют. Опять же, можно применить пару костылей, прежде чем ws клиент будет подключаться к ws серверу по протоколу ws, заставить ws клиент обращаться к другому серверному PHP скрипту по XMLHttpRequest(), и требовать от проверку запущенности ws сервера. Метод немного костылеватый, но имеет место в непромышленных решениях вроде небольшого чата или маленькой игрушки. Именно его я и использую в своих небольших проектах.

На всякий случай провел эксперимент, запустил ws сервер на хостинге предварительно внеся в PHP скрипт механизм закрытия всех соединений и прерывание процесса при получении сообщения “OFF” от клиента. Не трогал несколько дней, периодически отправляя разные сообщения и проверяя живучесть, отправил команду “OFF” спустя приблизительно два дня ws удачно завершил свою работу, время жизни процесса ws сервера оказалось 183 403 секунды (2 с небольшим суток) и думаю что он мог бы проработать еще дольше без всяких проблем.

Метод бесконечного выполнения PHP процесса запуск из консоли

В принципе, различие с запуском PHP скрипта из браузера практически отсутствует, за исключением того, что все данные о работе PHP скрипта выводятся в консоль и еще несколько небольших нюансов. Сам PHP скрипт также должен содержать set_time_limit(0); и ignore_user_abort(true);. Запуск PHP скрипта из *nix консоли осуществляется командой $ php -q scriptfile.php (для подключения к консоли я использовал Putty). Ключ -q (–no-header) обозначает что процесс должен быть запущен в тихом режиме и подавляет вывод заголовков HTTP которые обычно отправляются браузеру. $ man php позволит посмотреть другие интересующие ключи. Отличная документация на официальном сайте PHP которая помогла не только разобраться с запуском PHP скриптов из консоли, но и позволила внести существенные улучшения в сам PHP скрипт. Обратите внимание, что вы можете перенаправить вывод результатов выполнения скрипта в любой файл на сервере, используя символ ‘больше’ (‘>’), как обычно я и поступаю

$ php -q scriptfile.php > scriptfileoutput.txt 

PHP может быть использован для запуска PHP-скриптов в абсолютной независимости от Apache, но у меня нет уверенности что без Apache будет работать механизм сокетов, я не пробовал запускать без Apache — мне это показалось не к чему. Запуск через консоль считается более правильным нежели через веб-браузер, но он также как и запуск через браузер не способен решить ряд проблем. Возможно такой запуск и избавит нас от прекращения работы скрипта при перезапуске Apache и то это маловероятно, но что делать, если весь веб-сервер или виртуальная машина будет перезагружена. Вам придётся в ручную лезть на сервер и запускать скрипт, конечно если у вас большой игровой проект и есть выделенные системные администраторы которые мониторят состояние процессов на сервере и есть скрипты инициализации и загрузки ws сервера вместе с Apache и всем остальным, в таком случае это единственно правильный вариант, но мы говорим о бытовом удобном способе реализации ws сервера на PHP для небольших проектов. Также иногда встречается проблема при запуске PHP скрипта из консоли, которая прекращает выполнение PHP скрипта одновременно с тем когда вы выходите из консили, это связано с тем что выполнение PHP скрипта было привязано к вашей сессии как к клиенту. По идее это должно залечиваться использованием в PHP скрипте строки ignore_user_abort(true);, но это помогает не всегда в связи с разными настройками PHP. В таком случае используют трюк, давая PHP скрипту поток /dev/null, который он будет считать клиентом и не прекратит работу при вашем выходе из консоли.

$ php -q scriptfile.php < /dev/null > scriptfileoutput.txt &

Амперсанд в конце обязателен, чтобы вы могли нажать Ctrl+C и вернуться в консоль а процесс остался в памяти. Или можно воспользоваться утилитой nohup.

В добавок, будет полезно знать, что и на windows-платформе можно запускать выполнение скрипта из консоли

> w:\usr\local\php5\php.exe -q w:\home\localhost\www\echows.php

Если всё делать правильно, то лучше воспользоваться утилитой Supervisor: A Process Control System она следит за работой процесса, в случае необходимости запускает его, осуществляет регистрацию падений. Отличная правильная вещь, когда вы делаете серьезный проект и в вашем распоряжении выделенный сервер или хотя бы VDS.

ws сервер управление процессом PHP из браузера

В результате изучения способов запуска PHP скриптов у меня родилась очень простая идея — реализовать на стороне ws клиента перед подключением к ws серверу запрос XMLHttpRequest() к PHP скрипту который проверяет статус процесса ws сервера и если он не запущен запускает. Также возникла идея реализовать что-то вроде страницы администратора ws сервера, на которой будет доступен лог последних событий: о том когда был запущен, почему упал, кто инициировал последующий запуск и др., с которой можно будет дать команду ws серверу перезапуститься, выключиться, закрыть все соединения и др.

Какой должен быть интерфейс для ws клиента:

  • Проверка состояния ws сервера и запуск, если сервер недоступен

Какой должен быть интерфейс для ws администратора:

  • Информация о статусе ws сервера, желательно с количеством подключений и объёмом занимаемой памяти
  • Просмотр журналов
  • Остановка работы ws сервера
  • Перезагрузка ws сервера

Итак, для реализации нам нужен ws сервер, который необходимо повесить в памяти одним из описанных ранее способов, т.е. нужно сделать из него качественного демона. Отличная статья относительно процессов и демонов есть на хабре. Однако, к сожалению, найти хостинг с поддержкой команды создания дочерних процессов на PHP pcntl_fork еще труднее чем с поддержкой сокетов, поэтому придётся отказаться от классического способа демонизации. Также такие программы невозможно отладить на windows т.к. forkи существуют только в *nix операционных системах. Но всё же кое что полезное из статьи почерпнуть удалось, а именно создание PID-файла хранящего process id, который не позволит запускаться двум процессам одновременно — подробнее об этом чуть ниже.

В итоге я немного модифицировал код PHP скрипта ws echo сервера вместив в него код переключения потоков ввода/вывода STDIN, STDOUT, STDERR и, тем самым, упростил запуск ws сервера из консоли:

$ php -q /home/path/echows.php &

Получил хорошо работающий демон echows.php (архив с клиентом, версия без мониторинга) и без использования pcntl_fork. Он запускается через консоль, отвязывается от консоли и всё замечательно отвечает на все запросы пользователей по адресу ws://yourdomain.com:8889, и корректно закрывается при отправлении сообщения “OFF”. Но вот незадача, не удаётся проверить запущен ли демон или нет и тем самым избежать дублирующего запуска демона. Да, при запуске создаётся файл pid_file.pid, который хранит process id (уникальный номер процесса в системе OS *nix) нашего демона а при корректном завершении работы демона, например, при получении сообщения “OFF”, этот файл удаляется. При запуске можно конечно проверять наличие этого файла, и если файл есть, то сообщать о том что демон уже запущен и таким образом избегать дублирующего запуска, но что делать если демон завершил свою работу некорректно и не удалил файл pid_file.pid, в таком случае наш демон никогда больше не запустится. Опять же на хабре удалось найти отличную функцию проверки наличия демона.

function isDaemonActive($pidfile) {
if( is_file($pidfile) ) {
$pid = file_get_contents($pidfile);
//проверяем на наличие процесса, функция posix_kill может быть устаревшей возможно потребуется замена при переходе на PHP 5.3+
if(posix_kill($pid,0)) {
//демон уже запущен
return true;
} else {
//pid-файл есть, но процесса нет
consolemsg("there is no process with PID = ".$pid.", last termination was abnormal...");
consolemsg("try to unlink PID file...");
if(!unlink($pidfile)) {
consolemsg("ERROR");
//не могу уничтожить pid-файл. ошибка
exit(-1);
}
consolemsg("OK");
}
}
return false;
}

И опять проблема в том, что функция posix_kill($pid,0) оказалась не работоспособной по той же причине что и pcntl_fork. Я не смог с этим мириться и опять разработал “хитрое” решение. Т.к. я всё равно задумал реализовывать функционал показывающий состояние ws сервера, то мне так или иначе потребуется функция которая показывает статус процесса в ОС. Для реализации этой функции воспользуемся командой exec() которая позволяет выполнять любые команды консоли. И если мы выполним

exec("ps -aux -p ".$pid, $output); 

то в результате в массив $output, в случае если демон запущен и имеет $pid будет выдана информация о демоне (процессе).

USER PID %CPU %MEM VSZ RSS TT STAT STARTED TIME COMMAND
aow 62335 0.3 0.1 120080 16020 ?? SJ 4:31PM 0:00.02 php -q echows.php

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

Upd 2017.08.14: В примере выше используется BSD синтаксис команды ps. Для большинства *nix систем он будет отличным, а эта команда будет выводить все процессы вместо одного с идентификатором pid. Начиная с версии ws server admin panel v.0.4. и выше используется классический синтаксис, корректно работающий в большинстве *nix систем.

function isDaemonActive($pidfile) {
if( is_file($pidfile) ) {
$pid = file_get_contents($pidfile);
//получаем статус процесса
$status = getDaemonStatus($pid);
if($status['run']) {
//демон уже запущен
consolemsg("daemon already running info=".$status['info']);
return true;
} else {
//pid-файл есть, но процесса нет
consolemsg("there is no process with PID = ".$pid.", last termination was abnormal...");
consolemsg("try to unlink PID file...");
if(!unlink($pidfile)) {
consolemsg("ERROR");
//не могу уничтожить pid-файл. ошибка
exit(-1);
}
consolemsg("OK");
}
}
return false;
}

function getDaemonStatus($pid) {
$result = array ('run'=>false);
$output = null;
exec("ps -aux -p ".$pid, $output);

if(count($output)>1){//Если в результате выполнения больше одной строки то процесс есть! т.к. первая строка это заголовок, а вторая уже процесс
$result['run'] = true;
$result['info'] = $output[1];//строка с информацией о процессе
}
return $result;
}

Теперь когда функция проверки состояния готова, мы можем запускать демона не из консоли а выполняя команду PHP exec(“php -q echows.php &”); и выключать демона сообщением OFF.
Последнее что нужно изменить в ws echo сервере и ws клиенте добавить перед подключением AJAX запрос серверу, который бы его поднимал, в случае если он лежит. Теперь нам не нужно думать о том, в каком состоянии находится демон, т.к. знаем что даже если сервер перезагружался, клиент при первом же обращении его поднимет.

Браузерная панель управления ws сервером

Разработаю простую систему управления и мониторинга демона. Она очень проста и состоит из нескольких файлов echowsadmin.html (панель администратора), echowsadmin.js (логика панели администратора), echowsadmin.php (логика управления ws echo сервером). Разработать эту систему оказалось на удивление просто, я потратил не более 1го часа своего времени.

Для того, чтобы любой пользователь не мог выключить демона командой OFF, я убрал этот функционал из PHP кода ws echo сервера. Соответственно, реализовав функцию выключения из системы управления демоном. Реализация не самая изящная, вместо сигналов я использую файл off_file.pid, но зато гарантированно не требуется никаких дополнительных библиотек и выключение происходит корректно. Т.к. на ws сервере цикл while повисает в моментах слушания сообщений сокета, то после создания off_file.pid нужно соединиться с ws сервером чтобы он дошел до конца цикла и проверил наличие off_file.pid, для этого делаю небольшую хитрость, имитирую подключение по сокету из echowsadmin.php и ввожу некоторую задержку чтобы всё сработало и скрипт сообщил о благополучном завершении работы. Скачайте архив разработанной системы управления, ws сервера и клиента (устаревший архив, более новый в Downloads), не забудьте указать адрес расположения файла echowsadmin.php на вашем хостинге в echowsadmin.js в строке 10, адрес echowsstart.php в socket.js и адрес ws сервера в echowsadmin.php (вернее не адрес, а порт, т.к. файл должен находиться на сервере то адрес всегда будет 127.0.0.1), куда будет пытаться подключиться наш одноклеточный мини клиент при выключенном ws echo сервере. Кнопку перезапуска ws echo сервера я делать не стал, так как понятно, что для этого нужно нажать stop, а затем start и необходимость этого действия в одной кнопке практически отсутствует. Вся эта система управления работает только под управлением *nix операционных систем, т.е. на хостинге. А вот так она выглядит.

Панель управления демоном

Панель управления демоном

Очевидно, что можно много чего улучшить:

  • Улучшить представление и сделать более детальной информацию о статусе ws сервера
  • Добавить вывод в лог более подробной информации о занимаемой памяти и количества текущих соединений
  • Сделать проверку на операционную систему и разработать версию для Денвера и Windows
  • Сделать авторизацию

Но, в моём случае стояла задача сделать себе простенький инструмент мониторинга состояния демона через веб. Кстати, эту штуку слегка модифицировав можно использовать для мониторинга любого демона, а не только ws.

А что касается реализации для ОС Windows, то все места где pid можно обходить проверкой и таким образом обеспечиь запускаемость.

if (strtoupper(substr(PHP_OS,0,3)) === 'WIN') { 

}

А запущен демон или нет достаточно проверять просто наличием pid файла.

Пока я был на выходных, забыл выключить ws echo сервер, в итоге время его жизни составило 233774 секунд, т.е. где-то около 3х суток, занимаемая память так и осталась около 0.1%, что говорит о том, что решение имеет право на жизнь.

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

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