В этой статье мы рассмотрим интеграцию сервера реального времени Centrifugo с фреймворком Laravel, основные настройки и нюансы работы

Данная статья будет больше про реализацию на самом фреймворке чем описание Centrifugo.

Так же пример взаимодействия вы можете найти в данном шаблоне который описывал в статье про frankenphp+laravel

Centrifugo – это сервер для работы в реальном времени, который поддерживает различные транспорты для подключения клиентов, включая WebSocket, HTTP-streaming, Server-Sent Events (SSE) и другие. Он использует publish-subscribe паттерн для обмена сообщениями`

Он выступает промежуточным звеном (прокси) между вашим бэкендом и клиентами (веб, мобильные приложения). Его главная задача — доставлять сообщения от сервера к клиентам и между клиентами мгновенно.

Что он умеет:

  1. Односторонняя рассылка (Push-уведомления): Сервер может отправить сообщение всем подписанным клиентам или конкретным пользователям.

  2. Двустороннее общение (Pub/Sub): Клиенты могут подписываться на каналы (топики) и публиковать в них сообщения, которые получат все остальные подписчики (если есть права).

  3. Масштабирование: Легко запускается в кластере (через Redis или Tarantool), чтобы выдерживать нагрузку от миллионов соединений.

  4. Надёжность: Восстанавливает потерянные соединения и доставляет сообщения, которые были отправлены, пока клиент был оффлайн (персистентные каналы).

  5. Проверка прав доступа: Запросы на подключение и публикацию всегда идут через ваш бэкенд для авторизации, что обеспечивает безопасность.

Ключевая философия:

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

Документацию вы можете найти здесь

Разворачиваем Centrifugo:

Устанавливать будем через docker

Пример dockerfile:

Скрытый текст
FROM centrifugo/centrifugo:v6 as Base  
  
FROM base AS dev  
  
COPY .docker/centrifugo/config-test.json /centrifugo/config.json  
  
CMD ["centrifugo", "-c",  "config.json"]  
  
FROM base AS prod  
  
COPY .docker/centrifugo/config.json /centrifugo/config.json  
  
CMD ["centrifugo", "-c",  "config.json"]

В данном примере мы поднимаем контейнер с нашим сервисом и добавляем конфиг с настройками

Мы используем мультистейджинг разделяя конфиги:

  • тестовый конфиг мы храним в репозитории

  • второй на сервере или в env github/gitlab

Такое разделение нужно, так как там содержатся ключи шифрования и пароль от админки

Пример тестового конфига:

Скрытый текст
{
  "token_hmac_secret_key": "your-secret-here",
  "admin_password": "strong-password",
  "admin_secret": "admin-secret",
  "api_key": "your-api-key",

  "channels": [
    {
      "name": "news",
      "publish": true,      // Разрешить клиентам публикацию
      "subscribe": true,    // Разрешить клиентам подписку
      "history_size": 100,  // Хранить последние 100 сообщений
      "history_ttl": "5m"   // Хранить историю 5 минут
    },
    {
      "name": "user:$user", // Персональный канал (по пользователю)
      "subscribe": true,
      "publish": false      // Только сервер может публиковать
    },
    {
      "name": "chat:room-#rooms", // Канал с комнатами
      "presence": true,     // Включить отслеживание присутствующих
      "join_leave": true    // Отправлять события входа/выхода
    }
  ]
}

Чтобы сгенерировать свой конфиг вы должны зайти в контейнер и прописать команду

centrifugo genconfig

Или вы можете взять пример из документации

token_hmac_secret_key: Обязательный секрет для подписи JWT-токенов клиентов.

admin_password / admin_secret: Пароль для входа в админ-панель и секрет для админского API.

api_key: Ключ для вызова Server API (для публикации с бэкенда).

engine: Выбор движка для хранения данных (memory, redis, tarantool). По умолчанию — memory.

presence: Глобальное включение отслеживания присутствия в каналах.

history_meta_ttl: Как долго хранить мета-информацию истории сообщений.

namespaces: Более продвинутая альтернатива channels для группировки настроек каналов.

Дальше добавляем сервис в docker compose (ваше описание сервисов может отличаться):

dev - stage:

Скрытый текст
services:
	centrifugo:  
	  build:  
	    dockerfile: .docker/centrifugo/Dockerfile  
	    target: dev  
	  container_name: centrifugo.${APP_NAMESPACE}  
	  ports:  
	    - '8089:8000'  
	  networks:  
	    - app  
	  ulimits:  
	    nofile:  
	      soft: 65535  
	      hard: 65535

prod:

Скрытый текст
services:
	centrifugo:  
	  build:  
	    dockerfile: .docker/centrifugo/Dockerfile  
	    target: prod  
	  container_name: centrifugo.${APP_NAMESPACE}  
	  ports:  
	    - '8089:8000'  
	  networks:  
	    - app  
	  ulimits:  
	    nofile:  
	      soft: 65535  
	      hard: 65535

Данный сервис работает на внутреннем 8000 порту поэтому мы открываем доступный вам порт для обращения извне

Дальше мы запускаем наш docker compose и приступаем к добавлению переменных в env и установке sdk библиотеки для Laravel

Устанавливаем sdk который указан в доке - на данный этап официально рекомендуют устанавливать данный репозиторий

Следуем инструкциям по установке из README файла

После установке проверяем что у нас правильно указаны переменные в env

Скрытый текст
BROADCAST_DRIVER=centrifugo  
BROADCAST_CONNECTION=centrifugo

CENTRIFUGO_TOKEN_HMAC_SECRET_KEY="your_secret_key"  
CENTRIFUGO_API_KEY="your_api_key"
  
CENTRIFUGO_URL=http://centrifugo:8000

BROADCAST_DRIVER & BROADCAST_CONNECTION - драйвер и подключение которые мы сгенерировали исходя из README библиотеки

CENTRIFUGO_TOKEN_HMAC_SECRET_KEY - токен из конфига token_hmac_secret_key

CENTRIFUGO_API_KEY - токен из конфига api_key

CENTRIFUGO_URL - указываем наш сервис и его внутренний порт

Теперь мы можем приступить к написанию событий

как работают ивенты и broadcast вы можете узнать из документации

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

1) Пример отправки события через cron

В данном примере мы будем отправлять раз в 5 секунд на публичный канал текущую дату

Пример команды:

Скрытый текст
<?php  
  
declare(strict_types=1);  
  
namespace App\Console\Commands;  
  
use App\Events\ExampleEvent;  
use Illuminate\Console\Command;  
use Symfony\Component\Console\Attribute\AsCommand;  
  
#[AsCommand('example:run')]  
class ExampleCommand extends Command  
{  
    public function handle(): void  
    {  
        ExampleEvent::dispatch();  
    }  
}

Здесь мы вызываем событие на отправку даты

Пример отправки напрямую через sdk:

Скрытый текст
<?php  
  
declare(strict_types=1);  
  
namespace App\Console\Commands;  
  
use Symfony\Component\Console\Attribute\AsCommand;  
use denis660\Centrifugo\Centrifugo;  
use Illuminate\Console\Command;  
  
#[AsCommand('centrifugo:run')]  
class CentrifugoCommand extends Command  
{  
    public function handle(Centrifugo $centrifugo): void  
    {  
        $centrifugo->publish('example', ['time' => now()]);  
    }  
}

Регистрируем класс Centrifugo через аргумент команды и вызываем метод publish

publish - принимает первым аргументом название канала, а второй аргумент это массив с теми данными, которые мы хотим передать в канал

Теперь мы можем зарегистрировать команду и указать в какое время она будет выполняться - пример работы c крон в Laravel

Пример регистрации команды в крон:

Скрытый текст
// routes/console.php
<?php  
  
declare(strict_types=1);  
  
use App\Console\Commands\ExampleCommand;  
use Illuminate\Support\Facades\Schedule;  
  
Schedule::command(ExampleCommand::class)->everyFiveSeconds();

Дальше мы описываем событие, которое будет выполняться в нашей команде

Пример события:

Скрытый текст
<?php  
  
declare(strict_types=1);  
  
namespace App\Events;  
  
use App\Enums\ChannelName;  
use Carbon\Carbon;  
use Illuminate\Bus\Queueable;  
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;  
use Illuminate\Broadcasting\InteractsWithSockets;  
use Illuminate\Foundation\Events\Dispatchable;  
use Illuminate\Broadcasting\Channel;  
  
class ExampleEvent implements ShouldBroadcast  
{  
    use Dispatchable;  
    use InteractsWithSockets;  
    use Queueable;  
  
    public function __construct()  
    {  
    }  
  
    public function broadcastOn(): array  
    {  
        return [  
            new Channel(
	            // используем enum вместо магического значения  
                ChannelName::Example->value  
            ),  
        ];  
    }  
   
    public function broadcastWith(): array  
    {  
        return [  
            'date' => Carbon::now()->format('Y-m-d H:i:s'),  
        ];  
    }  
}

Логика отправки событий на канал готова

Фото ниже описывает авторизацию и подписки на канал, ниже мы можем наблюдать ответы от канала описанной логике - что каждые 5 секунд мы получаем текущее время

Про авторизацию канала и тестирование через postman я опишу ниже

2) Пример отправки сообщения в чат

В данном примере мы разберем отправку ивентов для сообщений месенджера

Пример контроллера на создания сообщения:

Скрытый текст
<?php  
  
declare(strict_types=1);  
  
namespace App\Http\Controllers\Api\Message;  
  
final readonly class MessageController  
{  
    public function __construct(  
        private MessageService $messageService,  
    ) {  
    }  

    public function store(int $chatId, StoreDTO $storeDTO): array  
    {  
        $chat = Chat::query()->findOrFail($chatId);  
        Gate::authorize('show', $chat);  
  
        return $this->messageService->store($chat, $storeDTO);  
    }

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

Пример сервиса для сообщения:

Скрытый текст
<?php  
  
declare(strict_types=1);  
  
namespace App\Services\Message;
  
final readonly class MessageService  
{    
    public function store(Chat $chat, StoreDTO $storeDTO): array  
    {  
        $message = Message::query()  
            ->create([  
                'chat_id' => $chat->id,  
                'user_id' => auth()->id(),  
                'message' => $storeDTO->message,  
            ]);  
  
        MessageCreated::dispatch($message);  
  
        return ShowDTO::from($message)->toArray();  
    }

Здесь мы создаем сообщения в чате, вызываем ивет на создания сообщения и возвращаем данные о созданом сообщении

Пример события для созданного сообщения:

Скрытый текст
<?php  
  
declare(strict_types=1);  
  
namespace App\Events\Message;  
  
use App\DTO\Event\Message\CreateDTO;  
use App\Enums\Channel\ChannelName;  
use App\Enums\Event\MessageEventName;  
use App\Models\Message;  
use Illuminate\Broadcasting\Channel;  
use Illuminate\Broadcasting\InteractsWithSockets;  
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;  
use Illuminate\Foundation\Events\Dispatchable;  
use Illuminate\Queue\SerializesModels;  
  
final class MessageCreated implements ShouldBroadcast  
{  
    use Dispatchable;  
    use InteractsWithSockets;  
    use SerializesModels;  
  
    public function __construct(  
        private readonly Message $message  
    ) {  
    }  
  
    public function broadcastAs(): string  
    {   
	    //enum для названия события
        return MessageEventName::MessageCreated->value;  
    }  
  
    public function broadcastOn(): Channel  
    {  
        return new Channel(
            //enum через который мы создаем имя канала chat.11  
            ChannelName::Chat->byId($this->message->chat_id)  
        );  
    }  
   
    public function broadcastWith(): array  
    {  
        return CreateDTO::from($this->message)->toArray();  
    }  
}

В данном события мы принимаем модель сообщения и выводим массив в канал

Так же обязательно использовать публичный канал и не регистрировать авторизацию канала, почему так - я объясню ниже

Пример который мы получим при отправке сообщения:

При отправки события - его получили все участники чата

Нюансы которые стоит уточнить:

Centrifugo не интегрируется с механизмом авторизации каналов Laravel "из коробки". Вместо этого он использует собственный, более гибкий механизм авторизации на основе JWT-токенов, которые генерируются вашим бэкендом - то есть код ниже не будет работать

Скрытый текст
use App\Models\User;

Broadcast::channel('orders.{orderId}', function (User $user, int $orderId) {
    return $user->id === Order::findOrNew($orderId)->user_id;
});

Приватные каналы в Centrifugo реализуются не через механизм Laravel, а через отправку запроса на ваш бэкенд (процесс подписки) или предварительную генерацию токена с правами доступа

Так же мы не можем через ивенты отправить всем пользователям канала кроме самого отправителя исходя из заключения выше - код ниже не будет работать

Скрытый текст
use App\Events\OrderShipmentStatusUpdated;

broadcast(new OrderShipmentStatusUpdated($update))->toOthers();

Если вы хотите реализовать данную логику придется ее писать самому

Если не хотим реализовывать данную логику, то придется обрабатывать на клиенте пропуск события у самого отправителя

Тестирование WS

Вы можете это делать используя sdk для клиента - примеры или же используя Postman или его аналоги - пример из документации

Для тестирование через Postman надо указать параметр в url cf_ws_frame_ping_pong=true

Ссылка будет выглядеть следующем образом:

ws://localhost:8089/connection/websocket?cf_ws_frame_ping_pong=true

Дальше мы будем описывать взаимодействие с помощью json

Пример авторизации в Centrifugo:

{"id": 1, "connect": { "token": "your_auth_token"}}

id - индетификатор записи

connect - тип действия, указываем при подключении к Centrifugo

token - токен для авторизации пользователя (в примере ниже покажу пример создания токена авторизации)

Пример подписки на канал:

Здесь мы подписываемся на публичный канал public-channel

{"id": 2, "subscribe": {"channel": "public-channel"}}

Для подписки на приватный канал надо указать еще токен авторизации канала

{"id": 2, "subscribe": {"channel": "private-channel", "token": "your_channel_token"}}

id - индетификатор записи

subscribe - тип действия, указываем при подписки на канал в Centrifugo

channel - ключ который содержит имя канала на который хотим подписаться

token - токен для авторизации пользователя (в примере ниже покажу пример создания токена авторизации)

В конечном итоге у вас должно получиться следующее

Дальше мы должны нажать кнопку Connect

После того как мы подключимся надо отправить наши инструкции нажав на кнопку send

После всех шагов мы должны увидеть данные ответы

После отправки событий он авторизует пользователя и подпишется на канал

В случае ошибок вы можете спокойно найти их описание по коду ошибок здесь

Заключение про Centrifugo

В итоге мы имеем что sdk нам дает поддержку событий - отправляем ивенты данные на указанный канал - а вся остальная логика ложится на centrifugo

Нам надо будет самим написать способы авторизации нашего бэкенд приложения и centrifugo - он умеет поддерживать авторизацию как через сессии так и через токен

Пример создания токена для авторизации в Centrifugo:

Пример контроллера авторизации:

Скрытый текст
<?php  
  
declare(strict_types=1);  
  
namespace App\Http\Controllers\Api;  
  
final readonly class AuthController  
{  
    public function __construct(  
        private AuthService $authService  
    ) {  
    }  
     
    public function WSAuth(): array  
    {  
        $user = User::query()->findOrFail(auth()->id());  
  
        return $this->authService->WSAuth($user);  
    }

Пример Сервиса AuthService:

Скрытый текст
<?php  
  
declare(strict_types=1);  
  
namespace App\Services\Auth;  
  
final readonly class AuthService  
{  
    public function __construct(  
        private Centrifugo $centrifugo,  
    ) {  
    }  
     
    public function WSAuth(User $user): array  
    {  
        $token = $this->centrifugo->generateConnectionToken((string) $user->id);  
  
        return new TokenDTO($token)->toArray();  
    }

Здесь мы используем класс Centrifugo который предоставляем нам sdk и вызываем у него метод generateConnectionToken который принимает строку UserId, в конце я вывожу сам токен

Уже после мы можем на клиента обрабатывать эту ручку и сгенерированный токен использовать для авторизации в Centrifugo

Приватные каналы:

Исходя из того что мы не можем использовать приватный каналы laravel, мы их должны защитить другим способом

Для этого Centrifugo предоставляет API чтобы авторизовывать клиента в приватный канал через токен - токен мы сгенерируем в нашем бэкенд приложении

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

Скрытый текст
public function generatePrivateToken(User $user): array {  
    $token = $this->centrifugo  
        ->generatePrivateChannelToken(  
            (string) $user->id,  
            'your_private_channel'  
        );  
  
    return new TokenDTO($token)->toArray();  
}

По аналогии с авторизацией пользователя в Centrifugo мы можем сгенерировать для приватного канала токен

Так же Centrifugo умеет подписывать авторизованного пользователя на каналы и отписывать (работает как для публичных так и для приватных каналов) - что позволяет нам более гибко управлять процессом

Пример подписки на канал

Покажу пример подписки пользователя на все свои чаты

Пример контроллера:

Скрытый текст
<?php  
  
declare(strict_types=1);  
  
namespace App\Http\Controllers\Api\WS;  
  
final readonly class WSController  
{  
    public function __construct(  
        private WSService $wsService,  
    ) {  
    }  
    
    public function subscribe(): Response  
    {  
        $user = auth()->user();  
  
        $this->wsService->subscribe($user);  
  
        return response()->noContent();  
    }  
}

Здесь мы получаем авторизованного пользователя и вызываем WSService

Пример сервиса WSService:

Скрытый текст
<?php  
  
declare(strict_types=1);  
  
namespace App\Services\WS;  
  
final readonly class WSService  
{  
    public function __construct(  
        private Centrifugo $centrifugo,  
        private ChannelService $channelService,  
    ) {  
    }  
  
    public function subscribe(User $currentUser): void  
    {  
        $chatChannels = $this->channelService->chats($currentUser);  
  
		foreach ($chatChannels as $channel) {  
		    $this->centrifugo->subscribe(  
		        $channel->name,  
		        (string) $currentUser->id  
		    );  
		}
    }  
}

Из channelService мы получаем его текущие чаты в уже в цикле мы подписываем каждый чат на пользователя используя метод subscribe

Пример отписки от канала

Здесь в примере мы отпишем при удалении группы каждого участника от Centrifugo

Скрытый текст
public function unsubscribe(Chat $group, Collection $members): void  
{  
    $members->each(function (User $member) use ($group): void {  
        $this->centrifugo->unsubscribe(  
            ChannelName::Chat->byId($group->id),  
            (string) $member->id  
        );  
    });  
}

Итог:

Мы с вами настроили и развернули Centrifugo в laravel проектe, показали вам примеры как можно с ним работать и так же добавил его в [шаблон](вставить ссылку), чтобы вы могли легко использовать его и начинать свои прекрасные проекты

GitHub - буду рад вашей подписки на меня в гитхабе

Благодарю вас, что прочитали данную статью

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