Привет, Хабр!
Недавно передо мной встала задача создания realtime чата для уже действующего сайта на Symfony 2.8
От LongPolling и SSE я сразу отказался по причинам, что они работают на уровне HTTP, а realtime с HTTP Symfony мне показался не лучшей затеей, поэтому смотреть имело смысл только в сторону протокола WebSocket. Прежде чем начать писать велосипед, я начал искать уже готовые пакеты для любимого фреймворка, и в конце поиска, который оказался достаточно коротким, я получил не так уж и много полезных библиотек.
Единственная, которую имеет смысл опубликовать и о которой я и буду рассказывать — GosWebSocketBundle
Установка:
Давайте потихоньку начинать.
Рассмотрим стандартную конфигурацию к бандлу:
Думаю тут и без объяснений все понятно. После того как мы сконфигурировали все, что нужно для первого запуска имеет смысл запустить сервер и увидеть успешное уведомление о том, что наш WS сервер работает и доступен по такому-то адресу.
Супер! Теперь мы можем подключиться к нашему серверу. Но тут есть нюанс, из документации нам предложено 2 варианта подключения JS библиотек для работы с бандлом.
1 cпособ:
Вставить следующий код перед тегом body:
Но тут оказался один минус, данный способ не работает в Symfony 2.8, т.к. AsseticBundle был уже удален в этой версии.
2 способ:
Просто подключить соответстующие библиотеки:
Да, бандл, как вы можете видеть, использует autobahn.js, подробнее о нем вы можете прочитать на официальном сайте
Итак! Необходимые библиотеки мы подключили, теперь давайте законектимся к нашему серверу:
Нам также по-умолчанию доступен такой способ подключения к серверу:
Смотрится куда более правильнее, не так ли?
Естественно, мы так же можем получить данные и из PHP:
Далее я буду реализовывать все вещи, связанные с Symfony, в AppBundle, чтобы вы спокойно смогли скопировать безо всяких затруднений и исправлений имен бандла.
Итак, приступим.
Нам нужно создать сервис для обработки наших первых ws соединений:
Далее нам необходимо «пометить» данный класс кактопик для нашего приложения, сделать это можно двумя способами:
Способ первый:
1.Просто создаем сервис
2.Добавляем его в основную конфигурацию бандла
Способ второй:
1.Определим класс сервис и добавим ему тег
Сам класс представляет из себя что-то вроде EventListener-а, что очень удобно для того чтобы отлавливать действия пользователей.
Я думаю тут понятно, что каждый метод срабатывает когда:
onSubscribe — пользователь «подписался на данный канал»
onUnSubscribe — пользователь отписался от данного канала
onPublish — пользователь нам что-то отправил
Далее у нас еще есть функция getName, она отвечает за идентификацию данноготопика\канала класса-сервиса, для того чтобы после Compiler смог его зарегистрировать и чтобы уже далее иметь возможность идентифицировать, какой конкретно класс подключать при том или ином запросе от клиента.
Сейчас вы поймете как это происходит.
Далее нам необходимо создать routing.yml файл для наших запросов на уровне ws:
Теперь самое сладкое! Давайте приступать!
Здесь мы подписываемся натопик\канал\сервис, который недавно создали и отправляем туда сообщение «Привет, я пришел от клиента!!!», при этом мы задаем: room = habrachat, user_id = 1
Даже сейчас мы можем запустить наш пример и увидеть в консоли:
Т.е. что мы видим?! Сначала отработал метод onSubscribe, а после — onPublish, подредактируем их слегка:
Далее перезапускаем наш WS-сервер и видим:
Во второй части статьи, которая на данный момент готовится, я расскажу о том, как правильно получить доступ к текущему пользователю, который на данный момент обращается к веб-сокет серверу
Для продакшена нам остается только установить supervisor:
1.Устанавливаем supervisor:
2.Конфигурация:
Создаем файл /etc/supervisor/conf.d/websocket.conf:
Теперь наш сервер будет автоматически перезапускаться в случае креша и запускаться при старте системы
3.Управление процессом:
Недавно передо мной встала задача создания realtime чата для уже действующего сайта на Symfony 2.8
От LongPolling и SSE я сразу отказался по причинам, что они работают на уровне HTTP, а realtime с HTTP Symfony мне показался не лучшей затеей, поэтому смотреть имело смысл только в сторону протокола WebSocket. Прежде чем начать писать велосипед, я начал искать уже готовые пакеты для любимого фреймворка, и в конце поиска, который оказался достаточно коротким, я получил не так уж и много полезных библиотек.
Единственная, которую имеет смысл опубликовать и о которой я и буду рассказывать — GosWebSocketBundle
Установка:
$ composer require composer require gos/web-socket-bundle
public function registerBundles()
{
$bundles = array(
// ...
new Gos\Bundle\WebSocketBundle\GosWebSocketBundle(),
new Gos\Bundle\PubSubRouterBundle\GosPubSubRouterBundle(),
);
}
Давайте потихоньку начинать.
Рассмотрим стандартную конфигурацию к бандлу:
# Web Socket Configuration
gos_web_socket:
server:
port: 3000 #The port the socket server will listen on
host: 127.0.0.1 #The host ip to bind to
Думаю тут и без объяснений все понятно. После того как мы сконфигурировали все, что нужно для первого запуска имеет смысл запустить сервер и увидеть успешное уведомление о том, что наш WS сервер работает и доступен по такому-то адресу.
$ app/console gos:websocket:server
Супер! Теперь мы можем подключиться к нашему серверу. Но тут есть нюанс, из документации нам предложено 2 варианта подключения JS библиотек для работы с бандлом.
1 cпособ:
Вставить следующий код перед тегом body:
{{ ws_client() }}
Но тут оказался один минус, данный способ не работает в Symfony 2.8, т.к. AsseticBundle был уже удален в этой версии.
2 способ:
Просто подключить соответстующие библиотеки:
<script type="text/javascript" src="{{ asset('bundles/goswebsocket/js/gos_web_socket_client.js') }}"></script>
<script type="text/javascript" src="{{ asset('bundles/goswebsocket/js/vendor/autobahn.min.js') }}"></script>
Да, бандл, как вы можете видеть, использует autobahn.js, подробнее о нем вы можете прочитать на официальном сайте
Итак! Необходимые библиотеки мы подключили, теперь давайте законектимся к нашему серверу:
var webSocket = WS.connect("ws://127.0.0.1:3000");
webSocket.on("socket/connect", function(session){
//session is an Autobahn JS WAMP session.
console.log("Successfully Connected!");
});
webSocket.on("socket/disconnect", function(error){
//error provides us with some insight into the disconnection: error.reason and error.code
console.log("Disconnected for " + error.reason + " with code " + error.code);
});
Нам также по-умолчанию доступен такой способ подключения к серверу:
var _WS_URI = "ws://{{ gos_web_socket_server_host }}:{{ gos_web_socket_server_port }}";
var myWs = WS.connect(_WS_URI);
Смотрится куда более правильнее, не так ли?
Естественно, мы так же можем получить данные и из PHP:
//Host
$container->getParameter('web_socket_server.host');
//Port
$container->getParameter('web_socket_server.port');
Далее я буду реализовывать все вещи, связанные с Symfony, в AppBundle, чтобы вы спокойно смогли скопировать безо всяких затруднений и исправлений имен бандла.
Итак, приступим.
Нам нужно создать сервис для обработки наших первых ws соединений:
namespace AppBundle\Topic;
use Gos\Bundle\WebSocketBundle\Topic\TopicInterface;
use Ratchet\ConnectionInterface;
use Ratchet\Wamp\Topic;
use Gos\Bundle\WebSocketBundle\Router\WampRequest;
class ChatTopic implements TopicInterface
{
public function onSubscribe(ConnectionInterface $connection, Topic $topic, WampRequest $request)
{
//this will broadcast the message to ALL subscribers of this topic.
$topic->broadcast(['msg' => $connection->resourceId . " has joined " . $topic->getId()]);
}
public function onUnSubscribe(ConnectionInterface $connection, Topic $topic, WampRequest $request)
{
//this will broadcast the message to ALL subscribers of this topic.
$topic->broadcast(['msg' => $connection->resourceId . " has left " . $topic->getId()]);
}
public function onPublish(ConnectionInterface $connection, Topic $topic, WampRequest $request, $event, array $exclude, array $eligible)
{
$topic->broadcast([
'msg' => $event,
]);
}
public function getName()
{
return 'app.topic.chat';
}
}
Далее нам необходимо «пометить» данный класс как
Способ первый:
1.Просто создаем сервис
services:
app.topic.chat:
class: App\Topic\ChatTopic
2.Добавляем его в основную конфигурацию бандла
gos_web_socket:
topics:
- @app.topic.chat
Способ второй:
1.Определим класс сервис и добавим ему тег
services:
app.topic.chat:
class: App\Topic\ChatTopic
tags:
- { name: gos_web_socket.topic }
Сам класс представляет из себя что-то вроде EventListener-а, что очень удобно для того чтобы отлавливать действия пользователей.
Я думаю тут понятно, что каждый метод срабатывает когда:
onSubscribe — пользователь «подписался на данный канал»
onUnSubscribe — пользователь отписался от данного канала
onPublish — пользователь нам что-то отправил
Далее у нас еще есть функция getName, она отвечает за идентификацию данного
Сейчас вы поймете как это происходит.
Далее нам необходимо создать routing.yml файл для наших запросов на уровне ws:
#AppBundle/Resources/config/pubsub/routing.yml
app_topic_chat:
channel: app/chat/{room}/{user_id}
handler:
callback: 'app.topic.chat' #Относится к getName, а не к имени сервиса
requirements:
room:
pattern: "[a-z]+" #accept all valid regex, don't put delimiters !
user_id:
pattern: "\d+"
Теперь самое сладкое! Давайте приступать!
webSocket.on("socket/connect", function(session){
session.subscribe("app/chat/habrchat/2", function(uri, payload){
console.log("Received message", payload.msg);
});
session.publish("app/chat/habrchat/2", "Привет, я пришел от клиента!!!");
})
Здесь мы подписываемся на
Даже сейчас мы можем запустить наш пример и увидеть в консоли:
Т.е. что мы видим?! Сначала отработал метод onSubscribe, а после — onPublish, подредактируем их слегка:
/**
* This will receive any Subscription requests for this topic.
*
* @param ConnectionInterface $connection
* @param Topic $topic
* @param WampRequest $request
* @return void
*/
public function onSubscribe(ConnectionInterface $connection, Topic $topic, WampRequest $request)
{
$room = $request->getAttributes()->get('room');
$userId = $request->getAttributes()->get('user_id');
//this will broadcast the message to ALL subscribers of this topic.
$topic->broadcast(['msg' => 'Новый пользователь зашел в комнату ' . $room . ' в личку к пользователю ' . $userId]);
}
/**
* This will receive any UnSubscription requests for this topic.
*
* @param ConnectionInterface $connection
* @param Topic $topic
* @param WampRequest $request
* @return void
*/
public function onUnSubscribe(ConnectionInterface $connection, Topic $topic, WampRequest $request)
{
$room = $request->getAttributes()->get('room');
$userId = $request->getAttributes()->get('user_id');
//this will broadcast the message to ALL subscribers of this topic.
$topic->broadcast(['msg' => 'Новый пользователь вышел из комнаты ' . $room . ' лички с пользователем ' . $userId]);
}
/**
* This will receive any Publish requests for this topic.
*
* @param ConnectionInterface $connection
* @param Topic $topic
* @param WampRequest $request
* @param $event
* @param array $exclude
* @param array $eligible
* @return mixed|void
*/
public function onPublish(ConnectionInterface $connection, Topic $topic, WampRequest $request, $event, array $exclude, array $eligible)
{
$room = $request->getAttributes()->get('room');
$userId = $request->getAttributes()->get('user_id');
$topic->broadcast([
'msg' => 'В комнату ' . $room . 'пользователю ' . $userId . ' поступило сообщение: ' . $event,
]);
}
Далее перезапускаем наш WS-сервер и видим:
Во второй части статьи, которая на данный момент готовится, я расскажу о том, как правильно получить доступ к текущему пользователю, который на данный момент обращается к веб-сокет серверу
Для продакшена нам остается только установить supervisor:
1.Устанавливаем supervisor:
$ apt-get install supervisor
2.Конфигурация:
Создаем файл /etc/supervisor/conf.d/websocket.conf:
command: /usr/bin/php /var/www/html/app/console gos:websocket:server
autorestart: true
autostart:true
stderr_logfile=/var/log/websocket.err.log
stdout_logfile=/var/log/websocket.out.log
Теперь наш сервер будет автоматически перезапускаться в случае креша и запускаться при старте системы
3.Управление процессом:
$ sudo supervisorctl start websocket
$ sudo supervisorctl restart websocket
$ sudo supervisorctl stop websocket
Поделиться с друзьями
Комментарии (8)
summerwind
02.06.2016 01:34От LongPolling и SSE я сразу отказался по причинам, что они работают на уровне HTTP, а realtime с HTTP Symfony мне показался не лучшей затеей
Могли бы вы пояснить, пожалуйста, чем вы считаете SSE хуже веб-сокетов?IncorrecTSW
02.06.2016 11:20Сдается автор слабо представляет с чем едят SSE, раз аргументирует тем что «на уровне HTTP». А вообще SSE не дружит с IE от слова совсем.
summerwind
02.06.2016 14:07
snnwolf
05.06.2016 16:54Мы на пытались пользовать сей бандл. Но решили от него отказаться в пользу чистого Ratchet'a, ибо сообщения тупили безбожно (посчитали, что виновата жирность symfony и убогость php [проект довольно большой], ну возможно ещё кривые руки были тому виной).
Evgeny42
Вчера как раз занимался похожими вещами. Использовал Ratchet вместе с phalcon, без проблем подружились. Единственное над чем пришлось повозиться это сессии. К которым phalcon не дает обращаться по id, пришлось самому делать запрос в базу сессий и парсить данные, которые там хранятся в странном формате.
Посмотрел, оказалось GosWebSocketBundle это тот же Ratchet просто обернутый в бандл.
php_freelancer
Да, совершенно верно, это тот же Ratchet.
Но тут с сессиями надо будет немного меньше возиться, в принципе там всё описано в документации, но я долго с этим разбирался и все-таки немного подводных камней да нащупал…
Даже решил отдельную статью под это дело выпустить скоро, она как раз готовится :)
Так вообще благо можно использовать общую конфигурацию (файрволы), что для http, что для ws с данным бандлом.
А учитывая что можно запустить вебсокет сервер из под любой (кастомной, под ws например) среды, как и любую команду в SF, то сконфигурировать всё гибко в приложении практически не составляет особого труда. Обожаю Симфони.
Но это всё уже в следующей статье.
Evgeny42
Насколько я знаю Ratchet из коробки поддерживает сессии Symfony.