【PHP】爆速でPubSubアプリが作れるThruway

## PHPでチャットしたい

現在[^1]`php websocket`とかでググるとRatchetというライブラリが真っ先にヒットします。私も最初はこれを弄っていました。

ですが**RatchetはWAMPv2に対応していない**という致命的な欠点を抱えています。

そのため何気なく`npm install autobahn`とかして最新の`autobahn.js`(WAMPv2対応)を突っ込んだりするとプロトコル違いが原因でhandshakeに失敗します。公式のtutorialで使ってるのに！

困って他をあたったところ、[Thruway][]というライブラリを見つけました。これが触ってみたらとてもいい感じでした。

しかし日本語の情報がわずかしかないので、ここではチャットアプリの実装を通じて[Thruway][]の使い方を広めようと思います。

[Thruway][]だけでclientサイドまでphpオンリーで実装できますが、今回はclientサイドは`autobahn`で行くことにします。

## 前提条件

下記ツールを用いますので、持ってない人は`/(apt-get|yum|dnf|インストール)/`してください。

* 必須
  - composer

* autobahn使わないならいらない
  - node.js
  - bower

## インストール

```
composer require voryx/thruway
composer require thruway/pawl-transport
```
以上。

autobahn.jsなどを、クライアントサイドに使う場合は``composer require voryx/thruway``のみでOKです。

今回はphpだけで動作確認までしたいので`thruway/pawl-transport`まで入れましょう。

## 動作確認

```terminal
php vendor/voryx/thruway/Examples/SimpleWsRouter.php
```

こんな感じでRouterが走り出します。

```Router_output
2018-09-08T07:15:54.0880690 debug      [Thruway\Peer\Router 32351] New router created
2018-09-08T07:15:54.0896410 info       [Thruway\Peer\Router 32351] Starting router
2018-09-08T07:15:54.0903750 info       [Thruway\Transport\RatchetTransportProvider 32351] Websocket listening on 127.0.0.1:9090
2018-09-08T07:15:54.0906830 info       [Thruway\Peer\Router 32351] Starting loop
```

Starting loopしたらもう一個terminalを立ち上げてclient側を動かします。

```terminal
php vendor/voryx/thruway/Examples/SimpleClient.php
```

最初に動かしたRouterがこんな感じの出力を吐けばちゃんと動いています。最後の方のonMessageが大事。

```Router_output
2018-09-08T07:20:34.2795810 debug      [Thruway\Transport\RatchetTransportProvider 1812] RatchetTransportProvider::onOpen
2018-09-08T07:20:34.2858580 debug      [Thruway\Transport\RatchetTransportProvider 1812] onMessage: ([1,"realm1",{"roles":{"publisher":{"features":{"subscriber_blackwhite_listing":true,"publisher_exclusion":true}},"subscriber":{"features":{}},"caller":{"features":{"caller_identification":true,"progressive_call_results":true,"call_canceling":true}},"callee":{"features":{"caller_identification":true,"progressive_call_results":true,"call_canceling":true}}},"authmethods":[],"authid":"anonymous"}])
2018-09-08T07:20:34.2863310 info       [Thruway\RealmManager 1812] Got prehello...
2018-09-08T07:20:34.2863440 debug      [Thruway\RealmManager 1812] Creating new realm 'realm1'
2018-09-08T07:20:34.2875190 debug      [Thruway\RealmManager 1812] Adding realm 'realm1'
2018-09-08T07:20:34.2898050 debug      [Thruway\Transport\RatchetTransportProvider 1812] onMessage: ([32,5799645875876444,{},"com.myapp.hello"])
2018-09-08T07:20:34.2904330 debug      [Thruway\Subscription\SubscriptionGroup 1812] Added subscription to 'exact':'com.myapp.hello'
2018-09-08T07:20:34.2905620 debug      [Thruway\Transport\RatchetTransportProvider 1812] onMessage: ([16,3149386944896931,{"acknowledge":true},"com.myapp.hello",["Hello, world from PHP!!!"]])
2018-09-08T07:20:34.2907040 debug      [Thruway\Transport\RatchetTransportProvider 1812] onMessage: ([64,3904229972015909,{},"com.myapp.add2"])
2018-09-08T07:20:34.2915070 debug      [Thruway\Transport\RatchetTransportProvider 1812] onMessage: ([48,4597463936048137,{},"com.myapp.add2",[2,3]])
2018-09-08T07:20:34.2930660 debug      [Thruway\Transport\RatchetTransportProvider 1812] onMessage: ([70,6219899842080365,{},[5]])
```

## Routerの実装

```php:router.php
<?php

require_once './vendor/autoload.php';

use Thruway\Peer\Router;
use Thruway\Transport\RatchetTransportProvider;

$router = new Router();

$transportProvider = new RatchetTransportProvider("127.0.0.1", 9090);

$router->addTransportProvider($transportProvider);

$router->start();
```

ThruwayのExampleにあるSimpleWsServer.phpのほぼ全コピーです。

これだけで`ws://127.0.0.1:9090`でアクセス可能なRouterの実装が完了します。

## Clientの実装

Clientはautobahn.jsに任せるのでひたすらhtmlとjsです。

##### client用のautobahn.min.jsを手に入れる
```terminal
bower install autobahn
```

``bower``でinstallしたパッケージは`bower_components`に入ってくれるのでこれを`<script src='...'>`で読み込めば使えるようになります。

というわけでチャット欄を設けてjsを読むhtmlを用意しましょう。

```chat.html
<html>
  <head>
    <meta charset='utf-8'>
    <title>chat</title>
  </head>

  <body>
    <h3>Chat</h3> 
    <ul id='chat-list'></ul>
    <input id='speech' type='text'>
    <button onclick='chat()'>chat</button>

    <script src="/bower_components/autobahn/autobahn.min.js"></script>
    <script src="/chat.js"></script>
  </body>
</html>
```

最低限。

最後にchat.htmlに読ませるjsを作って完成です。

```JavaScript:chat.js
const chat_url = 'php.chat';
var conn;
(function(){
  conn = new autobahn.Connection({url: 'ws://127.0.0.1:9090', realm: 'realm1'});
  conn.onopen = function(session){
    function received_chat(message){
      document.getElementById('chat-list').innerHTML += '<li>' + message[0] + '</li>';
    }
    session.subscribe(chat_url, received_chat);
  }
  conn.open();
}());

function chat(){
  const chat = document.getElementById('speech').value;
  conn.session.publish(chat_url, [chat], null, {exclude_me: false});
}
```

`autobahn`は`autobahn.min.js`内で定義された変数です。autobahnが定義している各メソッドやクラスはこいつ経由で参照します。

`new autobahn.Connection()`でWebSocket通信のurlやrealmを指定します。実際にconnectするには`conn.open()`です。

``conn.onopen``で、connectionを確立した時に処理する関数を指定します。今回は``session.subscribe``で`php.chat`チャンネルでpublishイベントがあった際の挙動を指定しています。

`function chat()`はchatボタンを押したときの挙動です。入力フォームの値を`php.chat`チャンネルにpublishします。

`{exclude_me: false}`というのは、publishした本人もpublishイベントを受け取るためのオプションです。これがないとpublishした本人は自分の発言がチャット欄に流れて来なくなります。

以上で実装終了です。

## 動かす

```terminal
php -S localhost:9999 | php router.php
```

任意のブラウザで`http://localhost:9999/chat.html`に行けばもうそこはチャットルームです。

![Thruway.gif](https://qiita-image-store.s3.amazonaws.com/0/210844/30928998-1f72-28f8-9847-06b022c6f588.gif)


## まとめ

ほとんどautobahnの説明でしたがたったこれだけのコードでWAMPv2に則ったチャットアプリを実装できてしまいました。

後は発言をredisに保存するなりxss対策するなり部屋を分割するなりすればいいと思います。

phpによるclientサイドの実装は[Thruway][]のREADME.mdの実例で分かりやすく説明しています。autobahnと見た目はあまり変わりません。

phpでWebsocket通信したいときには是非[Thruway][]の採用をご検討ください。

[^1]: 2018/09/08

[Thruway]:https://github.com/voryx/Thruway