В этой статье мы рассмотрим интеграцию сервера реального времени Centrifugo с фреймворком Laravel, основные настройки и нюансы работы
Данная статья будет больше про реализацию на самом фреймворке чем описание Centrifugo.
Так же пример взаимодействия вы можете найти в данном шаблоне который описывал в статье про frankenphp+laravel
Centrifugo – это сервер для работы в реальном времени, который поддерживает различные транспорты для подключения клиентов, включая WebSocket, HTTP-streaming, Server-Sent Events (SSE) и другие. Он использует publish-subscribe паттерн для обмена сообщениями`
Он выступает промежуточным звеном (прокси) между вашим бэкендом и клиентами (веб, мобильные приложения). Его главная задача — доставлять сообщения от сервера к клиентам и между клиентами мгновенно.
Что он умеет:
Односторонняя рассылка (Push-уведомления): Сервер может отправить сообщение всем подписанным клиентам или конкретным пользователям.
Двустороннее общение (Pub/Sub): Клиенты могут подписываться на каналы (топики) и публиковать в них сообщения, которые получат все остальные подписчики (если есть права).
Масштабирование: Легко запускается в кластере (через Redis или Tarantool), чтобы выдерживать нагрузку от миллионов соединений.
Надёжность: Восстанавливает потерянные соединения и доставляет сообщения, которые были отправлены, пока клиент был оффлайн (персистентные каналы).
Проверка прав доступа: Запросы на подключение и публикацию всегда идут через ваш бэкенд для авторизации, что обеспечивает безопасность.
Ключевая философия:
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 - буду рад вашей подписки на меня в гитхабе
Благодарю вас, что прочитали данную статью