Веб-сокеты на 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 демонов и веб-сокеты

  • Ruslan

    Здравствуйте, мне очень понравились ваши статьи на тему WS. Вы могли бы подсказать, как следовало бы изменить чат, чтобы можно было записывать его сообщения в бд.
    Была идея произвести запись после того, как все пользователи отключатся от системы. Но как реализовать это, с этим проблемы, ведь следует где-то хранить отправленные сообщения до записи в базу, на сырой вариант можно записывать сообщение каждый раз при отправке, но по мне так это может загрузить сервер, особенно когда пишет много пользователей

    • petukhovsky

      Да вобще всё просто — это же совсем не промышленное решение — и ляжет
      оно гораздо раньше чем нагрузится БД или даже файл из-за множества более вероятных событий, например, из-за частого обращения к лог-файлу или из-за переполнения количества соединений да и мало ли из-за чего, я бы рекомендовал писать сообщения в БД или даже в файл сразу при получении т.е. в теле метода onMessage. Важно, хоть это и не промышленное решение — для чата вполне годится, т.к. если к чату будет подключаться/отключаться больше 2-3х человек в секунду и в него будет падать более 5и сообщений в секунду, что чат выдержит без проблем и в 10 раз больше, но проблема в том, что это уже будет не чат а титры на большой скорости на которые не то что отвечать будет неудобно, их просто не успеешь прочесть, так что не думайте о производительности и смело сохраняйте данные куда захочется в методе onMessage. Если честно, за прошедшее время код чата сильно изменил — улучшив загрузчик на стороне сервера и подсчёт пользователей он-лайн как в примере на данной странице. Если будет интерес к теме — можно будет продолжить за одно и опубликовать свежие исходники.

      • Ruslan

        Большое спасибо за ответ, пишите, было бы интересно узнать + думаю тоже появятся наработки, которые впоследствии скину вам в комментарии, а там уж смотрите сами

      • Shone_dz

        Интерес к теме есть ) Будет здорово если опубликуйте свежую версию.

        • Материалы уже готовятся — пишу про свой опыт разработки большого проекта и о том, как я ловил ошибки и пользовался ранее написанной штукой по управлению ws. Где-то через неделю думаю закончу.

  • Александр

    Здравсвуйте!

    Провел наличие сокетов на хостинге, все нормально. На локальной машине все работало (win7, apache), а на хостинге (nix) не работает. Файл вроде запускается, присылает run:1 и pid файл создается, а сам чат не хочет работать, пишет, что не может соединиться. Если жму отправить то выходит вот такая ошибка: InvalidStateError: An attempt was made to use an object that is not, or is no longer, usable

    Что я не так делаю?

    • Александр. Пришлите пожалуйста ссылку на ваш запущенный на хостинге ws-сервер, я попробую подключиться. А ошибка которая вам выдаётся, скорее всего связана что AJAX со страницы летит вникуда. Проверьте все конфиги, особенно адреса указанные в начале всех JS файлов.

  • pav-pas

    Добрый день!

    у вас в файле где то написано так:

    Не будем делать сложные проверки на устойчивость и долбать сервер, pid-файл есть, значит всё работает.

    а что еще можно сделать? что проверить не упал ли процесс, так как мне надо сто пудово чтоб работало!

    Мне нужно сделать диалоги типо ВК, наверно вебсокеты самое то? ну всмысле не аяксом? проверять новые сообщения для кучи народу!

    я тут ниже чуть распишу скажите верно я мыслю или нет!

    т.е. при подключении в сокету с браузера, отправляем например такое собщение: id:123
    т.е. идентификатор пользователя

    далее, ссылку его соединения — присваиваем в массив типа:
    $aConn[126] = $lConnect
    далее, когда любой пользователя отправляет сообщение, оно сначало пишется в БД, а потом рассылается на всех участников диалога!
    ну и для тех участников для которых нет соединения, отправляем еще уведомление на почту!

    Прост таким образом я думаю можно избежать лишниш запросов к БД, как в случае с аяксом — пришлось бы сначало записать в БД, а потом каждый клиентом опросить БД, на наличие сообщений!

    • Здравствуйте!

      >у вас в файле где то написано так:
      >Не будем делать сложные проверки на устойчивость и долбать сервер, pid-файл >есть, значит всё работает.а что еще можно сделать?

      Это первая статья из цикла веб-сокеты http://petukhovsky.com/tag/websocket/
      поэтому тут многие исходники сырые. Посмотрите все статьи, там будет чат с исходниками и много практических советов.

      Мыслите вы абсолютно правильно — технология самая что ни на есть подходящая для вашей задачи.Только AJAX не нужен, пишите в БД прямо из процесса веб-сокета.

      Крайняя статья содержит панель которая делает проверку на наличие процесса а не только pid-файла
      http://petukhovsky.com/simple-web-socket-on-php-developing-and-testing/

      Также при использовании работы с БД из веб-сокета рекомендую использовать функции продолжительного подключения к БД, чтобы добиться лучшего быстродействия и экономить память
      http://php.net/manual/ru/function.mysql-pconnect.php — это ссылка на семейство функция. Сама mysql_pconnect уже устаревает.

      Также отправлять id по веб-сокету не совсем правильно с точки зрения безопасности. Сегодня вкратце написал это в комментариях в ответ на вопрос Николая Приймака во второй статье http://petukhovsky.com/simple-web-socket-on-php-daemon/

  • Николай Приймак

    Подскажите как получить ид усера которому мы отправляем сообщение
    по примеру функции onMessage

    $msgtosend = $this->users[$uid][‘id’].»(«.$this->users[$uid][‘ip’].»:».$this->users[$uid][‘port’].»): «.htmlspecialchars($f[‘payload’]);
    здесь у нас ид того кто отправляет

    как сделать так чтобы все получали свой ид через foreach ниже

    foreach($this->connects as $connect) //обрабатываем все соединения
    fwrite($connect, $this->encode($msgtosend));//echo функция ответа

    уже 6ч код нем могу сделать и похоже у меня не хватает навыков(((((

    • Николай Приймак

      Сделал вот так но не знаю можно ли такой варят использовать при большом онлайне? или есть получше варианты?

      protected function onMessage($connect, $data) {
      $uid = array_search($connect, $this->connects);

      $f = $this->decode($data);

      if($f[‘payload’] == «» || $f[‘payload’] == » «) return;

      $msgtosend = htmlspecialchars($f[‘payload’]);

      $ooo=0;
      foreach($this->connects as $connect){ //обрабатываем все соединения
      fwrite($connect, $this->encode($msgtosend.$this->users[$ooo][‘id’]));//echo функция ответа
      $ooo++;
      }

      }

      • Как я говорил уже в одном из комментариев, данный чат скорее сломается на уровне удобства интерфейса, нежели от нагрузки. Так что за нагрузку можете не переживать, если у вас меньше 100 пользователей онлайн.
        Если вам это нужно для приватных сообщений, то я это уже реализовал в статье посвященной релизу анонимного чата http://petukhovsky.com/simple-web-socket-on-php-anonymous-chat/

  • Maxim

    Мне кажется или ваш сервер упал? Потому что ваша демонстрация что-то не работает!

    • Честно говоря, практически не слежу. Перенастраивал хосты и не дал скрипту нужных прав. Всё поднял, спасибо, можете смотреть.

      • Maxim

        Не работает, увы!

        • Все те же работы. Поднял. В течение недели еще пару раз может отключиться.

  • брат Марио

    Коллеги, подскажите, как запустить это чудо на wsphp сервере? (wsphp.net)
    Буду премного благодарен 🙂

    • Если только переписать. wsphp.net крутая штука, там уже практически всё есть.