Использование Laravel с socket.io

Использование Laravel с socket.io

Вы давно планировали изучить вебсокеты, и хотели поднабраться опыта в написании приложений с использованием вебсокетов? Это отличный выбор!
Вебсокеты - это мощный и полезный инструмент, который выводит веб-приложения на новый уровень. Эта технология является первым помощником при реализации "real-time" приложений.
На примере сокетов можно создать: чат без перезагрузки страницы, личные переписки с мгновенными оповещениями, прямые трансляции и много других крутых вещей.

План

  1. Создание события
  2. Настройка Redis
  3. Установка Laravel-Echo
  4. Запуск Laravel Echo сервера
  5. Настройка supervizor
  6. Установка и настройка horizon
  7. Установка Laravel Echo и Socket.io клиента
  8. Клиентский код вещания
  9. Итоговая проверка результата
  10. Резюме

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

Как только я начинал знакомиться с этой прекрасной технологией, то впервые я применил вебсокеты с использованием сервиса pusher. Большинство туториалов для laravel, как раз таки и написаны на примере этого сервиса. Однако...

Почему не Pusher?

Laravel устанавливается настроенным под pusher. В чём прелесть его применения, так это, в возможности использования с минимум настроек. Всё просто: создай аккаунт на pusher.com, подключи нужные компоненты и у тебя рабочие сокеты.

Но, как и во всех платных сервисах, бесплатный тариф накладывает некоторые ограничения, какие, можете посмотреть на странице

И второе - когда я интересовался этой темой, большинство инструкций иллюстрировали использование Pusher. Что ещё больше побуждает меня написать статью о том, как использовать laravel socket.io.

Это не призыв к полному отказу от Pusher. Его оптимально использовать, когда вы делаете прототип сайта, и вам нужно быстро создать его, без долгих настроек. В других случаях, следует доверять только своему железу, и своим сервисам, потому, для обеспечения большей гибкости сокетов стОит перейти на более тяжеловесные альтернативы.

Что мы получим?

Используя socket.io мы не будем ограничены количеством возможных подключений. Теперь всё, что нас ограничивает - это выделенные ресурсы используемого железа. Потому, это не просто переход на "своё", а, дополнительно, ещё один шаг к легкому масштабированию нашей системы.

Давайте начнём!

Я пользователь windows, потому я буду использовать laravel-homestead. Если вы ещё не знакомы с Vagrant-ом, то советую почитать простую инструкцию по его установке.

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

Перед написанием инструкции, хочу повторить важные вещи, которые нужно знать:

  • Для того, чтобы laravel-события вещались, класс события должен реализовывать интерфейс ShouldBroadcast
  • Аутентификация в защищенные каналы производится в файле routes/channels.php
  • Публичные каналы - слушать может каждый посетитель
  • Приватные каналы - слушать может только аутентифицированный пользователь
  • Канал присутствия - аналогичный приватному, только в нём доступно больше метаинформации о канале, и есть возможность получить список подключённых пользователей методом события channel.broadcastOn()

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

Создадим первое событие:
php artisan make:event TranslationEvent

И укажем, что это событие является реализацией ShouldBroadcast интерфейса
first_event-3

В это событие будем передавать id трансляции, и сообщение, которое добавится в трансляцию. Так же, в методе broadcastOn вернём экземпляр класса Channel, который создаёт публичный канал.

Настройка Redis

В сборке Homestead Redis уже установлен, нам нужно только указать в конфигах, что мы собираемся использовать Redis в качестве драйвера очередей и событий.
Для этого нужно в .env файла BROADCAST_DRIVER указать redis, и QUEUE_DRIVER, так же redis.

Если вы ещё не знакомы с Redis, то советую ознакомиться с этой технологией в данной статье.

Итого должно получится:
env
Для простоты управления и визуального отображения redis-очередей, установим Horizon. Это не обязательно, однако, это сделает процесс более очевидным и понятным.

Установка Laravel-Echo

В этом пункте устанавливаем socket.io сервер, который идёт в комплекте с Laravel Echo

Laravel Echo нужно установить глобально, выполнив:
npm install -g laravel-echo-server
Если этой командой не удалось установить эту библиотеку, то попробуйте выполнить эту команду от имени root-пользователя:
sudo npm install -g laravel-echo-server

После установки laravel-echo-server нужно запустить скрипт инициализации конфигов:
laravel-echo-server init

Просто нажимая enter на все вопросы:
echo-server-init-1

После чего, в корне проекта появится laravel-echo-server.json - файл, который содержит основные настройки socket сервера.

Примите во внимание, что, в случае, если вы захотите перенести этот проект на боевой сервер, то убедитесь, что вы добавили laravel-echo-server.json в .gitignore, и сгенерируйте его заново, уже на новом сервере. А иначе, постоянно придётся изменять authHost в конфиге.

Запуск Laravel Echo сервера

Для запуска вебсокетов выполним команду:
laravel-echo-server start

И должны увидеть:
run-server

Настройка supervizor

Теперь, наши сокеты доступны, и к ним можно безпроблемно подключаться. Однако, бывают случаи, когда сокеты могут отключиться, в результате какой-то ошибки. И они так и буду отключены до тех пор, пока мы не включим их вручную.
Правда, вручную не придётся, потому как за нас это будет делать supervisor. Supervison - это программа, который следит за указанными процессами, и, если какой-то отключается, он, мгновенно пытается его включить заново.

Для того, чтобы создать задачу в supervisor-е, нужно в его папке /etc/supervisor/conf.d/ создать новый файл laravel-echo.conf

Для того, чтобы выйти из процесса запущенного websocket-a, нажмите Ctrl + C

И выполните команду:
sudo nano /etc/supervisor/conf.d/laravel-echo.conf

Которая создаст новый файл, куда поместите такое содержимое:

[program:laravel-echo]
directory=/home/vagrant/code/
process_name=%(program_name)s_%(process_num)02d
command=laravel-echo-server start
autostart=true
autorestart=true
user=vagrant
numprocs=1
redirect_stderr=true
stdout_logfile=/home/vagrant/code/storage/logs/echo.log

После нажнимате Ctrl + X подтвердите выбор клавищей Y и enter.

Будьте внимательны, при создании supervisor-файла. Вам нужно указать свои данные:
directory - которая указывает на папку проекта (можно узнать, выполнив команду pwd внутри проекта),
stdout_logfile - который указывает на лог-файл вашего проекта,
user - логин текущего пользователя.

Если для созданий файла вы используете vim, то, для выхода из редактирования файла нажмите Ctrl + C и потом напишите :quit

Установка и настройка horizon

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

Для его установки нужно выполнить:
composer require laravel/horizon
А, после:
php artisan vendor:publish --provider="Laravel\Horizon\HorizonServiceProvider"

И в итоге, из консоли мы сможем выполнить команду php artisan horizon, которая запустит процесс, и будет обрабатывать все наши события. Однако, чтобы постоянно не держать консоль открытой с запущенным процессом horizon, аналогично предыдущему пункту - настроим supervizor на этот процесс:

Создадим новый конфиг:
sudo nano /etc/supervisor/conf.d/horizon.conf

С таким содержимым:

[program:horizon]
process_name=%(program_name)s
command=php /home/vagrant/code/artisan horizon
autostart=true
autorestart=true
user=vagrant
redirect_stderr=true
stdout_logfile=/home/vagrant/code/storage/logs/horizon.log

И теперь, перейдя по адресу http://yourdomain/horizon, увидим удобную панельку, показывающую информацию о всех событиях:
horizon

Запуск команд supervizor-a

Сначала нужно остановить все задачи:
sudo supervisorctl stop all
Потом перечитать все команды (найти новые):
sudo supervisorctl reread

Если всё правильно - должны получить такое сообщение:
newreread-2

И, осталось запустить supervizor:
sudo supervisorctl reload

После чего, чтобы удостовериться, что наш процесс работает, выполним:
sudo supervisorctl status

И если вы получили что-то похоже на это, то вы всё сделали верно:
superciso-status

Установка Laravel Echo и Socket.io клиента

Для установки Laravel-echo библиотеки выполним:
npm install --save laravel-echo
И для socket.io:
npm install --save socket.io-client

И теперь, смело можно регистрировать socket.io библиотеку в нашем bootstrap.js (resorces/js/bootstrap.js)

import Echo from "laravel-echo"
window.io = require('socket.io-client');
// Have this in case you stop running your laravel echo server
if (typeof io !== 'undefined') {
  window.Echo = new Echo({
    broadcaster: 'socket.io',
    host: window.location.hostname + ':6001',
  });
}

Клиентский код вещания

Ранее, нами уже было создано событие TranslationEvent, которое должно транслировать сообщения в наш публичный канал.

Теперь, для облегчения отладки и тестирования - создадим консольную команду, которая будет вызывать событие, транслирующее сообщения в наш чат.
Выполним команду
artisan make:comand TranslationMessage

Которую настроем под себя

class TranslationMessage extends Command
{
    protected $signature = 'translation:messages {id} {message}';
    protected $description = 'Fire event';

    public function __construct()
    {
        parent::__construct();
    }

    public function handle()
    {
        event(new TranslationEvent(
            $this->argument('id'),
            $this->argument('message'))
        );
    }
}

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

Потому, первое, что нам нужно сделать - это создать соответствующий контроллер, и маршрут для него.

Создадим контроллер:
artisan make:controller TranslationController

Внутри которого нужно добавить метод:

public function index($id)
{
    return view('translation.index', ['id' => $id]);
}

И, соответсвенно, создать вид index в папке translation с содержимым:

@extends('layouts.app')
@section('content')
    <div class="container">
        <div class="row justify-content-center">
            <div class="col-md-8">
                <div class="card">
                    <div class="card-header">Translation <span class="btn btn-primary">{{ $id }}</span></div>

                    <div class="card-body">
                        <translation-component
                                :translation_id="{{ $id }}"
                        ></translation-component>
                    </div>
                </div>
            </div>
        </div>
    </div>
@endsection

В папке resources/assets/js/components создадим компонент под названием TranslationComponent.vue

<template>
    <div class="alert alert-warning">
        <div v-if="messages.length < 1">
            Beginning translation...
        </div>
        <div v-else>
            <ul class="list-group" v-for="message in messages">
                <li class="list-group-item">
                    {{ message.message }}
                </li>
            </ul>
        </div>
    </div>
</template>

<script>
    export default {
        props: ['translation_id'],
        data() {
            return {
                messages: []
            }
        },

        created() {
            Echo.channel(`translation.${this.translation_id}`)
                .listen('TranslationEvent', (e) => {
                    this.messages.push(e)
                });
        }
    }
</script>

<style scoped></style>

А resources/assets/js/app.js зарегистрируем этот компонент:

Vue.component('translation-component', require('./components/TranslationComponent.vue'));

И маршрут, по которому трансляции будут доступны:

Route::get('/translation/{id}', 'TranslationController@index');

Для того, чтобы js-файлы скомпилировались, и Vue-компонент стал доступен, нужно выполнить команду npm run dev, которая соберёт все клиентские скрипты.

Проверка результата

И теперь, если перейти по нужному маршруту, http://yourdomain.test/translation/1 (или любое другое число, вместо 1), то должны получить:
result

И, выполнив в консоли ранее созданную команду: artisan translation:messages 1 'Hello translation'

И, ожидаемо, получим:
resulur2

Что подтверждает, что всё было настроено правильно:
result-all

Резюме

Статья получилась объёмной, но, надеюсь, что интересной и полезной. В этой статье было разобрано, как подключать и работать с socket.io, затронули тему добавления и управления процессами в supervizor, и вещание laravel в публичные каналы. Так же, здесь, неявно было показано использование redis, в нашем случае - для реализации "общения" с сокетами.