Используем Laravel с Socket.IO

Опубликовано 3 комментариев 681 просмотров
Используем Laravel с Socket.IO

Сегодня, когда интернет полон real-time приложений довольно трудно (да и зачем?) игнорировать такой протокол, как вебсокеты. Он используется для получения пользователем информации с веб-сервера с минимальной задержкой (ч.к. в режиме реального времени).

Цель данного мануала - рассказать, как быстро и без проблем интегрировать свое приложение на Laravel с вебсокетами, используя доступный функционал фреймворка и JS-движок Socket.IO.

Все это будет описано на собственном примере. В моем случае есть комнаты (tasks), при открытии которых и наличии на это прав пользователь может получать сообщения, отправляемые в данную комнату другими пользователями и отправлять свои. Короче, простой чат с приватными комнатами.

Первичная настройка

Первым делом нам необходим Redis для трансляции событий в каналы. Установка не совсем подходит к тематике этой статьи и во многом зависит от используемой ОСи, так что я предположу что на сервере он уже присутствует. Доустановим пакет Predis, необходимый фреймворку для работы с хранилищем.

$ composer require predis/predis

Добавим/обновим переменные в файле .env.

BROADCAST_DRIVER=redis

REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379

Подключим сервис-провайдер BroadcastServiceProvider. Для этого найдем и раскомментируем в config/app.php строку

// App\Providers\BroadcastServiceProvider::class,

Установка JS-библиотек

На стороне клиента будут задействованы две библиотеки - Laravel Echo и Socket.IO.

$ npm i --save laravel-echo socket.io-client

Установим их и пока что отложим на потом.

Создание канала

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

$ php artisan make:channel MessagesChannel

В классе канала нам потребуется метод join, возвращающий true если пользователь имеет право на прослушку канала, иначе false.

<?php

namespace App\Broadcasting;

use App\User;

class MessagesChannel
{
    /**
     * @param User $user
     * @param int $task_id
     * @return bool
     */
    public function join(User $user, int $task_id)
    {
        return true || false;
    }
}

В качестве первого аргумента будет передана модель User с данными о текущем пользователе. Второй аргумент - переменная с целым числом $task_id, о появлении которой будет рассказано ниже.

Откроем файл routes/channels.php. Добавим

Broadcast::routes(['middleware' => ['web', 'auth']]);
Broadcast::channel('chat.{task_id}', \App\Broadcasting\MessagesChannel::class); 

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

Вторая строка - созданный ранее канал. Первый аргументом идет название канала, при этом значение {task_id} будет передано в аргументы метода join.

Можно не создавать класс канала, ограничившись анонимной функцией.

Broadcast::channel('chat.{task_id}', function(\App\User $user, int $task_id) {
    return true || false;
}); 

Создание события

Отправка события будет вызываться в момент совершения пользователем необходимого действия - отправки сообщения в чат. Это событие будет отправлено другим пользователям прослушивающим указанный в нем канал и самому отправителю (последнее опционально, подробнее дальше).

$ php artisan make:event ChatMessage
<?php

namespace App\Events;

use App\Models\Comment;
use Illuminate\Queue\SerializesModels;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;

class ChatMessage implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    protected $comment;

    public function __construct(Comment $comment)
    {
        $this->comment = $comment;
    }

    /**
     * Get the channels the event should broadcast on.
     *
     * @return \Illuminate\Broadcasting\Channel|array
     */
    public function broadcastOn()
    {
        return new PrivateChannel('chat.'.$this->comment->task_id);
    }

    public function broadcastWith()
    {
        return [
            'view' => view('tasks.comments.single', ['i' => $this->comment])->render()
        ];
    }
}

Подробней о доступных методах:

  • __construct вызывается при создании события. Содержит аргументы, указанными нами в момент отправки события.
  • broadcastOn возвращаем класс или массив классов PrivateChannel с переданным в качестве аргумента названием канала. У нас это - channel.{task_id}, где {task_id} - айдишник комнаты (канала), куда будет отправлено сообщение. Кроме PrivateChannel существуют Channel (публичные каналы) и PresenceChannels (что-то вроде PrivateChannel , но обладающим большим функционалом). О них можно больше узнать в документации.
  • broadcastAsПри трансляции события Laravel использует название класса как идентификатор события для его обработки на стороне клиента. Метод дает возможность указать свое название.
  • broadcastWith По умолчанию Laravel сериализует все публичные (public) свойства класса и передает их клиенту. Это можно изменить, вернув необходимую информацию этим методом.
  • broadcastWhen Если возникла необходимость передать событие только при исполнении какого-либо условия, то объявление данного метода поможет в решении задачи. Возвращает true если условие соблюдено, иначе false.
  • $broadcastQueue - публичная переменная. Может быть указана в том случае, если существует необходимость использовать очередь отличную от стандартной, указанной в config/queue.php.

В __construct я ожидаю модель Comment. Указываю в broadcastOn созданный ранее канал, заменяя {task_id} на ID существующей комнаты, к которой привязано сообщение. Кастомизирую отправляемую информацию при помощи broadcastWith, отправляя пользователям только отрендеренный шаблон с сообщением.

Отправка события

Существует две функции - event и broadcast. Обе в качестве аргумента ожидают экземпляр ранее добавленного события ChatMessage, унаследованного от ShouldBroadcast. Обе делают одно и то же - отправляют событие клиентам, прослушивающим канал. Отличие только одно - в broadcast доступен метод toOthers, позволяющий исключить из списка получателей текущего пользователя. Я использую вторую функцию, т.к. в моей вариации чата события, происходящие на стороне клиента в момент после отправки сообщения текущим пользователем и другими пользователями несущественно отличаются.

<?php

namespace App\Http\Controllers;

use App\Events\ChatMessage;
use App\Models\Comment;

class TasksController extends Controller
{
    public function comment(Request $request)
    {
        /**
         * Валидация. Добавляю сообщение в базу,
         * получаю модель Comment $comment с сообщением
         */

        event(new ChatMessage($comment)); // Это для примера. Отправка сообщения всем активным пользователям канала
        broadcast(new ChatMessage($comment))->toOthers(); // Отправляю сообщение всем, кроме текущего пользователя
    }
}

Установка и настройка Laravel Echo Server. Автозапуск сервера

Laravel Echo Server - сервер Node.js, используемый Laravel для трансляции событий Socket.IO. Установим его.

$ npm i -g laravel-echo-server pm2

Кроме сервера так же мы установили PM2 - удобный менеджер процессов Node.js. Он потребуется для автозапуска и перезагрузки нашего сервера Echo в случае ошибки. Инициализируем Laravel Echo Server.

$ laravel-echo-server init

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

$ laravel-echo-server init
? Do you want to run this server in development mode? No
? Which port would you like to serve from? 6001
? Which database would you like to use to store presence channel members? redis
? Enter the host of your Laravel authentication server. http://localhost
? Will you be serving on http or https? http
? Do you want to generate a client ID/Key for HTTP API? Yes
? Do you want to setup cross domain access to the API? No
appId: APP_ID
key: APP_KEY
Configuration file saved. Run laravel-echo-server start to run server.

В папке приложения появится файл laravel-echo-server.json примерно следующего содержания:

{
   "authHost": "http://localhost",
   "authEndpoint": "/broadcasting/auth",
   "clients": [
      {
         "appId": "APP_ID",
         "key": "APP_KEY"
      }
   ],
   "database": "redis",
   "databaseConfig": {
      "redis": {},
      "sqlite": {
         "databasePath": "/database/laravel-echo-server.sqlite"
      }
   },
   "devMode": false,
   "host": null,
   "port": "6001",
   "protocol": "http",
   "socketio": {},
   "sslCertPath": "",
   "sslKeyPath": "",
   "sslCertChainPath": "",
   "sslPassphrase": "",
   "apiOriginAllow": {
      "allowCors": false,
      "allowOrigin": "",
      "allowMethods": "",
      "allowHeaders": ""
   }
}

В случае, если вы используете HTTPS и указали этот момент при создании конфига так же будет необходимо указать полный путь до сертификата и ключа.

Можно попробовать запустить сервер, введя в консоли laravel-echo-server start. В случае успешного запуска вы получите сообщение

$ laravel-echo-server start

L A R A V E L  E C H O  S E R V E R

version 1.3.6

Starting server...

✔  Running at localhost on port 6001
✔  Channels are ready.
✔  Listening for http events...
✔  Listening for redis events...

Server ready!

Теперь отдадим управление сервером Echo, - запуск при перезагрузке сервера, ребут скрипта в случае его падения, - в руки PM2. Cоздадим файл laravel-echo-server.js.

let echo = require('laravel-echo-server');

echo.run(
    // Конфиг из laravel-echo-server.json
);

Добавим скрипт в PM2 и добавим менеджер в автозагрузку при старте системы.

$ pm2 startup
$ pm2 start laravel-echo-server.js
[PM2] Starting /LARAVEL_DIR/laravel-echo-server.js in fork_mode (1 instance)
[PM2] Done.

Прослушивание канала и получение новых сообщений

Создавать в resources/assets/js новый файл JS или использовать app.js - выбор ваш. Открываем. Подключаем библиотеки, начинаем прослушку.

import Echo from "laravel-echo"
window.io = require('socket.io-client');

let echo = new Echo({
    broadcaster: 'socket.io',
    host: 'http://localhost:6001' // значение должно быть равным authHost из конфига + порт
});

echo
    .private(`chat.${comments.channel}`)
    .listen('ChatMessage', (e) => {
        /**
         * Действия, происходящие при получении события клиентом
         * напр. console.log(e);
         */
        comments.list.find('ul > li.empty').remove();
        comments.list.find('ul').append(e.view);
        comments.count.text(parseInt(comments.count.text()) + 1);
        comments.list.scrollTop(9999999999);
        comments.sound.play();
    });

chat.${comments.channel} - название канала. В переменной comments.channel содержится ID канала, а ChatMessage - название события.

Заключение

Как видите, работа Laravel c вебсокетами довольна проста в освоении. Сам я потратил несколько часов, чередуя чтение официальной документации и первой страницы гугла по запросу в духе "laravel socket.io", полную устаревших или неполных данных. Надеюсь, я достаточно подробно объяснял все этапы. Если у вас возникли вопросы или вы заметили неточность - форма для добавления комментариев ниже :)

  • wisp

    я испольщую Supervisor для поддержки процесса в рабочем состоянии.

    command=/path/to/node /path/to/laravel-echo-server start --dir=/path/to/project_root

  • Vlad Batenko Igor

    Как сделать чат с отправкой сообщения отправлялось и события но без использования ajax? а именно человек пишет сообщение нажимает ентер, события пробрасывается в laravel там происходит запись в базу и возвращение данных всем пользователям? а то по всей сети все делают через ajax

  • gtxtymt

    Vlad Batenko Igor,

    setInterval и чекаем бд на новые записи. но не знаю, "по всей сети все делают через ajax" - очень сомнительное утверждение

В ответ на сообщение

Доступна разметка Markdown. А еще вы можете использовать крутой пак эмоций.

Нажимая на кнопку «Отправить» вы даете свое согласие на обработку персональных данных в соответствии с законом №152-ФЗ «О персональных данных» от 27.07.2006 и принимаете условия Политики конфеденциальности.