|
|||||||
Вебсокеты на php. Выбираем вебсокет-сервер
Время создания: 07.05.2018 22:27
Автор: https://habr.com/users/morozovsk/
Текстовые метки: websocket
Раздел: WebSocket
Запись: Velonski/mytetra-database/master/base/15257140484y7qwkox3c/text.html на raw.githubusercontent.com
|
|||||||
|
|||||||
Давным-давно я публиковал статью на хабре, как написать свой вебсокет-сервер с нуля. Статья переросла в библиотеку. Несколько месяцев я занимался её развитием, ещё несколько лет — поддержкой и багфиксом. Написал модуль интеграции с yii2. Какой-то энтузиаст написал интеграцию с laravel. Моя библиотека совместима с php7. Недавно я решил отказаться от её дальнейшей поддержки (причины ниже), поэтому хочу помочь её пользователям перейти на другую библиотеку. Прежде чем начать писать свой вебсокет-сервер, я выбирал из готовых продуктов, и на тот момент их было всего два: phpdaemon и ratchet. phpdaemon 1400 звёзд на гитхабе зависит от установки библиотеки libevent протоколы: HTTP, FastCGI, FlashPolicy, Ident, Socks4/5. Ratchet 3600 звёзд на гитхабе тянет за собой около десятка зависимостей протоколы: websocket, http, wamp поддержка windows нет ssl Эти библиотеки были очень монструозны и при этом не соответствовали моим внутренним требованиям: отсутствие зависимостей наличие таймеров Таймеры мне нужны были для написания игры на вебсокетах для расчёта взаимодействий между всеми пользователями каждые 0.05 секунды. В итоге я написал библиотеку для себя и поделился ею с сообществом на гитхабе. Сделал несколько демок (в том числе игру «танчики»). Переписал стороннюю игру (с разрешения авторов) с node.js на свою библиотеку. Делал нагрузочное тестирование. Демки работали годами без перезагрузки. Старался отвечать на тикеты в течения дня. Всё это показывало, что моя библиотека может быть использована на продакшене и многие её использовали. Была единственная проблема. Мне хватало моей библиотеки для использования в своих проектах, а вот другим нет. Они хотели, чтобы я её развивал, а мне это было не нужно. Кому-то требовалась поддержка windows, а кому-то ssl, pg_notify, safari, pthreads и многое другое. Открытые тикеты с запросами на реализацию различного функционала висят годами. Не так давно, я решил пересмотреть ещё раз, какие продукты могут быть полезны для пользователей моей библиотеки и был приятно удивлён, что кроме двух проектов, описанных выше появился ещё третий. Он полностью удовлетворял моим запросам и даже больше. Workerman 4500 звёзд на гитхабе отсутствие зависимостей протоколы: websocket, http/https, tcp, сustom поддержка таймеров интеграция с react-компонентами поддержка windows Первый его релиз был ещё два года назад, но почему-то всё новые и новые люди начинали пользоваться моей библиотекой для новых проектов. Я ещё могу понять, что ею пользуются на старых проектах (работает — не трогай), но на новых… — для меня это была загадка. Если загуглить «php websocket», то первая страница — это моя статья на Хабре, а вторая — «Ratchet», который кому-то может показаться сложным и он выберет из-за этого мою библиотеку или вообще откажется от идеи делать вебсокеты. Что ж, пришло время исправить эту досадную ошибку и донести до как можно большего количества людей о существовании такой библиотеки как Workerman и привести несколько примеров по её использованию. На главной странице проекта в гитхабе уже есть несколько примеров. Рассмотрим один из них: websocket server <?php require_once __DIR__ . '/vendor/autoload.php'; use Workerman\Worker; // Create a Websocket server $ws_worker = new Worker("websocket://0.0.0.0:8000"); // 4 processes $ws_worker->count = 4; // Emitted when new connection come $ws_worker->onConnect = function($connection) { echo "New connection\n"; }; // Emitted when data received $ws_worker->onMessage = function($connection, $data) { // Send hello $data $connection->send('hello ' . $data); }; // Emitted when connection closed $ws_worker->onClose = function($connection) { echo "Connection closed\n"; }; // Run worker Worker::runAll(); tcp server <?php require_once __DIR__ . '/vendor/autoload.php'; use Workerman\Worker; // #### create socket and listen 1234 port #### $tcp_worker = new Worker("tcp://0.0.0.0:1234"); // 4 processes $tcp_worker->count = 4; // Emitted when new connection come $tcp_worker->onConnect = function($connection) { echo "New Connection\n"; }; // Emitted when data received $tcp_worker->onMessage = function($connection, $data) { // send data to client $connection->send("hello $data \n"); }; // Emitted when new connection come $tcp_worker->onClose = function($connection) { echo "Connection closed\n"; }; Worker::runAll(); Чтобы запустить пример, нужно установить workerwan: composer require workerman/workerman Пример можно запустить с помощью команды php test.php start и в консоли мы увидим: ----------------------- WORKERMAN ----------------------------- Workerman version:3.3.6 PHP version:7.0.15-0ubuntu0.16.10.4 ------------------------ WORKERS ------------------------------- user worker listen processes status morozovsk none websocket://0.0.0.0:8000 1 [OK] ---------------------------------------------------------------- Все команды workerman: php test.php start php test.php start -d -демонизировать скрипт php test.php status php test.php stop php test.php restart php test.php restart -d php test.php reload В принципе, используя первый пример можно сделать чат на вебсокетах и других примеров не нужно. Но за несколько лет я понял, что в основном пользователям моей библиотеки был нужен пример того как можно отправить из своего кода на php уведомление выбранному пользователю, а не всем одновременно, как часто бывает в примерах. Например: пользователь #1 лайкает фотографию пользователя #2 и мы хотим отправить пользователю #2 об этом уведомление, если он сейчас на сайте. на сайте появилось новое объявление и мы хотим отправить уведомление нашему модератору, чтобы он его проверил Из двух примеров выше можно собрать один, который будет делать то что нам нужно: Отправка сообщения одному пользователю: код сервера server.php: <?php require_once __DIR__ . '/vendor/autoload.php'; use Workerman\Worker; // массив для связи соединения пользователя и необходимого нам параметра $users = []; // создаём ws-сервер, к которому будут подключаться все наши пользователи $ws_worker = new Worker("websocket://0.0.0.0:8000"); // создаём обработчик, который будет выполняться при запуске ws-сервера $ws_worker->onWorkerStart = function() use (&$users) { // создаём локальный tcp-сервер, чтобы отправлять на него сообщения из кода нашего сайта $inner_tcp_worker = new Worker("tcp://127.0.0.1:1234"); // создаём обработчик сообщений, который будет срабатывать, // когда на локальный tcp-сокет приходит сообщение $inner_tcp_worker->onMessage = function($connection, $data) use (&$users) { $data = json_decode($data); // отправляем сообщение пользователю по userId if (isset($users[$data->user])) { $webconnection = $users[$data->user]; $webconnection->send($data->message); } }; $inner_tcp_worker->listen(); }; $ws_worker->onConnect = function($connection) use (&$users) { $connection->onWebSocketConnect = function($connection) use (&$users) { // при подключении нового пользователя сохраняем get-параметр, который же сами и передали со страницы сайта $users[$_GET['user']] = $connection; // вместо get-параметра можно также использовать параметр из cookie, например $_COOKIE['PHPSESSID'] }; }; $ws_worker->onClose = function($connection) use(&$users) { // удаляем параметр при отключении пользователя $user = array_search($connection, $users); unset($users[$user]); }; // Run worker Worker::runAll(); код клиента client.html: <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <script> ws = new WebSocket("ws://127.0.0.1:8000/?user=tester01"); ws.onmessage = function(evt) {alert(evt.data);}; </script> </head> </html> код отправки сообщений с нашего сайта send.php: <?php $localsocket = 'tcp://127.0.0.1:1234'; $user = 'tester01'; $message = 'test'; // соединяемся с локальным tcp-сервером $instance = stream_socket_client($localsocket); // отправляем сообщение fwrite($instance, json_encode(['user' => $user, 'message' => $message]) . "\n"); Справедливости ради я решил написать такой же пример для ratchet, но документация мне не помогла, как 3 года назад. Зато на stackoverflow предложили немного костыльный, но рабочий вариант: соединяться из своего php-скрипта по ws-соединению. Конечно это не так же просто как соединиться с tcp-сокетом с помощью stream_socket_client и отправить сообщение с помощью fwrite. Но уже что-то. Плюс ещё остался для меня незакрытый вопрос: поддерживает ли ratchet возможность запуска нескольких воркеров и если да, то как в таком случае отправлять сообщение одному пользователю, ведь не понятно на каком он воркере. На workerman это можно сделать так. В общем, я выбрал для себя библиотеку Workerman и рекомендую переходить на неё пользователям моей библиотеки. Все примеры лежат на гитхабе. Update: в комментариях рекомендуют swoole. Я натыкался на эту библиотеку ранее, но у меня сложилось ложное впечатление, что что она не поддерживает php7 и после этого она выпала из моего круга зрения. А зря. Интересная библиотека. |
|||||||
Так же в этом разделе:
|
|||||||
|
|||||||
|