はじめに
普段みなさんが使っているSNS等のアプリには、フォローしたユーザーがメッセージの送信や投稿をすると、リアルタイムで通知がされるようになっていると思います。
今では、SNSにおいてリアルタイム通信というのは、必須といえる機能となりUX(ユーザー体験)をよりよくしてくれます。
Webアプリにおけるリアルタイム通信といえば、WebSocketやWebRTCが思い浮かびますよね。
ただ、私自身はWebSocketやWebRTCを使った開発経験はなく、ちょうどブログのネタも探していたので、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
で作られたLaravelでWebSocket通信(ブロードキャスト)するためのサーバー(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 /usr/bin/composer /usr/bin/composer
# phpredis // インストールを忘れすに!!
RUN pecl install redis \
&& docker-php-ext-enable redis
# Node.js
COPY /usr/local/bin /usr/local/bin
COPY /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.php
のRedis周りの設定は以下のようになっています。
'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 / Engineer
パン屋から転身してエンジニア3年目。主にPHP/Laravelを使っています。最近ではVue.js/Nuxt.jsと人間に興味あり。