12
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

LaravelでWebSocket

Last updated at Posted at 2020-08-02

目次

Laravelの記事一覧は下記
PHPフレームワークLaravelの使い方

Laravelバージョン

動作確認はLaravel Framework 7.19.1で行っています

前提条件

eclipseでLaravel開発環境を構築する。デバッグでブレークポイントをつけて止める。(WindowsもVagrantもdockerも)
本記事は上記が完了している前提で書かれています
プロジェクトの作成もapacheの設定も上記で行っています

LaravelでDIを使う
本記事は上記で作成したフォルダとファイルを使用します

Laravelの認証(web画面)
本記事は上記が完了している前提で書かれています

LaravelでRedisを操作する
本記事は上記で作成したフォルダとファイルを使用します。LaravelでRedisを使う設定は完了済みで、sessionもRedisに格納されている前提で書かれています

Ratchetのインストール

今回はRatchetを使っていきます
Ratchet (http://socketo.me/)
コマンドラインで
cd sample
composer require cboden/ratchet
xdebugの設定をしているとeclipseが実行していいですかというプロンプトを出すのでOKを押します
eclipseのプロジェクトを右クリック→リフレッシュ

サービスクラス修正

LaravelでRedisを操作するで作成したサービスクラスを修正します
このサービスクラスは上記記事でServiceProviderに登録してあります

(1) /sample/app/Services/Interfaces/RedisService.php修正

RedisService.php
<?php
namespace App\Services\Interfaces;

interface RedisService
{
    public function __construct();

    public function setKey1($value);
    public function getKey1();
    public function getSession($key);

}

getSessionメソッドを追加しました

(2) /sample/tests/Services/Impl/RedisServiceImpl.php修正

Tests\Services\Impl\RedisServiceImpl.php
‥‥
    public function getSession($key){
    }
‥‥

getSessionメソッドを追加しました

(3) /sample/app/Services/Impl/RedisServiceImpl.php修正

App\Services\Impl\RedisServiceImpl.php
‥‥
    private $sessionConn = null;

    public function __construct()
    {
‥‥
        $this->sessionConn = Redis::connection(config('session.connection'));
    }
‥‥
    public function getSession($key)
    {
        $key = config('cache.prefix') . ':' . $key;
        $value = $this->sessionConn->get($key);
        return $value;
    }
‥‥

$sessionConnプロパティを追加しました
コンストラクタを修正しました
getSessionメソッドを追加しました

WebSocket処理作成

(1) /sample/app/Http/Controllers/ChatController.php作成

ChatController.php
<?php
namespace App\Http\Controllers;
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;

class ChatController implements MessageComponentInterface  {

    protected $userList;

    public function __construct() {
        $this->userList = [];
    }

    public function addUser($resourceId, $user) {
        $this->userList[$resourceId] = ["user" => $user];
    }

    public function onOpen(ConnectionInterface $from) {
        if (!array_key_exists($from->resourceId, $this->userList)) {
            $this->userList[$from->resourceId] = ["user" => null];
        }
        $this->userList[$from->resourceId]["conn"] = $from;
        $msg = 'ゲストさんが入室しました';
        if (!is_null($this->userList[$from->resourceId]["user"])) {
            $msg = $this->userList[$from->resourceId]["user"]->name . 'さんが入室しました';
        }
        foreach ($this->userList as $userInfo) {
            $data = ['msg' => $msg, 'position' => 'center'];
            $userInfo["conn"]->send(json_encode($data));
        }
    }

    public function onMessage(ConnectionInterface $from, $msg) {
        foreach ($this->userList as $userInfo) {
            $data = ['msg' => $msg];
            if (!is_null($this->userList[$from->resourceId]["user"])) {
                $data['name'] = $this->userList[$from->resourceId]["user"]->name;
            } else {
                $data['name'] = 'ゲスト';
            }
            if ($from === $userInfo["conn"]) {
                $data['position'] = 'right';
            } else {
                $data['position'] = 'left';
            }
            $userInfo["conn"]->send(json_encode($data));
        }
    }

    public function onClose(ConnectionInterface $conn) {
        unset($this->userList[$conn->resourceId]);
    }

    public function onError(ConnectionInterface $conn, \Exception $e) {
        $conn->close();
    }
}

onOpenはクライアントが接続してきたときに自動的に実行されます
onMessageはクライアントがsendしてきたときに自動的に実行されます
onCloseはクライアントが切断したときに自動的に実行されます
$this->userListにEloquentのユーザーモデルとWebSocketのコネクションが格納されます
$this->userListをforeachで回してWebSocket接続している全ユーザーにJSONをsendしています

(2) /sample/app/Serversフォルダ作成
/sample/app/Servers/WsServer.php作成

WsServer.php
<?php
namespace App\Servers;

use Ratchet\ComponentInterface;
use Ratchet\ConnectionInterface;
use Psr\Http\Message\RequestInterface;
use App\Services\Interfaces\RedisService;
use App\Models\User;

class WsServer extends \Ratchet\WebSocket\WsServer
{

    private $component;

    public function __construct(ComponentInterface $component) {
        parent::__construct($component);
        $this->component = $component;
    }

    public function onOpen(ConnectionInterface $conn, RequestInterface $request = null) {

        $sessionName = config('session.cookie');
        $sessKey = null;
        $sessValue = null;

        $headerCookie = $request->getHeader('Cookie');
        if (isset($headerCookie[0])) {
            $cookieList = explode(';', $headerCookie[0]);
            foreach ($cookieList as $cookie) {
                $cookie = trim($cookie);
                $cookieKeyValue = explode('=', $cookie, 2);
                if ($sessionName === $cookieKeyValue[0]) {
                    $sessKey = decrypt(urldecode($cookieKeyValue[1]), false);
                    break;
                }
            }
        }

        $redisService = app()->makeWith(RedisService::class);
        if (!is_null($sessKey)) {
            $sessValue = $redisService->getSession($sessKey);
            $sessValue = unserialize(unserialize($sessValue));
            foreach ($sessValue as $k => $v) {
                if (strpos($k, 'login_web_') === 0) {
                    $user = User::find($v);
                    $this->component->addUser($conn->resourceId, $user);
                }
            }
        }

        return parent::onOpen($conn, $request);
    }

}

コンストラクタに渡されてくる$componentは先ほど作成したChatControllerです
onOpenはクライアントが接続してきたときに自動的に実行されます
Cookieを取得し、その中からsessionに使っているCookie値を取りだします(if ($sessionName === $cookieKeyValue[0]))。
取り出したCookie値を使いRedisからsessionを取得します($sessValue = $redisService->getSession($sessKey);)。
LaravelでRedisを操作するでsessionはRedisに格納されるようにしました。
そこからユーザーIDをとります。sessionの中のlogin_web_から始まるキーにユーザーIDがvalueとして格納されています(if (strpos($k, 'login_web_') === 0))。

(3) /sample直下にwsServer.php作成

wsServer.php
<?php

use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Illuminate\Contracts\Console\Kernel;
use App\Http\Controllers\ChatController;
use App\Servers\WsServer;

require dirname(__FILE__) . '\vendor\autoload.php';


$app = require __DIR__.'/bootstrap/app.php';
$app->make(Kernel::class)->bootstrap();

$server = IoServer::factory(
    new HttpServer(
        new WsServer(
            new ChatController()
            )
        ),
    8282
    );

$server->run();

webSocketのエントリポイントになります
8282は今回WebSocketで使うポートです。firewallで許可しているポートにしてください

Controllerにメソッド追加

(1) /sample/app/Http/Controllers/SampleController.phpにwebSocketメソッドを追記

SampleController.php
    public function webSocket(Request $request)
    {
        return view('sample.webSocket');
    }

(2) /sample/routes/web.phpに下記を追記
Route::get('sample/web-socket', 'SampleController@webSocket');

viewの作成

(1) /sample/resources/views/sample/webSocket.blade.phpファイル作成

webSocket.blade.php
<html>
    <head>
        <title>sample</title>
        <style type="text/css">
        .container {
            width: 100%;
            height: 100%;
            box-sizing: border-box;
        }
        .msg-log {
            width: 100%;
            height: 92%;
            vertical-align:top;
            box-sizing: border-box;
            padding: 0px;
            border: black solid 1px;
            overflow-y: scroll;
        }
        .input-area {
            width: 100%;
            box-sizing: border-box;
        }
        .msg {
            width: 90%;
            height: 8%;
            vertical-align:top;
            box-sizing: border-box;
            padding: 0px;
            float: left;
        }
        .btn {
            width: 10%;
            height: 8%;
            vertical-align:top;
            box-sizing: border-box;
            padding: 0px;
        }
        .receive-msg-left {
            border-radius: 10px;
            border: black solid 1px;
            padding: 10px;
            margin: 0px 10px 10px 10px;
            display: inline-block;
            float: left;
            background-color: #FFFFFF;
        }
        .receive-msg-right {
            border-radius: 10px;
            border: black solid 1px;
            padding: 10px;
            margin: 0px 10px 10px 10px;
            display: inline-block;
            float: right;
            background-color: #00FF00;
        }
        .receive-msg-center {
            border: none;
            padding: 0px;
            margin: 10px;
            display: block;
            text-align: center;
            background-color: transparent;
        }
        .name-left {
            border: none;
            padding: 0px;
            margin: 0px 0px 0px 10px;
            display: inline-block;
            float: left;
            background-color: transparent;
        }
        .name-right {
            border: none;
            padding: 0px;
            margin: 0px 10px 0px 0px;
            display: inline-block;
            float: right;
            background-color: transparent;
        }
        .br {
            line-height: 0px;
            clear: both;
        }
        </style>
        <script type="text/javascript">

          var conn = "";

          function open(){

        	  conn = new WebSocket('ws://localhost:8282');

        	  conn.onopen = function(e) {
              };

              conn.onerror = function(e) {
                alert("エラーが発生しました");
              };

              conn.onmessage = function(e) {
            	  var data = JSON.parse(e.data);
            	  var msgLog = document.getElementById("msg_log");
                  var divObj = document.createElement("DIV");
                  var msg = null;
                  var msgSplit = null;
                  var nameObj = null;
                  var rowObj = null;
                  var br = null;
                  if (data["name"]) {
                      msg = document.createTextNode(data["name"]);
                      nameObj = document.createElement("DIV");
                      if (data["position"] == "left") {
                    	  nameObj.className = 'name-left';
                      } else {
                    	  nameObj.className = 'name-right';
                      }
                      nameObj.appendChild(msg);
                	  msgLog.appendChild(nameObj);

                	  br = document.createElement("BR");
                      br.className = 'br';
                	  msgLog.appendChild(br);
                  }
                  if (data["position"] == "left") {
                      divObj.className = 'receive-msg-left';
                  } else if (data["position"] == "center") {
                      divObj.className = 'receive-msg-center';
                  } else {
                      divObj.className = 'receive-msg-right';
                  }
            	  msgSplit = data["msg"].split('\n');
            	  for (var i in msgSplit) {
                      msg = document.createTextNode(msgSplit[i]);
                      rowObj = document.createElement("DIV");
                      rowObj.appendChild(msg);
                      divObj.appendChild(rowObj);
            	  }

            	  msgLog.appendChild(divObj);

            	  br = document.createElement("BR");
                  br.className = 'br';
            	  msgLog.appendChild(br);

                  msgLog.scrollTop = msgLog.scrollHeight;

              };

              conn.onclose = function() {
                  alert("切断しました");
                  setTimeout(open, 5000);
              };

          }

          function send(){
        	  conn.send(document.getElementById("msg").value);
          }

          function close(){
        	  conn.close();
          }

          open();

        </script>
    </head>
    <body>
        <div class="container">
            <div id="msg_log" class="msg-log"></div>
            <div class="input-area">
                <textarea id="msg" class="msg"></textarea>
                <button class="btn" onclick="send();" >送信</button>
            </div>
        </div>
    </body>
</html>

conn = new WebSocket('ws://localhost:8282');で接続してます。8282は先ほどwsServer.phpに書いたポートです
conn.send(document.getElementById("msg").value);でWebSocketサーバーにデータ送信しています
conn.onmessageはwebSocketサーバーからsendがあった場合に自動的に実行されます

動作確認

コマンドラインで
cd sample
php wsServer.php
xdebugの設定をしているとeclipseが実行していいですかというプロンプトを出すのでOKを押します
これでWebSocketサーバーが起動します

Chromeから
http://localhost/laravelSample/
左上のLOGINリンクからログインする
Firefoxから
http://localhost/laravelSample/
左上のLOGINリンクからログインする
Chromeから
http://localhost/laravelSample/sample/web-socket
にアクセスする
Firefoxから
http://localhost/laravelSample/sample/web-socket
にアクセスする

Chromeで
あいうえお
かきくけこ
と入力し、送信ボタンをクリックします

Firefoxで
さしすせそ
と入力し、送信ボタンをクリックします

a.png

動きました

12
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?