Laravel Broadcastingを試してみます。とりあえずMacのローカルで試してみます。
やりたいこと
- 全体配信(接続しているクライアント全体にメッセージを送る)
- 個別配信(特定のクライアントにメッセージを送る)
- 配信はuser id等で指定したい
という感じです。
素のSocket.ioではユーザーと通信の紐づけを自分で管理しないといけないのでLaravelのユーザー管理機能と連携したい。
最初に知っておいた方がいいこと
PrivateChannelの配信先は認証済みユーザーであること
個別配信(PrivateChannel)の利用はLaravelで認証済みのユーザーが前提のようです。最初、それを理解しておらずしばらくハマりました。また、標準ではweb middlewareが適用されており、API経由で利用(認証)するにはmiddlewareの変更等も必要です。
SocketサーバはPusherとLaravel-Echo-Serverが利用できる
その他も利用できますが、更新サンプルなどはPusherの利用前提で書かれていたりするので随時読み替えが必要です。
Pusherを利用した記事はこちらをどうぞ。
各種ツールの相関関係
下記のような感じ。
作るもの
consoleに購読したチャネルの内容が表示されるだけのもの。
準備
まず、必要なものをインストールしておきます。
Laravel
何は無くてもLaravel。
composer create-project laravel/laravel echo
cd echo
関連ライブラリ
次に、ライブラリ群をインストールします。jsとphp用。
npm install --save laravel-echo socket.io-client
composer require predis/predis
Redis
Laravel-Echo-Serverを利用する場合、Redisが必須となります。
インストールがまだであれば、こちらを参考にインストールしてください。
インストール後、redisを起動させておきます。
redis-server
Laravel-Echo-Server
Laravel−Echo-Serverをインストールします。
npm install -g laravel-echo-server
インストールしたら設定(ファイルの定義)を行います。initコマンドで対話形式で設定できます。
laravel-echo-server init
今回は下記のような感じで返答しました。serverの位置(IPやポート)などは環境に応じて適切に設定してください。
? 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:8000
? 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? Yes
? Specify the URI that may access the API: http://localhost:8000
? Enter the HTTP methods that are allowed for CORS: GET, POST
? Enter the HTTP headers that are allowed for CORS: Origin, Content-Type, X-Auth-Token, X-Requested-With, Accept, Authorizatio
n, X-CSRF-TOKEN, X-Socket-Id
appId: 15efabbfxxxxxxxx
key: 366fc3b105d867a3990b5b57xxxxxxxx
なお、設定はlaravel-echo-server.jsonというファイルの記録されるようです。ソースの管理の都合上、Laravelのプロジェクトファイル内に保存したほうがいい感じです。
設定が終わったら起動します。
laravel-echo-server start
L A R A V E L E C H O S E R V E R
version 1.4.2
⚠ Starting server in DEV mode...
✔ Running at localhost on port 6001
✔ Channels are ready.
✔ Listening for http events...
✔ Listening for redis events...
Server ready!
Laravel側の準備
具体的な実装を行う前にLaravelの設定を行います。
config/app.php(抜粋)
config/app.phpの下記行がコメントアウトされているので、コメントインします。
.....
App\Providers\BroadcastServiceProvider::class,
.....
.env(抜粋)
続いて.envの設定をします。BROADCAST_DRIVERをlogからredisに変更します。
なお、後々DBも利用するので、DBの設定も済ましておきましょう(migrateできるよう)。
.....
BROADCAST_DRIVER=redis
.....
実装1:Eventの追加(Channel:全員と通信する)
まずは全体配信(通信)の機能を実装してみます。
通信はEventを介して行われるので、Eventを作成します。
php artisan make:event PublicEvent
では、ファイルを下記のようにします。やってることは、
- ShouldBroadcastをimplements
- public-eventというChannelを定義
- messageという名称でPUBLICという文字列を配信
という感じ。
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Queue\SerializesModels;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
+class PublicEvent implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct()
{
//
}
public function broadcastOn()
{
+ return new Channel('public-event');
}
+ public function broadcastWith()
+ {
+ return [
+ 'message' => 'PUBLIC',
+ ];
+ }
}
Eventを発火させる機能を作ります。今回は特定のURLにアクセスしたら発火するようにしました。
真っ白な画面になるので、いちおうpublicとだけ表示させておきます(無くてもいいです)。
Route::get('/public-event', function(){
broadcast(new \App\Events\PublicEvent);
return 'public';
});
クライアント側機能の実装
標準JS環境をVueからReactに変更する(必須ではない)
私はReactを利用しるので、プリセット環境をVueからReactに切り替えておきます。
php artisan preset react
bootstrap.js(抜粋)への記述
続いてクライアント側機能の実装を行います。
今回はブラウザのコンソールにメッセージを表示させるだけなので、resources/js/bootstrap.jsの最後に下記のコードを書きます。
//for Echo
import Echo from 'laravel-echo';
window.io = require('socket.io-client');
//接続情報
window.Echo = new Echo({
broadcaster: 'socket.io',
host: 'http://localhost:6001',
});
//購読するチャネルの設定
window.Echo.channel('public-event')
.listen('PublicEvent', (e) => {
console.log(e);
});
ソースのコンパイル
JSの記述が反映されるようコンパイルします。
npm install
npm run dev
welcome.blade.php
メッセージの配信を確認するための記述をwelcome.blade.phpに対して行います。
コンソールに出力するため、HTML等の編集はほとんどありませんが、JSを実行するために2つの記述を追加します。
1. csrf_tokenのための記述
下記をheadのmetaタグに追加します。
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
+<meta name="csrf-token" content="{{ csrf_token() }}">
2. コンパイルしたJSの実行
書きを</body>の手前にでも挿入しておきます。
+<script src="{{ asset('js/app.js')}}"></script>
</body>
動作確認
では、Laravelを起動して動作を確認してみます。
Laravelの起動
php artisan serve
確認画面を開く
/(webcome.blade.php)にアクセスして(Chromeの)デバッグコンソールを開きます。
Eventの発火
/public-eventにアクセスしてEventを発火させ、welcome.blade.phpのコンソールにPUBLICと表示されるのを確認します。
実装2:Eventの追加(PrivateChannel:ユーザーを指定して通信する)
続いて特定のユーザーに対して通信を行う方法(PrivateChannel)を利用してみます。
Eventを作成します。
php artisan make:event PrivateEvent
実装します。基本的にはPublicEventと同じですが、チャネルの指定をPrivateChannel()として行っているところが違います。
メッセージとしてPRIVATEを送るようにします。
PrivateEvent
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Queue\SerializesModels;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
+class PrivateEvent implements ShoudBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct()
{
//
}
public function broadcastOn()
{
+ return new PrivateChannel('private-event');
}
public function broadcastWith()
{
return [
+ 'message' => 'PRIVATE',
];
}
}
routes/channel.php
なお、PrivateChannelでは、channel.phpにおいて認証?処理を行う必要があります。
特定のchannelのパターン毎にロジックを定義します。
trueかfalseをreturnするのですが、一旦全てOKというこで、trueを返しておきます。
応用は後ほどやります。
Broadcast::channel('private-event', function () {
return true;
});
ルートの追加
PrivateEventを発火するためのルートを定義します。
Route::get('/private-event', function(){
broadcast(new \App\Events\PrivateEvent());
return 'private';
});
bootstrap.js
bootstrap.jsにPrivateChannelを処理するための記述を追加します。Echo.private()となります。
//for Echo
import Echo from 'laravel-echo';
window.io = require('socket.io-client');
window.Echo = new Echo({
broadcaster: 'socket.io',
host: 'http://localhost:6001'
});
window.Echo.channel('public-event')
.listen('PublicEvent', (e) => {
console.log(e);
});
+window.Echo.private('private-event')
+ .listen('PrivateEvent', (e) => {
+ console.log(e);
+ });
jsを編集したら忘れずにnpm run devを。
Laravel-Echo-Serverのコンソール
なお、この時点でブラウザを更新すると、クライアント側のjsがサーバにサブスクリプト(join)のための通信を行いますが認証エラーが出ているようです。
つまり、PrivateChannelを利用するには(Laravel)の認証が必要なようです。
[09:04:21] - 0MCfTL-LAYuqaXdIAAAC joined channel: public-event
⚠ [09:04:21] - 0MCfTL-LAYuqaXdIAAAC could not be authenticated to private-private-event
{
"message": "",
上記はpublicへの参加はできてますが、privateへの参加が認証エラーとなっています。
認証の準備
では、認証機能を追加します。
私のユースケースではAPI経由でのアクセスとなるのでAPI認証を行えるようにしてみます。
migrateの準備
usersテーブル用のマイグレーションファイルを下記のように編集します。api_tokenを追加してるだけです。
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateUsersTable extends Migration
{
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
+ $table->string('api_token')->unique()->nullable();
});
}
public function down()
{
Schema::dropIfExists('users');
}
}
migrateを実行します。
php artisan migrate
認証に利用するuserを1名追加しておきます(make:authで対応してもいいかと思います)。
php artisan tinker
>>> $user = new App\User;
=> App\User {#2907}
>>> $user->name = 'test1';
=> "test1"
>>> $user->email = 'test1@test.com';
=> "test1@test.com"
>>> $user->password = Hash::make('testtest');
=> "$2y$10$pN7.zZyJ87zgusHvkjkMv.bnFyT/g174WTi7kMzezES8V5KWPMGp."
>>> $user->api_token = 'hogehoge';
=> "hogehoge"
>>> $user->save();
=> true
hogehogeは単にAPI認証だけでなくUserの特定に利用されるのでUser毎に一意である必要があります。
認証方法の変更
標準ではweb (group) middlewareが適用されているようなのでAPIに変更します。
app/Providers/BroadcastServiceProvider.phpを下記のように変更します。これによりBearerによるtoken認証が行われるようになります。
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Broadcast;
class BroadcastServiceProvider extends ServiceProvider
{
public function boot()
{
+ Broadcast::routes(['middleware' => 'auth:api']);
require base_path('routes/channels.php');
}
}
クライアント側にBearerを添付する記述を追加します(抜粋)
window.Echo = new Echo({
broadcaster: 'socket.io',
host: 'http://localhost:6001',
+ auth: {
+ headers: {
+ 'Authorization' : 'Bearer hogehoge',
+ }
+ }
});
クライアント側を更新するとエラーが消え、joinedとなりました。
[09:25:52] - 3oy2dVcqrlot8kvdAAAH left channel: public-event (transport error)
[09:25:52] - H9OVvfM5zRJbf52xAAAI joined channel: public-event
[09:25:52] - H9OVvfM5zRJbf52xAAAI authenticated for: private-private-event
[09:25:52] - H9OVvfM5zRJbf52xAAAI joined channel: private-private-event
実装3:改良する
publicもprivateも動くようになりましたが、ここまでの記述ではprivateも全員が同じchannelを購読しているため、PrivateChannelを利用しても全員に配信されてしまう実装になっています。なので、それを直します。
やりたいことは、id=1の人にだけメッセージを送りたい。といったことです。
では、PrivateEventの発火元であるルートの記述を下記のように変更します。
id=1のuserを取得し、PrivateEventにuserを渡します。
Route::get('/private-event', function(){
//id=1のuserを取得
+ $user = App\User::find(1);
//Eventをuserを渡して発火
+ broadcast(new \App\Events\PrivateEvent($user));
return 'private';
});
続いてPrivateEvent.phpを変更します。渡されたuser情報を利用してuser専用のチャネルを設定します。
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Queue\SerializesModels;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use App\User;
class PrivateEvent implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
+ public $user;
public function __construct(User $user)
{
+ $this->user = $user;
}
public function broadcastOn()
{
+ return new PrivateChannel('private-event.' . $this->user->id);
}
public function broadcastWith()
{
return [
'message' => 'PRIVATE',
];
}
}
チャネル名がprivate-event.user_idとなるように定義します。
id=1の場合、チャネルはprivate-event.1となり、id=1の人しか購読しないチャネルとなる想定です。
続いてchannel.phpのchannelのマッチングパターンを適切に変更します。
なおchannel()関数の第一引数には認証されたuser情報が入ります。その情報を利用して配信対象が適切であるかなどをチェックします。
Broadcast::channel('private-event.{id}', function ($user, $id) {
//認証したuserとチャネルを指定するidが一致するかチェック
return (int) $user->id === (int) $id;
});
では、最後にクライアント側(js)を編集します。
クライアント側でも購読するチャネルがprivate-event.user_idとなるよう実装します。
//user_idを取得する(下記はダミー)
const user_id = 1;
window.Echo.private('private-event.' + user_id.toString())
.listen('PrivateEvent', (e) => {
console.log(e);
});
応用
以上です。