Быстрый старт с WebSocket на основе phpDaemon

  • Tutorial
На хабре уже есть статья по этой теме. Но фреймворк с тех пор сильно обновился и, к сожалению, по старой статье разобраться скорее всего будет проблематично. Кроме того, в изучении чего-то нового всегда самое сложное — это начало. Поэтому по свежей памяти постараюсь описать процесс старта хотя бы в общих чертах.

Установка


В принципе, установка описана на официальном сайте.
Я предполагаю, что у вас уже установлен PHP с версией не младше 5.3. Поэтому описываю только остальное (на примере FreeBSD):
  1. Установить из портов необходимые библиотеки для PHP:

    Это можно сделать по отдельности:
    • php5-pcntl
      # cd /usr/ports/devel/php5-pcntl
      # make install clean
      
    • php5-shmop
      # cd /usr/ports/devel/php5-shmop
      # make install clean
      
    • php5-sockets
      # cd /usr/ports/net/php5-sockets
      # make install clean
      

    либо с помощью порта php5-extensions:
    # cd /usr/ports/lang/php5-extensions
    # make config
    

    Отмечаем галочки напротив PCNTL, SHMOP и SOCKETS и устанавливаем:
    # make install clean
    

    При втором подходе в конце можно получить ошибку, что порт php5-extensions уже установлен. Тем не менее, сами библиотеки будут установлены нормально.

  2. Устанавливаем pecl-eio:
    # cd /usr/ports/devel/pecl-eio
    # make install clean
    


  3. Устанавливаем pecl-event:
    # cd /usr/ports/devel/pecl-event
    # make install clean
    


  4. Если еще не установлен git, ставим (опции можно не менять):
    # cd /usr/ports/devel/git
    # make install clean
    


  5. Устанавливаем phpDaemon как сказано на сайте:
    # cd /usr/local
    # git clone git://github.com/kakserpom/phpdaemon.git
    # chmod +x phpdaemon/bin/phpd
    # ln -s /usr/local/phpdaemon/bin/phpd /usr/bin/phpd
    



В принципе, сама установка на этом завершена. Теперь стоит попробовать запустить первое приложение. Для этого возьмем пример из комплекта — ExampleWebSocket. Для этого необходимо прописать конфигурацию нашего WebSocket-сервера в файле /usr/local/phpdaemon/conf/phpd.conf:
user www;
group www;

max-workers 8;
min-workers 1;
start-workers 1;
max-idle 0;

Pool:Servers\WebSocket {
    enable 1;
    listen "tcp://0.0.0.0";
    listen-port 8047;
    privileged;
}

ExampleWebSocket {}



Теперь пробуем запустить наш сервер:
# phpd start


Все должно пройти нормально и в логах (/var/log/phpdaemon.log) мы должны увидеть примерно следущее:
M#7964 \PHPDaemon\Core\Pool:Servers\WebSocket up.
M#7964 \PHPDaemon\Core\Pool:\PHPDaemon\Servers\WebSocket\Pool up.
W#7966 \PHPDaemon\Examples\ExampleWebSocket up.
Spawning 1 worker(s)
W#7967 \PHPDaemon\Examples\ExampleWebSocket up.


Пишем первое приложение


Хотелось бы предложить в дополнение к идущему в комплекте еще один пример WebSocket-приложения. Думаю, лишний пример никогда не будет вреден разбирающемуся в новом человеку.
Сразу следует оговориться, что в данный момент времени официальная документация сильно устарела. Разработчик обещает исправить это в будущем. Тем не менее, всегда можно открыть интересующий класс фреймворка, найти нужный метод и разобраться как он работает. Чаще всего это не отнимает много времени: код интуитивно понятен.

Итак:
  1. Создаем в папке PHPDaemon/Applications новый файл с именем MyWebSocket.php со следующим кодом:
    <?php
    namespace PHPDaemon\Applications;
    
    class MyWebSocket extends \PHPDaemon\Core\AppInstance {
    
    	public $enableRPC=true; // Без этой строчки не будут работать широковещательные вызовы
    	public $sessions=array(); // Здесь будем хранить указатели на сессии подключившихся клиентов
    
    	// С этого метода начинается работа нашего приложения
    	public function onReady() {
    		$appInstance = $this;
    		
    		// Метод timerTask() будет вызываться каждые 5 секунд
    		$this->timerTask($appInstance);
    		
    		// Наше приложение будет доступно по адресу ws://site.com:8047/myws
    		\PHPDaemon\Servers\WebSocket\Pool::getInstance()->addRoute('myws', function ($client) use ($appInstance) {
    			$session=new MyWebSocketRoute($client, $appInstance); // Создаем сессию
    			$session->id=uniqid(); // Назначаем ей уникальный ID
    			$this->sessions[$session->id]=$session; //Сохраняем в массив
    			return $session;
    		});
    
    	}
    
    	function timerTask($appInstance) {
    		// Отправляем каждому клиенту свое сообщение
    		foreach($this->sessions as $id=>$session) {
    			$session->client->sendFrame('This is private message to client with ID '.$id, 'STRING');
    		}
    		
    		// После отправляем всем клиентам сообщение от каждого воркера (широковещательный вызов)
    		$appInstance->broadcastCall('sendBcMessage', array(\PHPDaemon\Core\Daemon::$process->getPid()));
    		
    		// Перезапускаем наш метод спустя 5 секунд
    		\PHPDaemon\Core\Timer::add(function($event) use ($appInstance) {
    			$this->timerTask($appInstance);
    			$event->finish();
    		}, 5e6); // Время задается в микросекундах
    	}
    	
    	// Функция для широковещательного вызова (при вызове срабатывает во всех воркерах)
    	public function sendBcMessage($pid) {
    		foreach($this->sessions as $id=>$session) {
    			$session->client->sendFrame('This is broadcast message from worker #'.$pid, 'STRING');
    		}
    	}
    
    }
    
    class MyWebSocketRoute extends \PHPDaemon\WebSocket\Route {
    
    	public $client;
    	public $appInstance;
    	public $id; // Здесь храним ID сессии
    
    	public function __construct($client,$appInstance) {
    		$this->client=$client;
    		$this->appInstance=$appInstance;
    	}
    	
    	// Этот метод срабатывает сообщении от клиента
    	public function onFrame($data, $type) {
    		// Отправляем ему ответ
    		$this->client->sendFrame('Server receive from client #'.$this->id.' message "'.$data.'"', 'STRING');
    	}
    	
    	// Этот метод срабатывает при закрытии соединения клиентом
    	public function onFinish() {
    		// Удаляем сессию из массива
    		unset($this->appInstance->sessions[$this->id]);
    	}
    
    }
    

  2. Правим наш конфигурационный файл до следующего вида:
    user www;
    group www;
    
    max-workers 8;
    min-workers 1;
    start-workers 1;
    max-idle 0;
    
    Pool:Servers\WebSocket {
        enable 1;
        listen "tcp://0.0.0.0";
        listen-port 8047;
        privileged;
    }
    
    MyWebSocket {}
    

  3. Создаем на компьютере HTML-файл со следующим содержимым:
    <script type="text/javascript">
    function add(text) {
    	document.forms[0].b.value=text+"\n"+document.forms[0].b.value;
    }
    
    if("WebSocket" in window) {
    	var timer;
    	var ws=new WebSocket("ws://site.com:8047/myws");
    	ws.onopen=function() {
    		add('Connection opened');
    		timer=window.setInterval(function() {
    			var date = new Date();
    			var message='ping at '+date.getSeconds();
    			ws.send(message);
    			add('Client sent message "'+message+'"');
    		}, 30000);
        };
    
        ws.onmessage=function(evt) {
    		add('Message from server: "'+evt.data+'"');
        };
        
        ws.onclose=function() {
    		add('Connection closed');
            window.clearTimeout(timer);
        };
    } else {
        alert("Your browser doesn't support WebSocket");
    }
    </script>
    <form>
    <textarea name="b" style="width:100%;height:100%"/></textarea>
    </form>
    

  4. Перезапускаем демона:
    # phpd restart
    

  5. Открываем наш HTML-файл в браузере (я проверял только в Opera 12.13 и Google Chrome 24.0.1312.57).


После этого JS-клиент начинает взаимодействовать с сервером и в браузере будут выводиться все их взаимодействия.

Примечание


У сервера имеется таймаут равный двум минутам (120 секунд). Соответственно, клиент должен периодически посылать серверу сообщения «пустышки», чтобы сервер не счел его неактивным и не отключил. И не забудьте заменить site.com на адрес вашего хоста.

P.S. Статья обновлена 19.02.2014 и адаптирована под текущую версию фреймворка (1.0-beta3).
Поделиться публикацией
Похожие публикации
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 34
  • 0
    А под нагрузкой тестировали?
    Сколько примерно одновременных клиентов может обслужить такой сервис?
    По наличию свободных портов макс. около 60к или все же меньше?
    • 0
      и еще немаловажно, исследовать зависимость между количеством подключенных клиентов и задержку в отправке сообщений между клиент-сервером.
      • 0
        Если будет возможность — обращу внимание и отпишусь.
        • 0
          Если что, можно заюзать PhantomJS для лоад теста.
      • 0
        К сожалению, пока нет возможности протестировать под серьезной нагрузкой. Но в сети вроде встречал упоминания, что phpDaemon дает весьма неплохие результаты.
        • 0
          теоретически — количество свободных портов * количество подключений к 1 порту (64К) если не ошибаюсь.
          • +1
            Кол-во свободных портов сервера не имеет значения, потому что все клиенты подключаются к одному порту (в данном случае 8047). Кол-во подключений к порту не ограничивается 64 тысячами.
          • 0
            Кто-нибудь в курсе, какими преимуществами обладает phpDaemon по сравнению с node.js?
            • 0
              Кстати, я тоже сейчас думаю что выбрать для работы WebSocket-ами. Или изучить новый для себя Node.js, или попытаться использовать хорошо знакомый PHP с phpDaemon.

              В phpDaemon немного пугает то, что PHP изначально не был приспособлен для работы как демон, да и поддержка у него не лучшая. В Node.js с информацией и поддержкой все отлично, но это для меня новая среда, с существенно иными подходами и стилем программирования.

              Думаю, если бы мне требовалось делать долгосрочный и большой проект, я бы склонился все же к Node.js. А если проект небольшой и не сложный, то я, как PHP-шник, я попробую phpDaemon.
              • +4
                Я 2 дня мучался с phpDaemon, перерыл всю информацию какую смог найти, понял что большинство на форумах давно устарело. С трудом сделал рабочий пример, который терял коннекшены когда ему заблагорассудится.Потом плюнул и за 3 часа прочел теорию о Node.js и написал ту же логику без проблем.

                Может я и ошибасюь, но если вебсокеты — то Node.js
                • –1
                  Я предполагаю, что если хорошо владеешь PHP, но пугает серьезное программирование на Node.js то можно сделать на Node.js лишь маленький скрипт, который ретранслирует вызовы в PHP и обратно. А всю сложную логику сделать уже на PHP.
                  • 0
                    Думал о таком, но как-то во всех сценариях пришедших в голову без демона (самописного или на базе phpdaemon) на PHP не обойтись. Разве что писать либу для node.js, работающую с пулом php-fpm. Ну или тупо вызывать в аналогах system или exec CLI скрипт на PHP.
                    • 0
                      Можно общаться и через message queue, например, rabiitmq или zmq. Такой подход, в частности, я видел в недавнем примере zerogw
                      • 0
                        Так всё равно же нужен будет демон на PHP, который будет вытаскивать сообщения из очереди и их обрабатывать. Или нет? Я из подобных систем только с Gearman разбирался.
                        • 0
                          Конечно, но, в данном случае, демон будет очень простой и не нужно заморачиваться с websockets.
                    • 0
                      У меня была уже часть написана на PHP. Переписывать ее на Node — нужно было выучить фреймворк Express. Посчитал лишним и настроил общение c PHP через Memcached (умные люди советуют Redis, но мне хватило и мемкеша)
                    • +1
                      Тоже как-то для одной локальной задачи познакомился с WebSockets и нормальных решений не смог найти для серверной стороны (все громоздкие были для моей задачи). В итоге простейшую реализацию на перле написал по документации за полчаса.
                  • –1
                    Хех, только решил Хабр глянуть перед тем как нечто подобное написать (пост о веб-сокетах на PHP и пример приложения, правда с взаимодействием «динамической» части с обычной «статической» через что-нибудь типа German) и вот, не успел…
                    • 0
                      А вы пишите, не стесняйтесь, я думаю будет интересно не только мне ;)
                    • +1
                      Народ, а никто не пробовал случаем Ratchet + Autobahn? Сейчас к нему присматриваюсь. Очень подкупает хорошая документация (по сравнению с тем же phpDaemon).
                      • –1
                        Кто использовал WebSockets, приведите примеры для чего?
                        • 0
                          Ещё не использую, но планирую для уведомления одних клиентов о событиях на сервере и других клиентах.
                          • 0
                            Если нужно уведомлять только клиентов в реальном времени, зачем двусторонняя связь?
                            • 0
                              SSE не рассматривали или что-то подобное, что передаёт данные в реальном времени только на клиент?
                              • 0
                                Не вижу особой разницы с WS. Всё равно нужен будет демон, поддерживающий постоянные соединения с другими клиентами и механизм информирования демона.
                                • 0
                                  В случае с SSE можно использовать обычный php файл (урл по которому будет запускаться php скрипт) и ничего не нужно устанавливать на сервере. Только убрать буферизацию, чтобы данные сразу отправлялись на клиент и в случае с php отпустить сессию session_write_close(), чтобы параллельные запросы работали от одного юзера.

                                  Насколько это хуже или лучше не знаю!? Но настраивать гораздо проще чем с сокетами.
                                  • 0
                                    Не понял как. Вот есть у меня 1000 клиентов, установивших соедение с сервером на предмет получения событий от него. С чем они это соединение устанавливают? С апачем? 1000 воркеров?
                                    • 0
                                      Выходит что так — будут висеть разные процессы и в них может быть разная логика. Не всем одно и тоже отсылается.

                                      А сокет вы будете открывать получается один на всех? Без привязки к конкретному пользователю? и все подключенные пользователи будут получать абсолютно одинаковую информацию?
                                      • 0
                                        На одном сокете одного процесса могут висеть тысячи соединений и работа с ними происходить независимо.
                                        • 0
                                          А что вы планируете использовать для сокетов?
                                          • 0
                                            PhpDaemon. Уже есть поддержка сервера ws, уже есть проверенная временем реализация многопроцессного сервера, на базе libevent.

                                            Ну, а в учебных целях или при незначительной нагрузке можно написать свой однопроцессный велосипед на базе обычных сокет-функций или даже многопроцессный на базе pcntl и semaphore, а то и с ptheads попробовать замутить, но подозреваю, что там будет много подводных камней.
                          • 0
                            Баг странный… Есть два клиента. Перезапускаем демон, оба клиенте коннектятся, шлют сообщения, оба видят кто что шлет. Если у одного из клиента обновить страницу, то отсылаемые от него сообщения другой клиент уже не видит.
                            • 0
                              Вероятно, имеется какая-то ошибка в PHP-коде. Вслепую сложно сказать. Попробуйте сделать простое тестовое приложение, если глюк повторится и в нем, выгрузите код этого приложения и задайте вопрос на официальном форуме или тостере.
                            • 0
                              Исходники примера хотелось бы на git, с зависимостями в composer.json.

                              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.