Produced by FOURIER

Laravel × Redis × Docker での Websocketリアルタイム通信

IchikawaIchikawa calender 2022.12.22

はじめに

普段みなさんが使っているSNS等のアプリには、フォローしたユーザーがメッセージの送信や投稿をすると、リアルタイムで通知がされるようになっていると思います。

今では、SNSにおいてリアルタイム通信というのは、必須といえる機能となりUX(ユーザー体験)をよりよくしてくれます。

Webアプリにおけるリアルタイム通信といえば、WebSocketWebRTCが思い浮かびますよね。

ただ、私自身はWebSocketWebRTCを使った開発経験はなく、ちょうどブログのネタも探していたので、Laravel × Redis × Docker という構成でWebSocketによる双方向通信を実装していこうと思います。

目標

ブラウザAからメッセージを送信すると、サーバーサイドでイベントが発火し、WebSocketで接続されている別のブラウザBがデータを受信し、コンソールにメッセージが表示されるというところをゴールとして実装していきます。

完成形のコードは、以下リポジトリの feature/websocket ブランチにあります。

fourierLab/techblog-laravel-redis-docker-websocket

Contribute to fourierLab/techblog-laravel-redis-docker-websocket development by creating an account on GitHub.

https://github.com/fourierLab/techblog-laravel-redis-docker-websocket

完成形のコードを確認したい方は、以下コマンドでGitクローンしてください。

git clone -b feature/websocket https://github.com/fourierLab/techblog-laravel-redis-docker-websocket techblog-laravel-redis-docker-websocket

環境構築

まずは環境構築をしていきます。すでにLaravelが動作する環境があれば不要ですが、docker-compose.ymlやDockerfileの設定、コンテナの再ビルドは忘れないよう注意していください。

Laravel 環境の準備

1. Gitクローン

こちらのリポジトリから環境構築のためのコードを取得します。上述したリポジトリと同じです。

fourierLab/techblog-laravel-redis-docker-websocket

Contribute to fourierLab/techblog-laravel-redis-docker-websocket development by creating an account on GitHub.

https://github.com/fourierLab/techblog-laravel-redis-docker-websocket

master ブランチ → Dockerfileの設定とLaravelの準備まで完了しています

feature/websocket ブランチ → WebSocketを使ったリアルタイム通信の実装が完了しています

git clone https://github.com/fourierLab/techblog-laravel-redis-docker-websocket techblog-laravel-redis-docker-websocket

2. ディレクトリの移動

cd techblog-laravel-redis-docker-websocket

3. dockerコンテナのビルド

docker compose build --no-cache

4. dockerコンテナの起動

docker compose up -d

5. laravelの初期設定

docker compose exec app bash
composer install
cp .env.example .env
php artisan key:generate
php artisan migrate

上記のコマンドをすべて実行したら、http://localhost にアクセスしてください。

以下の画面が表示されれば環境構築完了です。

Dokcer周りの設定

WebSocket通信を実現する上でポイントとなるDockerの設定ファイルを紹介します。

すでにLaravelが動作する環境があり、「Laravel 環境の準備」をやっていない方は、Dockerfileの追加やdocker-compose.ymlの調整をお願いします。

laravel-echo-serverのDockerfile

socket.ioで作られたLaravelWebSocket通信(ブロードキャスト)するためのサーバー(Nodejs)です。

laravel-echo-server start のコマンドで起動して、6001ポートでWebSocket接続を受けます。

FROM node:16.15.1-alpine
RUN npm install -g laravel-echo-server
WORKDIR /work
CMD ["laravel-echo-server", "start"]

php-fpmのDockerfile

Redisブロードキャスタを使用するので、peclを使い phpredis をインストールします。

Laravelで発火したイベントを受け取り、Redisのpub/sub機能を使い、laravel-echo-server を介することでクライアント(ブラウザ)にデータを送ります。

FROM php:8.1-fpm

# php設定コピー
COPY ./docker/app/php.ini /usr/local/etc/php/php.ini
COPY ./docker/app/php-fpm.d/zzz-docker.conf /usr/local/etc/php-fpm.d/zzz-docker.conf

# パッケージインストール
RUN apt update && \
    apt -y install \
    git \
    zip \
    unzip \
    vim \
    && docker-php-ext-install opcache \
    && docker-php-ext-install pdo_mysql bcmath

# Composer
COPY --from=composer:2.1 /usr/bin/composer /usr/bin/composer

# phpredis // インストールを忘れすに!!
RUN pecl install redis \
    && docker-php-ext-enable redis

# Node.js
COPY --from=node:16.15.1 /usr/local/bin /usr/local/bin
COPY --from=node:16.15.1 /usr/local/lib /usr/local/lib

WORKDIR /var/www/html

docker-compose.yml

redisコンテナやecho-serverコンテナの設定を確認してください。

version: "3.8"

services:
  app:
    container_name: laravel-websocket-app
    build:
      context: .
      dockerfile: ./docker/app/Dockerfile
    restart: always
    volumes:
      - type: bind
        source: ./src
        target: /var/www/html
      - type: volume
        source: php_fpm_socket
        target: /var/run/php-fpm

  web:
    container_name: laravel-websocket-web
    build:
      context: .
      dockerfile: ./docker/web/Dockerfile
    restart: always
    volumes:
      - type: bind
        source: ./src
        target: /var/www/html
      - type: volume
        source: php_fpm_socket
        target: /var/run/php-fpm
    ports:
      - "${WEB_PORT}:80"
    depends_on:
      - app

  db:
    container_name: laravel-websocket-db
    build:
      context: .
      dockerfile: ./docker/mysql/Dockerfile
    restart: always
    volumes:
      - type: volume
        source: mysql_volume
        target: /var/lib/mysql
    ports:
      - ${DB_PORT}:3306
    environment:
      MYSQL_DATABASE: ${DB_DATABASE}
      MYSQL_USER: ${DB_USERNAME}
      MYSQL_PASSWORD: ${DB_PASSWORD}
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}

  redis:
    container_name: laravel-websocket-redis
    image: "redis:latest"
    restart: always
    volumes:
      - type: volume
        source: redis_data 
        target: /data
    ports:
      - "6379:6379"

  echo-server:
    container_name: laravel-websocket-echo-server
    build: ./docker/echo-server
    restart: always
    volumes:
      - type: bind
        source: ./src
        target: /work
    ports:
      - "6001:6001"

volumes:
  php_fpm_socket:
  mysql_volume:
  redis_data:

networks:
  default:
    name: laravel-websocket-network

laravel-echo-serverの設定

初回コンテナ起動時には、laravel-echo-server の設定が済んでいないので、エラーが発生してecho-serverのコンテナが起動できません。

以下のコマンドを実行して、初期設定をします。

docker compose run echo-server laravel-echo-server init

設定は以下の通りです。

? Do you want to run this server in development mode? Yes
? 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? No
? Do you want to setup cross domain access to the API? No
? What do you want this config to be saved as? laravel-echo-server.json
Configuration file saved. Run laravel-echo-server start to run server.

入力が終わると、Laravelプロジェクトの直下に laravel-echo-server.json というファイルが生成されます。

生成されたファイルを編集します。redis の設定に以下を追記してください。

"databaseConfig": {
        "redis": {
            "host": "redis",
            "port": 6379
        },
  ...
},

最終的には、以下のようになります。

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

以上で、laravel-echo-server の設定は完了です。

dockerのコンテナを確認すると、echo-serverのコンテナが問題なく起動されていると思います。

$ docker compose ps
NAME                           COMMAND                 SERVICE      STATUS   PORTS
... 
laravel-websocket-echo-server  "docker-entrypoint.s…"  echo-server  running  0.0.0.0:6001->6001/tcp

また、laravel-echo-server が正しく起動されると、laravel-echo-server.lock ファイルも生成されるので確認してください。

redisの設定

.env を以下のように編集します。

BROADCAST_DRIVER=redis  // logからredisに変更
CACHE_DRIVER=file
FILESYSTEM_DISK=local
QUEUE_CONNECTION=redis  // syncからredisに変更、dockerコンテナ名(redis)
SESSION_DRIVER=file
SESSION_LIFETIME=120

REDIS_HOST=redis        // dockerコンテナ名(redis)
REDIS_PASSWORD=null
REDIS_PORT=6379
REDIS_CLIENT=phpredis   // appコンテナにインストールされたphpredisを使用
REDIS_PREFIX=""

./src/config/app.php を開き、App\Providers\BroadcastServiceProvider::class のコメントアウトを外します。

App\Providers\BroadcastServiceProvider::class, // コメントアウトを外す

特に設定を変更する必要はありませんが、./src/config/database.phpRedis周りの設定は以下のようになっています。

'redis' => [

    'client' => env('REDIS_CLIENT', 'phpredis'),

    'options' => [
        'cluster' => env('REDIS_CLUSTER', 'redis'),
        'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'),
    ],

    'default' => [
        'url' => env('REDIS_URL'),
        'host' => env('REDIS_HOST', '127.0.0.1'),
        'username' => env('REDIS_USERNAME'),
        'password' => env('REDIS_PASSWORD'),
        'port' => env('REDIS_PORT', '6379'),
        'database' => env('REDIS_DB', '0'),
    ],

    'cache' => [
        'url' => env('REDIS_URL'),
        'host' => env('REDIS_HOST', '127.0.0.1'),
        'username' => env('REDIS_USERNAME'),
        'password' => env('REDIS_PASSWORD'),
        'port' => env('REDIS_PORT', '6379'),
        'database' => env('REDIS_CACHE_DB', '1'),
    ],

],

ライブラリのインストール

appコンテナ内でライブラリをそれぞれインストールをしてください。

appコンテナには、ルートディレクトリから以下のコマンドを実行することで接続できます。

docker compose exec app bash

laravel-echo

laravel-echo - npm

Laravel Echo library for beautiful Pusher and Socket.IO integration. Latest version: 1.14.2, last published: a month ago. Start using laravel-echo in your project by running `npm i laravel-echo`. There are 101 other projects in the npm registry using laravel-echo.

https://www.npmjs.com/package/laravel-echo

Laravelが提供しているJavaScriptのライブラリです。

laravel-echo-server がブロードキャストしたイベントを受け取ってくれます。

npm install laravel-echo

socket.io-client

socket.io-client - npm

Realtime application framework client. Latest version: 4.5.4, last published: a month ago. Start using socket.io-client in your project by running npm i `socket.io-client`. There are 7128 other projects in the npm registry using socket.io-client.

https://www.npmjs.com/package/socket.io-client

クライアントからWebSocket通信でサーバーへ接続するために必要になります。

socket.io-client のバージョンが2系でないと、laravel-echo-server に接続できないようなので、バージョンを^2.4.0で指定します。

GitHubのイシューにも上がっています。2022年12月時点ではまだ解決していないようです。

Unable to connect with `laravel-echo-server` with latest `socket.io-client` · Issue #576 · tlaverdure/laravel-echo-server

Describe the bug Whenever I try to connect with laravel-echo-server with the latest socket.io-client and laravel-echo, it does not connect to the laravel-echo-server, however If I downgrade socket.io-client version to 2.3.0, it works lik...

https://github.com/tlaverdure/laravel-echo-server/issues/576

npm install socket.io-client@^2.4.0

これで、環境構築とWebSocket通信を実現するための準備ができました。

機能実装

クライアント(ブラウザ)からメッセージを送信すると、WebSocketで常時接続されているクライアント(ブラウザ)に対してイベントを介し、データ(メッセージ)送信する機能を実装します。

イベント

MessageEventを作成します。appコンテナで以下のコマンドを実行してください。

php artisan make:event MessageEvent

./src/app/Events/MessageEvent.php が生成されるので、以下のように編集します。

<?php

namespace App\Events;

use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class MessageEvent implements ShouldBroadcast // インターフェースの記述を忘れずに
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public $message;

    /**
     * Create a new event instance.
     *
     * @return void
     */
    public function __construct($message)
    {
        $this->message = $message;
    }

    /**
     * Get the channels the event should broadcast on.
     *
     * @return \Illuminate\Broadcasting\Channel|array
     */
    public function broadcastOn()
    {
        return new Channel('message-event');
    }

    public function broadcastWith()
    {
        return [
            'message' => $this->message,
        ];
    }
}

Redisによる処理をキューで実行するには、ワーカーを起動する必要があります。

appコンテナで以下のコマンドを実行すると、ワーカーが起動します。

php artisan queue:work

ルーティング

./src/routes/web.php に以下のルーティングを追記してください。

それぞれ以下の処理をします

  • メッセージ送信フォームを表示する
  • ブラウザから送信されたメッセージを処理する
Route::get('messages', [App\Http\Controllers\MessagesController::class, 'index'])->name('messages.index');
Route::post('messages', [App\Http\Controllers\MessagesController::class, 'store'])->name('messages.store');

コントローラー

MessagesControllerを作成します。appコンテナで以下のコマンドを実行してください。

php artisan make:controller MessagesController

コントローラーの記述はとてもシンプルです。単純にビューを返す index() とメッセージが送信された際にイベントを呼ぶ store() になります。

実際にはメッセージをDBに保存する等の処理が入ると思いますが、大筋のところには影響ないので割愛します。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class MessagesController extends Controller
{
    public function index()
    {
        return view('messages.index');
    }

    public function store(Request $request)
    {
        event(new \App\Events\MessageEvent($request->message));

        return redirect()->route('messages.index');
    }
}

ビュー

続いてビューファイルを用意します。./src/resources/views/messages/index.blade.php を作成してください。

メッセージを送信するだけの味気ないものですが、WebSocket通信を確認するには十分なのでこのまま進めます。

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Docker Laravel Websocket</title>
        @vite(['resources/css/app.css', 'resources/js/app.js'])
    </head>
    <body>
      <div>
        <form action="{{ route('messages.store') }}" method="POST" >
          @csrf
          <input type="text" name="message" value="">
          <button type="submit">送信</button>
        </form>
      </div>
    </body>
</html>

クライアント側の記述

バックエンドでイベントが発生した際に、クライアント(ブラウザ)でイベントを受け取るための設定をします。

./src/resources/js/bootstrap.js で事前にインストールしたライブラリを読み込みます。以下を追記してください。

import Echo from 'laravel-echo';
import io from 'socket.io-client';
window.io = io;
window.Echo = new Echo({
    broadcaster: 'socket.io',
    host: window.location.hostname + ':6001'
});

そして、./src/resources/views/messages/index.blade.php</body> の下にも追記します。

    </body>
    <script>
      window.addEventListener('load', (event) => {
        window.Echo.channel('message-event').listen('MessageEvent', (e) => {
          console.log(e)
        })
      });
    </script>
</html>

これでクライアント(ブラウザ)側の設定は完了です。

動作検証

コンパイル

appコンテナで、以下のコマンドを実行します。このコマンドを実行しないとVite manifest not found at とエラーが出てしまいます。

npm run build

ワーカー起動

前述しましたが、Redisによる処理をキューで実行するためワーカーを起動します。

appコンテナで以下のコマンドを実行します。

php artisan queue:work

ブラウザ確認

ブラウザを二つ開いてください。一つをブラウザA、もう一つをブラウザBとします。

ブラウザBでデベロッパーツールのコンソールを開きます。

ブラウザAからフォームにテキストを入力して、メッセージを送信してください。

すると、ブラウザBのコンソールにブラウザAで入力したメッセージが表示されます。

まとめ

最後まで記事を読んでいただきありがとうございます。初めてのWebSocketを使った機能実装でしたので、いろいろと苦戦しました。

ただ、WebSoketによるリアルタイム通信が成功した時は、苦労した分とても嬉しかったです。

次は実案件でも使えるようローカル環境ではなく、本番運用を想定したサーバー構築もしたいなと思っています。まぁぶっちゃけ、リアルタイム性が求められる案件は少なく日の目を浴びることはないかもですが…笑

では、また次回の記事で!

新しいメンバーを募集しています

Ichikawa

Ichikawa / Engineer

パン屋から転身してエンジニア3年目。主にPHP/Laravelを使っています。最近ではVue.js/Nuxt.jsと人間に興味あり。