Введение. Популярные драйверы Laravel и их проблемы

В современных веб-приложениях используются веб-сокеты, которые служат для создания пользовательских интерфейсов обновляющихся в реальном времени. В случае, если данные обновляются на сервере, то обычно происходит отправка сообщения, оповещения через соединение WebSocket для обработки клиентом. Вместо того, чтобы постоянно, в цикле следить за серверной частью приложения на предмет обновлений и изменения данных, которые должны быть отражены в вашем пользовательском интерфейсе, вы используете соединение Websocket, как эффективную альтернативу. Сегодня websocket порой не просто альтернатива, а необходимость в данном виде задач.

Для того чтобы помочь вам в разработке подобных функций, Laravel упрощает передачу серверных событий через соединение WebSocket. В момент трансляции ваших событий Laravel, вы можете использовать одни и те же имена событий и данные между клиентским JavaScript-приложением и серверным приложением Laravel.

Впервые столкнувшись с необходимостью обеспечить ваше приложение Laravel системой работы и сообщений в реальном времени вы начинаете с документации по широковещанию. Из нее становится ясно, что список указанных в документации драйверов не так уж широк, по умолчанию Laravel содержит два серверных драйвера трансляции на выбор: Pusher Channels и Ably. При этом основные провайдеры платные. Альтернативы есть, но они во многом завязаны на протоколе pusher и\или Laravel Echo. Поиск по другим альтернативам приводит к примерно этому неполному списку:

Пользоваться платными аналогами pusher не входит в планы (Pie Socket), часть сервисов, например, Cowboy фильтруется из-за слабой поддержки и неясности интеграции.

pusher, socket.io, soketi умеют подключаться через поддерживаемый сообществом Laravel Echo, об этих инструментах вы получаете информацию сразу со страницы документации, поэтому сначала взгляд обращается на них, так было у меня.

Однако, уже на первых этапах интеграции (на 2022-2023 год) были обнаружены проблемы совместимости Laravel Echo с новыми версиями socket.io, которые в итоге привели к выбору Laravel Websockets
Так, Laravel Echo работал только с "socket.io-client": "^2.4.0", версии 3.4 либо приводили к ошибкам, либо не соединялись с сервером и не передавали события. Подробности можно узнать здесь:
Laravel Echo Server, Redis, Socket.IO: Can't seem to make them work
Laravel Echo Server, no listening to channels but works with events
Not receiving websocket's message in frontend by the listener
В этом обсуждении Socket.IO v3.0 support авторы четко ответили, что "Мы решили, что больше не будем тратить время на Socket.io, поскольку не поддерживаем пакет laravel-echo-server. Вместо этого мы рекомендуем использовать что-то вроде https://github.com/beyondcode/laravel-websockets . Извините за это и спасибо за понимание".

Кроме этого, до представления Reverb библиотека Laravel Echo была немного заброшена и разработка велась неактивно. И только с появлением Reverb авторы возобновили активную разработку. Да так, что сразу все проблемы (issue) и Pull requests закрыты. Не каждый раз такое встретишь. Хотелось бы верить, что это надежно, а не на скорую руку.

Как выяснилось, проблемы совместимости можно решить. На стороне фронтэнд клиента, pusher, socket.io, soketi можно использовать и без laravel echo, с официальными клиентами:
pusher

var pusher = new Pusher("APP_KEY");
var channel = pusher.subscribe("APPL");
channel.bind("new-price", (data) => {
  // add new price into the APPL widget
});

socket.io

var socket = io(window.location.hostname + ':6001/', { transports: ['websocket'], });

socket.emit('subscribe', {
    channel: 'live',
    // auth: options.auth
}).on('live-event', function (channel, data) {
    console.log(data);
});

socketi использует тот же Pusher SDK клиент

const PusherJS = require('pusher-js');

let client = new PusherJS('app-key', {
    wsHost: '127.0.0.1',
    wsPort: 6001,
    forceTLS: false,
    encrypted: true,
    disableStats: true,
    enabledTransports: ['ws', 'wss'],
});

client.subscribe('chat-room').bind('message', (message) => {
    alert(`${message.sender} says: ${message.content}`);
});

И это является решением на клиентской стороне. Но, если вы столкнетесь с проблемами и захотите полностью независимую от Laravel Echo конфигурацию, на стороне сервера socket.io и socketi требуют установки и настройки собственного сервера на node js. Т.е. вам нужно будет настроить и держать в актуальном состоянии node js сервер, плюс к этому сам сервер websocket.

На 2022-2023 год автор статьи отдал предпочтение Laravel Websockets серверу с официальным Pusher клиентом. Библиотека достаточно известная, на хабре, например, есть статья посвященная именно вопросу отказа от Pusher в пользу Laravel Websockets
Однако, этот репозиторий был заархивирован владельцем 7 февраля 2024 г. Сейчас он доступен только для чтения. Последний релиз был в августе 2023, с этого времени много ошибок, связанных с неверными версиями зависимостей.

Пакет имеет свои недостатки связанные с производительностью, ведь сервер работает на php. Требует установки многочисленных зависимостей. Заглянув в composer.json мы, видим их широкий список:

"clue/redis-react": "^2.6",
"guzzlehttp/psr7": "^2.6",
"illuminate/console": "^10.47|^11.0",
"illuminate/contracts": "^10.47|^11.0",
"illuminate/http": "^10.47|^11.0",
"illuminate/support": "^10.47|^11.0",
"laravel/prompts": "^0.1.15",
"pusher/pusher-php-server": "^7.2",
"ratchet/rfc6455": "^0.3.1",
"react/promise-timer": "^1.10",
"react/socket": "^1.14",
"symfony/console": "^6.0|^7.0",
"symfony/http-foundation": "^6.3|^7.0"

Прошло время, и передо мной вновь встали рабочие задачи, которые снова вернули меня к теме websocket и обработки ответов и запросов в реальном времени. Ввиду того, что Laravel Websockets устарел и был заархивирован встала необходимость интеграции нового инструмента. Стало ясно, что "гудбай" придется сказать Laravel Websockets, и нужна новая современная, open source альтернатива. Первоначально я рассматривал вопрос наименьшего сопротивления, который официально поддерживается сообществом и имеет готовую модель интеграции.

Посмотрел красивую презентацию Reverb на LARACON EU 2024 года. Разработка (первый коммит) началась более двух лет назад, наиболее активно им стали заниматься в последнее время и Reverb имеет хорошую рекламную поддержку со стороны официальной команды Laravel. Но я решил, раз уж вкладывать свое время в изучение и интеграцию, то почему это должен быть Reverb, а не centrifugo, с которым я познакомился и заинтересовался еще несколько лет назад, но так и не дошел до интеграции.

Интеграция сервера centrifugo и Laravel драйвер

Сервер centrifugo. Исполняемый файл

Centrifugo – это программа написанная на языке Go, сервер сообщений в реальном времени (real-time). Сервер держит постоянные соединения от пользователей приложения и предоставляет API для моментальной рассылки какого-либо уведомления активным пользователям, подписанным на канал уведомления. Можно использовать для создания чатов, "живых" комментариев, multiplayer игр, стримить данные и метрики (например, быстро меняющиеся курсы валют).
Более подробно о сути и преимуществах данного сервера описано самим его создателем в Centrifugo – 3.5 миллиона оборотов в минуту

Мой локальный компьютер управляется Manjaro Linux, Arch-based дистрибутивом. В официальных репозиториях этих дистрибутивов и aur каталогах пакета centrifugo нет, поэтому я в ручную скачивал и настраивал его из раздела гитхаб "релизы". Авторы предоставляют варианты пакета для большинства современных платформ и архитектур:

  • Linux 64-bit Debian-based (amd64.deb, x86_64.rpm),

  • MacOS (darwin_amd64.tar.gz),

  • FreeBSD (freebsd_amd64.tar.gz),

  • Linux 32-bit (linux_386.tar.gz),

  • Linux 64-bit (linux_amd64.tar.gz),

  • Linux ARM 64-bit (linux_arm64.tar.gz),

  • ARM v6 (linux_armv6.tar.gz),

  • Windows (windows_amd64.zip)

Плюс такого подхода - вы всегда имеете самую свежую сборку. Минус - вам нужно самостоятельно следить за обновлением пакета, но как решить этот вопрос я расскажу ниже. Я использую версию linux_amd64.tar.gz

Как работает программа-сервер

Скачав пакет в нужную папку и разархивировав его мы имеем статически скомпилированный двоичный файл centrifugo готовый к запуску. Вам нужно дать права на исполнение. Только один файл программы и никаких десятков зависимостей в composer, за обновлением которых нужно следить, как было в beyondcode/laravel-websockets. Никаких зависимостей в виде сервера node js.

Centrifugo требует файла конфигурации с несколькими секретными ключами, перед подключением нужна авторизация. Есть команда genconfig, которая генерирует минимальный файл конфигурации для начала работы:

./centrifugo genconfig

При этом в текущем каталоге (по умолчанию) создается файл конфигурации config.json с некоторыми автоматически сгенерированными значениями параметров. Можно сделать конфиг не требующий авторизации, это небезопасно и только для тестовых целей. После этого все, что вам нужно просто запустить исполняемый файл и сервер начнет работу. Эта программа на go и есть ваш основной сервер сообщений в реальном времени, который начинает ждать команд от api.

centrifugo --config=config.json

# 2024-07-02 14:32:07 [INF] starting Centrifugo engine=memory gomaxprocs=12 pid=81478 runtime=go1.22.3 version=5.4.0
# 2024-07-02 14:32:07 [INF] using config file path=/home/yoda/config.json
# 2024-07-02 14:32:07 [INF] enabled JWT verifiers algorithms="HS256, HS384, HS512"
# 2024-07-02 14:32:07 [INF] serving websocket, api endpoints on :8000

Это сам сервер в чистом виде. Теперь наша основная задача посылать правильно подготовленные команды для api, centrifugo в ответ будет высылать сообщения.

Вся магия работы основана на знакомом всем REST Api, поэтому мы можем работать с приложением как и любым другим HTTP API, которые встречаются нам на практике. Есть возможность работать и по GRPC API, но она выходит за рамки этой статьи.

Управление программой сервером. HTTP API

HTTP API — это самый простой способ взаимодействия с Centrifugo из серверной части вашего приложения.

HTTP API Centrifugo работает на пути /api (по умолчанию).
HTTP API endpoint:

http://localhost:8000/api

Формат запроса очень прост:

  1. это HTTP POST-запрос к определенному пути API метода

  2. указывается данные для авторизации через header Authorization: apikey <YOUR_API_KEY>

  3. посылается тело post запроса (параметры) JSON объект.

более подробно: https://centrifugal.dev/docs/server/server_api#http-api

Схема с официального сайта
Схема с официального сайта

По умолчанию, все endpoints работают на порту 8000. Это можно легко изменить через конфиг программы port:

{"port": 9000}

теперь api будет доступно по этому адресу:

http://localhost:9000/api

API сервера поддерживает следующие методы:

  • publish

  • broadcast

  • subscribe

  • unsubscribe

  • disconnect

  • refresh

  • presence

  • presence_stats

  • history

  • history_remove

  • channels

  • info

  • batch

Будьте внимательны, некоторые методы по умолчанию отключены, например, presence_stats, history. При попытке доступа к:

http://localhost:8000/api/presence_stats

Вас ждет ошибка:
not available

Code:    108
Message: "not available"

Ошибка «Не доступен» означает, что ресурс не включен.

Например, это может быть возвращено при попытке доступа к истории или присутствию в канале, который не настроен для использования функций истории или присутствия.

Если вы хотите использовать эти методы в конфиг файле centrifugo нужно поставить, например, такие значения:

"history_size": 20,
"history_ttl": "6s",
"presence": true,

Вернемся к основным функциям.
Пример обращения к методу publish, по умолчанию:

http://localhost:8000/api/publish

Вот пример публикации сообщения в Centrifugo используя сырой запрос из curl:

curl --header "X-API-Key: <API_KEY>" \  --request POST \  --data '{"channel": "chat", "data": {"text": "hello"}}' \  http://localhost:8000/api/publish

Цель этой статьи показать использование centrifugo в Laravel, мы, разумеется, сырые запросы делать не планируем, но об этом дальше. Сейчас главное, что взаимодействие очень простое - это путь к методу и post запрос.

В случае успешной публикации вы получите такой ответ:

{"result": {}}

Этот ответ со стандартными настройками, он отличается при настройки параметров присутствия, которая по умолчанию отключена.

Особо стоит обратить внимание, что с 5 версии centrifugo переходит на новый формат api. Форматы запроса у версии до 4 включительно и 5-ой и далее имеют различия.
Old HTTP API format is DEPRECATED

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

К слову, автор статьи уже послал pull request в официальный php клиент и его успешно приняли
centrifugo v5 new HTTP API version support
Так что 5 версия и новый формат api не является проблемой для php клиентов сейчас.

Если раньше до 4 версии включительно, формат запроса был:

curl --header "Authorization: apikey <API_KEY>" \
  --request POST \
  --data '{"method": "info", "params": {}}' \
  http://localhost:8000/api

т.е. путь до метода указывался в data атрибутах post запроса - --data '{"method": "info", "params": {}}'.
В 5 версии авторы centrifugo решили унифицировать запросы к api со стандартами:

curl --header "X-API-Key: <API_KEY>" \
  --request POST \
  --data '{"channel": "test", "data": {"value": "test_value"}}' \
  http://localhost:8000/api/publish

из data атрибутов убрали method. Метод теперь часть ссылки(пути)

было

стало

--data '{"method": "publish"}'

http://localhost:8000/api/publish

Кроме этого, внесены изменения в авторизацию.

До 4 версии формат заголовка (header) http авторизации был:

X-API-Key: <YOUR_API_KEY>

в 5 версии изменили на:

Authorization: apikey <YOUR_API_KEY>

PHP SDK для centrifugo и драйвер Laravel

Для трансляции мы будем использовать драйвер Laravel, он помогает упростить взаимодействие между Laravel и Centrifugo, предоставляя удобные оболочки. Драйвер Laravel это высокоуровневая оболочка для PHP SDK. Он нужен, чтобы нам не приходилось делать запросы напрямую из класса php клиента и мы могли использовать родные методы Laravel broadcast. Laravel упрощает «вещание» серверных событий через соединение WebSocket и использование универсальных методов, независимо от поставщика драйвера.
Основные концепции широковещательной передачи просты: клиенты подключаются к именованным каналам во внешнем интерфейсе, в то время как ваше приложение Laravel транслирует события на эти каналы во внутреннем интерфейсе. Эти события могут содержать любые дополнительные данные, которые вы хотите сделать доступными для внешнего интерфейса.

Официальный php клиент

Библиотека PHP для связи с HTTP API Centrifugo
centrifugal/phpcent
это просто клиент Centrifugo HTTP API Client, а не драйвер для Laravel.
Его можно использовать как самостоятельный инструмент взаимодействия с api.

$client = new \phpcent\Client("http://localhost:8000/api");
$client->setApiKey("Centrifugo API key");
$client->publish("channel", ["message" => "Hello World"]);

Вещи описанные в разделе документации Laravel Broadcasting (Широковещание) с ним не выйдут. Его нужно встраивать.

Полноценные драйвера для Laravel Broadcasting от сообщества

Как уже сказано ранее, по умолчанию Laravel содержит два серверных драйвера трансляции на выбор: Pusher Channels и Ably. Поддержку остальных провайдеров можно получить написав драйвер самостоятельно, либо если его предоставит сам провайдер. Для centrifugo Laravel драйвера от провайдера не предоставляется, но есть написанные сообществом. К слову, на официальном сайте сообщается, что

интеграцию Centrifugo можно провести в несколько шагов даже без сторонних библиотек используя руководство по интеграции. Прямая интеграция может позволить использовать все функции Centrifugo без ограничений, которые могут быть введены сторонней оболочкой.

Это конечно, не совсем так, ведь при "прямой интеграции" вы не сможете полноценно использовать трансляцию (broadcast) событий в Laravel.
Например так, не выйдет при "прямой интеграции":

$order = Order::create([]);

OrderShipmentStatusUpdated::dispatch($order);

Вам нужно будет напрямую вызывать методы собственного класса или сырые http запросы, чтобы отправить данные, а также самостоятельно отправлять их в очереди для отложенной отправки, производить авторизацию приватных каналов и пр. вещи, которые при использовании драйвера делаются и пишутся один раз и выполняются "под капотом".

Обзор и изучение информации позволил сформировать следующий список драйверов Laravel для centrifugo:

  1. LaraComponents / centrifuge-broadcaster https://github.com/LaraComponents/centrifuge-broadcaster Прародитель для всех остальных, ниже представленных.

  2. Opekunov / laravel-centrifugo-broadcaster https://github.com/Opekunov/laravel-centrifugo-broadcaster

  3. denis660 / laravel-centrifugo https://github.com/denis660/laravel-centrifugo

  4. alexusmai / laravel-centrifugo https://github.com/alexusmai/laravel-centrifugo

4 крайне схожих по реализации пакета. Реализация содержит

  • HTTP API Клиент,

  • CentrifugoBroadcaster (непосредственно сам драйвер)

  • сервис провайдер.

HTTP API клиент, которых во многом повторяет оригинальный centrifugal/phpcent . C той разницей, что centrifugal/phpcent не требует установки GuzzleHttp\Client и пользуется родным модулем php "ext-curl".
С одной стороны, можно было было добавить как HTTP API клиент зависимость и не писать свой клиент. С другой, мы получаем самостоятельный пакет, если centrifugal/phpcent перестанет развиваться, представленные драйвера от него не зависят.
Ни у одного из пакетов нет поддержки Laravel 11.

Собственно в этом драйвере и реализуются большинство api методов Centrifugo нужные Laravel для broadcasting. Реализованы они через известный многим GuzzleHttp\Client

Клиент (класс) Centrifugo можно использовать и отдельно от драйвера.

Простой пример использования клиента:

<?php
declare(strict_types = 1);

namespace App\Http\Controllers;


use denis660\Centrifugo\Centrifugo;
use Illuminate\Support\Facades\Auth;

class ExampleController
{

    public function example(Centrifugo $centrifugo)
    {
        // Отправить сообщение в канал news
        $centrifugo->publish('news', ['message' => 'Hello world']);

        // Сгенерировать токен для подключения
        $token = $centrifugo->generateConnectionToken((string)Auth::id(), 0, [
            'name' => Auth::user()->name,
        ]);

        // Сгенерировать токен для подключения к приватному каналу
        $apiSign = $centrifugo->generatePrivateChannelToken((string)Auth::id(), 'channel', time() + 5 * 60, [
            'name' => Auth::user()->name,
        ]);

        // Получить список активных каналов
        $centrifugo->channels();

        // Получить информацию о канале news, список активных клиентов
        $centrifugo->presence('news');

    }
}

при этом в документации к драйверу нет информации как аутентифицировать и получить токен используя встроенные средства Laravel. А ведь генерация Токена начального подключения это, по умолчанию, обязательное условие соединения с сервером centrifugo.
Токен предоставляется так:

const client = new Centrifuge('ws://ваш.сайт/connection/websocket', {
    token: 'JWT-GENERATED-ON-BACKEND-SIDE'
});

Если вы оставите свойство токена пустым, сервер выдаст ошибку "client credentials not found" и не даст подключить клиента:

{"level":"info","client":"38ed3443-91fc-4100-9581-704012710fe1","user":"","time":"2024-06-09T15:00:06+03:00","message":"client credentials not found"}

см. API клиентского SDK

Клиент и сервер

Но как так получается, что и js приложение, и php приложение в терминологии это клиенты?

После запуска centrifugo слушает (ожидает) соединений на порту 8000

Proto    Local Address     Foreign Address         State       PID/Program name          
tcp      0.0.0.0:8000      0.0.0.0:*               LISTEN      81478/centrifugo       

js клиент в приложениях на Laravel, как правило, не общается с клиентом api centrifugo напрямую (хотя такая возможность есть).
js клиент соединяется не с api, а с websocket и случает канал.

точка доступа к websocket
на локальном компьютере разработки
ws://localhost:8000/connection/websocket
на production сайте
ws://ваш_сайт/connection/websocket
Обращение с браузера клиента на адрес ws://ваш_сайт/connection/websocket как правило проксируется через ваш веб сервер, например, nginx. Это нужно настроить, об этом позже. Поэтому при первоначальной настройке важно не перепутать и не сделать так, как указано во всех документациях и справках к js клиенту new Centrifuge('ws://localhost:8000/connection/websocket'). Этот адрес только для локальной разработки, если оставить его без изменений, когда production сервер отдаст страницу браузеру пользователя на ней будет запрос на localhost. Браузер пользователя будет стучаться в свой же локальный компьютер и соединения не будут проходить. Когда настраиваешь много компонентов системы это можно и упустить.

Наше php Laravel приложение является для js приложения в браузере - сервером, который принимает и обрабатывает, отсылает сообщения. В момент когда php приложение получает запрос от js оно, в свою очередь, посылает запрос к приложению centrifugo по http api. И в этом смысле, php приложение (sdk, драйвер Laravel) является клиентом api centrifugo.

php отдает команды и посылает сообщения через http api
точка доступа по умолчанию
http://localhost:8000/api

Соответственно один управляет и отправляет, другой слушает и получает. php посылает в канал, js получает с канала. Так что php это не совсем клиент, это бэкэнд управления centrifugo api, клиентом является js приложение в браузере. Есть и другие варианты настройки, но это основной.

Перед тем как клиент присоединиться к серверу нужно пройти авторизацию.
js клиент проходит авторизацию получая jwt токен от сервера. Поскольку мы условились, что этим сервером является php приложение, оно и должно выдать этот токен.

Соответственно, php нужно сгенерировать логин используя api метод и предоставить его клиенту js.

Подключение и настройка драйвера Laravel

Установите драйвер через composer.

Откройте ваш config/app.php и добавьте следующее в раздел:

<?
return [
    'providers' => [    
        App\Providers\BroadcastServiceProvider::class,
    ],  
];

Откройте ваш config/broadcasting.php и добавьте туда новое подключение:

<?
return [
        'centrifugo' => [
            'driver' => 'centrifugo',
            'token_hmac_secret_key'  => env('CENTRIFUGO_TOKEN_HMAC_SECRET_KEY',''),
            'api_key'  => env('CENTRIFUGO_API_KEY',''),
            'url'     => env('CENTRIFUGO_URL', 'http://localhost:8000'), // centrifugo api url
            'verify'  => env('CENTRIFUGO_VERIFY', false), // Verify host ssl if centrifugo uses this
            'ssl_key' => env('CENTRIFUGO_SSL_KEY', null), // Self-Signed SSl Key for Host (require verify=true)
        ],    
];

Также вы должны добавить эти две строчки в ваш .env файл:

CENTRIFUGO_TOKEN_HMAC_SECRET_KEY=token_hmac_secret_key-from-centrifugo-config
CENTRIFUGO_API_KEY=api_key-from-centrifugo-config

Эти строки необязательны:

CENTRIFUGO_URL=http://localhost:8000
CENTRIFUGO_SSL_KEY=/etc/ssl/some.pem
CENTRIFUGO_VERIFY=false

Не забудьте изменить параметр BROADCAST_DRIVER в файле .env!

BROADCAST_DRIVER=centrifugo

Пример файла конфигурации сервера Centrifugo (не путать с config/broadcasting.php):

{
    "token_hmac_secret_key": "ключ",
    "admin_password": "пароль",
    "admin_secret": "секрет",
    "api_key": "ключ",
    "allowed_origins": [
        "http://127.0.0.1",
        "http://localhost",
    ],
    "admin": true,
    "client_connect_include_server_time": true,
    "allow_subscribe_for_client": true,
    "history_size": 20,
    "history_ttl": "6s",
    "presence": true,
    "log_level": "debug",
    "address": "127.0.0.1"
}

Бэкэнд websocket сервер Laravel

Встает вопрос когда лучше генерировать и передавать параметры авторизации сделать?

  • можно генерировать в middleware

  • можно отправить запрос, предварительно создав маршрут в broadcast для генерации токена

  • генерировать при входе в аккаунт на постоянной основе

  • можно выдавать токен для веб сокета вместе с авторизацией в аккаунте вашего приложения - авторизируем, сохраняем в сессии токен, регенерируем при определенных условиях

<?

class AuthService
{
 public function generateConnectionToken(User $user): string
    {
        /* @var  CentrifugoInterface $cenrifugo */
        $cenrifugo = \app()->make('centrifugo');

        $token = $cenrifugo->generateConnectionToken((string) $user->id);

        session(['jwt_centrifugo_token' => $token]);

        return $token;
    }
}

маршрут

<?

Route::middleware(['auth'])
    ->group(function () {
    
    Route::get('item', 'index')->name('item');
   //... другие роуты
});

контроллер

<?
class ItemController
{
    public function index()
   {
      return response()->json([
        'token'=>session('token'),
      // ... другие данные
      ]);
   }
}

Лучше будет не передавать каждый раз token в контроллере, а сделать отдельный middleware, и расшаривать данные токена нужным контроллерам. Или сделать это в boot методе app/Providers/AppServiceProvider.php через View Share.

Токен нужно будет обновлять если изменился конфиг centrifugo или ключ CENTRIFUGO_TOKEN_HMAC_SECRET_KEY

Токен - это доступ к серверу сообщений centrifugo. Если брать пример из жизни. Это как студенческий в университет, получив его вы можете пройти охранника и свободно прогуливаться по территории. Однако, такой доступ не дает права посещать чужие лекции, заходить в деканат и кафедру.
Этим доступом по каналам удобнее заниматься через сам фреймворк Laravel, например, так:

<?
use App\Models\User;

Broadcast::channel('educational department', function (User $user, ?int $certificateId) {
    return $user->status->name === 'teacher' && !empty($certificateId);
});

Если результат этого замыкания === true, клиент js пользователя проходит авторизацию в канале и может прослушивать сообщения с него.

Можно и через api клиент

<?
// Сгенерировать токен для подключения к приватному каналу
$apiSign = $centrifugo->generatePrivateChannelToken(
  (string)Auth::id(), 'channel', time() + 5 * 60, [
            'name' => Auth::user()->name,
]);

Если вы сделали что-то неверно, коды ошибок можно посмотреть здесь:
Client protocol codes

Подключение к centrifugo через js клиент

Теперь ваши пользователи могут начать подключаться к Centrifugo. Вам необходимо получить клиентскую библиотеку centrifuge-js для интерфейса вашего приложения.

centrifuge-js является официальным SDK реального времени и соответствует спецификации centrifugo. Предоставляет клиент для подключения к Centrifugo или любому Centrifuge-based серверу с использованием чистого WebSocket или альтернативных транспортов (HTTP-streaming, SSE/EventSource, экспериментальный WebTransport) из веб-браузера, сред ReactNative или NodeJS.

У клиента на js на данный момент самый широкий набор возможностей. Среди клиентов на других языках, centrifuge-js выделяется дополнительными возможностями, а именно:
в функциях, связанных с

  • подключением - batching API, bidirectional WebSocket emulation,

  • подпиской на стороне клиента - Оптимистичные подписки, delta compression. centrifuge-js по умолчанию использует сериализацию JSON для кадров протокола. Это делает общение с сервером Centrifugo удобным, поскольку мы обмениваемся удобочитаемыми кадрами JSON между клиентом и сервером. И это дает возможность использовать centrifuge-js без дополнительной зависимости от protobuf.js библиотеки.

Websocket — основной транспорт в Centrifugo. Это очень эффективный протокол с низкими издержками поверх TCP.

Самым большим преимуществом является то, что Websocket работает «из коробки» во всех современных браузерах, и почти все языки программирования имеют реализации Websocket. Это делает Websocket универсальным транспортом, который можно использовать для подключения к Centrifugo практически отовсюду. Но если вас он не устраивает, есть и другие.

Путь соединения WebSocket по умолчанию в Centrifugo:

/connection/websocket

Каждый клиент должен предоставить токен подключения (JWT) при подключении. Вы должны сгенерировать этот токен на своей серверной стороне, используя секретный ключ Centrifugo, который вы установили в конфигурации серверной части (обратите внимание, что в случае токенов RSA вы генерируете JWT с закрытым ключом). О том, как сгенерировать этот JWT, смотрите в специальной главе .

Вы передаете этот токен из серверной части в свое внешнее приложение (передаете его в контексте шаблона или используете отдельный запрос со стороны клиента, чтобы получить пользовательский JWT со стороны серверной части). И использовать этот токен при подключении к Centrifugo (например, в браузерном клиенте есть специальный метод setToken).

Если вы неправильно передадите токен авторизации вас ждет следующий код ошибки:
Unauthorized

Code:    101 
Message: "unauthorized"

Ошибка Unauthorized говорит о том, что запрос неавторизован.
Для первоначальной настройки и отладки лучше поставить в конфиг файле centrifugo

"log_level": "debug",

Тогда в логах приложения centrifugo вы сможете видеть больше информации о том, проходят ли соединения, сообщения и подробности об ошибках.

Подключитесь к локальному Centrifugo с помощью JavaScript SDK:

const centrifuge = new Centrifuge('ws://localhost:8000/connection/websocket', {
    // token: ?,
});

client.connect();

Обратите внимание, что мы явно вызываем .connect() метод инициации установления соединения с сервером.
При этом не обязательно сразу соединяться с сервером. Можно это делать по условию, и самое главное можно предварительно настроить все этапы соединения, оповещения и подписок. Настроить сам объект который мы сохранили в переменную centrifuge.

Пример, если вы используете vue.

import { Centrifuge } from 'centrifuge';
import { reactive } from "vue";

export function useCentrifugo(endpoint = "ws://ваш.сайт/connection/websocket", options) {
    const centrifugo = new Centrifuge(endpoint, options)

	// Клиентские методы и события. Настраиваем, что делать в случае началы соединения, успешного соединения, разрыва связи 
    centrifugo.on('connecting', function (ctx) {
        console.log(`connecting: ${ctx.code}, ${ctx.reason}`);
    }).on('connected', function (ctx) {
        console.log(`connected over ${ctx.transport}`);
    }).on('disconnected', function (ctx) {
        console.log(`disconnected: ${ctx.code}, ${ctx.reason}`);
    });

    // Настраиваем подписки, их например, можно  передать в аргументах options?.channels, при создании объекта 
    const subscriptions = reactive({})

    if (options?.channels) {
        for (let i = 0; i < options.channels.length; i++) {

            const channel = options?.channels[i];

            subscriptions[channel] = centrifugo.newSubscription(channel);

            subscriptions[channel].on('publication', function (ctx) {
                console.log(ctx);
            }).on('subscribing', function (ctx) {
                console.log(`subscribing: ${ctx.code}, ${ctx.reason}`);
            }).on('subscribed', function (ctx) {
                console.log('subscribed', ctx);
            }).on('unsubscribed', function (ctx) {
                console.log(`unsubscribed: ${ctx.code}, ${ctx.reason}`);
            })
        }
    }

    return { centrifugo, subscriptions }
}

При этом subscriptions[channel].on можно добавлять, переписывать и дальше. После первоначальной настройки и запуска функции:

const websocket = useCentrifugo("ws://ваш.сайт/connection/websocket", options);

websocket.centrifugo.on('disconnected', function (ctx) {
    // новый код;
})

websocket.subscriptions['ваш канал подписки'].on('disconnected', function (ctx) {
    // новый код;
})

Более развернутый пример:

const { centrifugo, subscriptions } = useCentrifugo(
    pageData.value.props.config.ws?.route,
    pageData.value.props.config.ws
);

centrifugo.on('connected', function (ctx) {
    subscriptions['ваш канал подписки'].on('publication', function (sub) {
        if (sub.data.event == 'markedCreated') {
          
          // показываем модальное окно
            showModal(sub.data.item)
            
        }
    }).subscribe();

  // Подписываемся только, если в админке включен этот канал и если он доступен. Иначе будут постоянные ошибки подключения
    if (pageData.value.props.config.live !== undefined && pageData.value.props.config.live.status) {

        subscriptions.live.on('publication', function (sub) {
            // console.log(sub)
        }).subscribe();

    }
});

Как видно в примере, мы используем проверку на имя события.

if (sub.data.event == 'markedCreated')

Это является альтернативой listen метода, если вы использовали Laravel echo ранее:

Echo.channel('канал подписки')
    .listen('markedCreated', (e) => {
    // показываем модальное окно
    showModal(e.item)
});

По умолчанию, клиент умеет прослушивать только эти события подписки:

  • publication – вызывается при получении новой публикации из канала подписки

  • join – вызывается, когда кто-то присоединился к каналу

  • leave – вызывается, когда кто-то покидает канал

  • subscribing - вызывается, когда подписка переходит на subscribing состояние (первоначальная подписка и повторная подписка)

  • subscribed – вызывается, когда подписка переходит на subscribed состояние

  • unsubscribed – вызывается, когда подписка переходит на unsubscribed состояние

  • error– вызывается, когда подписка на канал завершилась с ошибкой. Его можно вызывать несколько раз в течение всего срока службы, поскольку браузерный клиент автоматически переподписывается на каналы после успешного повторного подключения. (например, из-за временного отключения сети или перезапуска сервера Centrifugo)

События, которые вы посылаете из Laravel, он не распознает автоматически.

В своем приложении Laravel, чтобы передать сообщение вы делаете что-то вроде этого:

<?

class MarkedCreated extends Event implements ShouldBroadcast  
{  
    use InteractsWithSockets, SerializesModels;  
  
    public function __construct(public Item $item)  
    {               
    }   

    public function broadcastOn(): Channel
    {
        return new Channel('marked');
    }

    public function broadcastAs(): string
    {
        return 'markedCreated';
    }

}

Вместо заключения.

Спасибо всем, кто дочитал до конца. Надеюсь, данная статья будет полезной всем, кто ищет новый способ для работы с websocket в Laravel и хочет расширить свои навыки.
В дальшейшем, планирую расширить содержание этой статьи второй частью, где расскажу о том, как написать systemd сервис для автоматического запуска centrifugo, ansible скрипты для автоматической публикации на сервере, и скрипт автоматического обновления версии centrifugo используя репозитории github.
Кроме преимуществ в виде производительности, простоты настройки, функциональности представленных в этой статье, приятно, что проект является open-source и создан нашим коллегой соотечественником и его командой.
Если вас интересует драйвер для Laravel 11 и поддержка нового api 5 версии centrifugo я сделал форк и внес эти функции, сам использую в работе Laravel driver Centrifugo 5

Комментарии (0)