MyTetra Share
Делитесь знаниями!
Вебсокеты на 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 и после этого она выпала из моего круга зрения. А зря. Интересная библиотека.

Так же в этом разделе:
 
MyTetra Share v.0.59
Яндекс индекс цитирования