Laravelでソケット通信（Laravel WebSocket）

Laravelでソケット通信をやってみます。

# ソケット通信って？

主にリアルタイムなデータの送受信に使用されるもので、チャットアプリなどに用いられるものです。チャットアプリのメッセージは、画面を更新しなくても常に最新のものが表示されていきますよね。

おなじみのHTTP通信では、リクエストが送られたタイミングでレスポンスが返ってくるので、ユーザーが画面を更新しない限り最新の情報が表示されません。毎秒ごとにリクエストを発行する…というのは現実的ではないですよね。

そこで必要になるアプローチがソケット通信になります。
サーバーを常時監視し、アクションがあれば更新、といったことが可能になります。

# Laravel WebSocketについて

ソケット通信を実装する一つの方法として、Pusherを用いる方法があります。Pusherでアカウントを作成して、Pusherサーバーを利用しての実装となります。
しかしなんと、Laravelは「Laravel WebSocket」により、自身でソケット通信を行うことが可能です。つまり、Pusherでアカウントを作成する必要はありません。Laravel自身がソケット通信のサーバーにもなるということです。

https://beyondco.de/docs/laravel-websockets/getting-started/introduction

今回は、このLaravel WebSocketを用いて、Laravelでのソケット通信をやってみたいと思います。

# やりたいこと

Laravel環境を2つ用意します。1つがバックエンド・1つがフロントという構成です。
バックエンド側のとあるアクションにPOSTされる度に、フロント側がリアルタイム（＝画面更新なし）で表示を更新していく、というサンプルを作成します。

# １．Dockerで環境作成

DockerでLaravel環境を2つ作成します。Dockerfileの内容など細かい部分は省きますので、ある程度お好みで作成して頂ければと思います。

### バックエンド側

以下の要件を満たすLaravel環境を作成します。

* DBが使える
* ポート6001を許可する（ソケット通信がポート6001を使用するため）
``` YAML:docker-compose.yml
ports:
  - 8080:80
  - 6001:6001
```

### フロント側

* node及びnpmが使える

# ２．実装準備

dockerをビルド及び起動したら、まずは実装準備（composerなど）をします。

## バックエンド側の実装準備

バックエンド側のコンテナに入り、下記でLaravelプロジェクトを作成

``` shell
composer create-project laravel/laravel socket-test-back
```

`socket-test-back/`で、下記を実行

``` shell
composer require beyondcode/laravel-websockets
composer require pusher/pusher-php-server "~3.0"
php artisan vendor:publish --provider="BeyondCode\LaravelWebSockets\WebSocketsServiceProvider" --tag="migrations"
php artisan vendor:publish --provider="BeyondCode\LaravelWebSockets\WebSocketsServiceProvider" --tag="config"
php artisan migrate
```

`.env`のPUSHER情報を編集　※PUSHER_APP_ID、PUSHER_APP_KEY、PUSHER_APP_SECRETは任意の名前
``` ENV:.env
BROADCAST_DRIVER=pusher
PUSHER_APP_ID=sampleId
PUSHER_APP_KEY=sampleKey
PUSHER_APP_SECRET=sampleSecret
PUSHER_APP_CLUSTER=mt1
```

`config/broadcasting.php`の修正　※Pusherの向き先を自身（localhost）にするイメージ
``` PHP:broadcasting.php
'pusher' => [
    'driver' => 'pusher',
    'key' => env('PUSHER_APP_KEY'),
    'secret' => env('PUSHER_APP_SECRET'),
    'app_id' => env('PUSHER_APP_ID'),
    'options' => [
        'cluster' => env('PUSHER_APP_CLUSTER'),
        'encrypted' => true,
        'useTLS' => false,
        'host' => 'localhost', 
        'port' => 6001,
        'scheme' => 'http', 
    ],
],
```

以上で、バックエンド側の実装準備は完了になります。

## フロント側の実装準備

フロント側のコンテナに入り、下記でLaravelプロジェクトを作成

``` shell
composer create-project laravel/laravel socket-test-front
```

必要なものをインストール
``` shell
npm install pusher-js
npm install laravel-echo
```

`resource/js/bootstrap.js`の該当箇所のコメントアウトを外して、下記のようにする
``` JavaScript:bootstrap.js
import Echo from 'laravel-echo';

window.Pusher = require('pusher-js');

window.Echo = new Echo({
    broadcaster: 'pusher',
    key: process.env.MIX_PUSHER_APP_KEY,
    cluster: process.env.MIX_PUSHER_APP_CLUSTER,
    forceTLS: false,
    wsHost: 'localhost',
    wsPort: 6001,
});
```

`.env`のPUSHER情報を編集　※PUSHER_APP_KEYとPUSHER_APP_CLUSTERをバックエンド側に合わせる
``` ENV:.env
PUSHER_APP_ID=
PUSHER_APP_KEY=sampleKey
PUSHER_APP_SECRET=
PUSHER_APP_CLUSTER=mt1
```

npm起動
``` shell
npm install
npm run dev
```

以上で、フロント側の実装準備は完了になります。

# ３．実装

ソケットを使用する準備は完了したので、実装していきます。

## バックエンド側の実装

以下実行して、SampleEventとSampleControllerを作成

``` shell
php artisan make:event SampleEvent
php artisan make:controller SampleController
```

修正する

``` PHP:SampleEvent.php
<?php

namespace App\Events;

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

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

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

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

``` PHP:SampleController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Events\SampleEvent;

class SampleController extends Controller
{
    public function fireChannel(Request $request)
    {
        SampleEvent::dispatch($request->comment);

        return response()->json(
            ["status" => 200]
        );
    }
}
```

routes/api.phpを修正

``` PHP:api.php
Route::controller(SampleController::class)->group(function () {
    Route::post('/sample/fire_channel', 'fireChannel');
});
```

これでバックエンド側の実装は完了しました。`/sample/fire_channel`にPOSTすることで、チャネル`sample-channel`にブロードキャストされます。

## フロント側の実装

適当な画面を作成し、bladeを以下のように修正します。

``` PHP:index.blade.php
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>TEST</title>
    </head>
    <body>
        <div id="container"></div>
    </body>
    <script src="https://code.jquery.com/jquery-3.3.1.js"></script>
    <script src="/js/app.js"></script>
    <script>
        {{-- チャンネルを監視 イベント検知すると処理実行 --}}
        window.Echo.channel('sample-channel')
            .listen('SampleEvent', (e) => {
                $("#container").append("<div>" + e.comment + "</div>");
            });
    </script>
</html>
```

これでフロント側の実装は完了しました。上記では、`sample-channel`にブロードキャストされる度に、POSTされた`comment`を表示していくということをしています。

# 4．検証

バックエンド側で以下を実行し、ソケットサーバーを起動します。
``` shell
php artisan websockets:serve
```

フロント側で作成した画面（ここでは`/test`にしてます）を開いた状態で、Postmanで`/sample/fire_channel`にPOSTしていきます。

![ソケット検証.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3287737/17525260-a7df-c1c5-2542-8859fe5b7bb2.gif)

ブラウザの画面更新なしで、情報が追加されていってますね！

# まとめ

いかがでしたでしょうか？
今回はソケット通信を検証するためだけのサンプルとして、簡易にPOST値を表示するだけのものを作成しました。実際の開発では、イベント検知の度にDBを取得しにいって、リアルタイムで最新の情報を表示するといったことが多いかと思います。
ソケット通信を活用して、ユーザビリティ向上を図りましょう！

## 告知
最後にお知らせとなりますが、イーディーエーでは一緒に働くエンジニアを募集しております。

私は元々、自社開発企業にて開発を行っていましたが、自分の可能性を広げるべく、受託開発メインのイーディーエーに転職を決意しました。
イーディーエーに入社を決めた理由は、以下の3点です。
・多種多様な案件がある
・様々なことに挑戦できる
・かつフルリモートで勤務できる

様々な案件に挑戦し、スキルを身に付けたい。そしてフルリモートで仕事以外の時間も充実させたいという方は、ぜひ採用情報ページに足を運んでいただければと思います。
https://eda-inc.jp/recruit/
みなさまからのご応募をお待ちしております。