CakePHP5入門【CakePHP5実用編⑤】WebSocketとRatchet

B美

C菜

A子

B美
cd html/authapp[Enter]
composer require cboden/ratchet react/socket[Enter] |
これで(WebSocketライブラリである)「Ratchet」が導入されるからね
(「react/socket」のほうは、ソケット通信には必須らしい…よく知らんけど)




B美
bin/cake bake command WebSocketServer[Enter] |
双方向通信を行うためには、このサーバが必要だからね


C菜

B美
まず、先頭には
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface; use Ratchet\Http\HttpServer; use Ratchet\Server\IoServer; use Ratchet\WebSocket\WsServer; use React\EventLoop\Factory as LoopFactory; use React\Socket\Server as ReactServer; |
を追加して、クラス定義には「MessageComponentInterface」をインプリメントしてね
class WebSocketServerCommand extends Command implements MessageComponentInterface |

A子

C菜

B美
簡単に言っちゃうと…
「MessageComponentInterface」の中に定義されているメソッド(中身は無くて、宣言だけ有る)の「実装」を強制する仕組みね

A子
さっぱり分からんがな(怒)

C菜

B美
これはオブジェクト指向における「多態性(ポリモーフィズム)」にも関わってくるんだけど、(難しいから)あまり気にしなくても大丈夫よ
(「WebSocketの機能を実現するための約束事」とでも思っておけばOK)
ちなみに、WebSocketだけじゃなく、今後色々なところで出現する(かもしれない)けどね(苦笑)

A子

B美
それじゃ、続きね
「WebSocketServerCommand」クラスの中に、二つの配列フィールド(「$clients」及び「$rooms」)と「initialize」メソッドを追加するよ
(なお、「protected」というのは、前回出てきた「アクセス修飾子」の一つね)
class WebSocketServerCommand extends Command implements MessageComponentInterface
{ protected array $clients = []; protected array $rooms = []; public function initialize(): void { parent::initialize(); $this->clients = []; $this->rooms = []; } |

A子

B美
んじゃ、いよいよ処理の中核である「execute」メソッドを実装していくよ
public function execute(Arguments $args, ConsoleIo $io): void
{ $loop = LoopFactory::create(); $socket = new ReactServer(WEBSOCKET_IP.':'.WEBSOCKET_PORT, $loop); $server = new IoServer(new HttpServer(new WsServer($this)), $socket, $loop); $io->out('WebSocketサーバを起動しました。'); $server->run(); } |

A子
「WEBSOCKET_IP」と「WEBSOCKET_PORT」って、定数定義かな?

B美
「config」ディレクトリの中にある「const.php」には以下の二行を追加してね
define("WEBSOCKET_IP", "192.168.1.205"); //WebSocketサーバのIPアドレス
define("WEBSOCKET_PORT", "50080"); //WebSocketサーバのポート番号 |
あ、IPアドレスは各人の開発環境に合わせてね
それと、ポート番号をここでは「50080」番にしているけど、これは(使用していない)別のポート番号に変更してもOKだからね
(もちろん「0~65535」の範囲内で、かつ「ウェルノウンポート」ではないものに…)


A子

C菜

B美
「Well Known」ポート以外にも「レジスタード」ポート(または「ユーザ」ポート)である「1024~49151」や「ダイナミック」ポート(または「プライベート」ポート)である「49152~65535」もあるけどね
要するに「個人で新たなポート番号を利用する場合、でかい数字にしとけ!」…ってこと
(具体的には、「ざっくり50000番以降を使えば、既存のアプリケーションとかぶることはない」…って覚えておけば良いわ)

A子
これで終わり?

B美
(さっき出てきた「インタフェースの実装」がまだだからね)
public function onOpen(ConnectionInterface $conn): void
{ $conn->roomId = null; $this->clients[$conn->resourceId] = $conn; } public function onMessage(ConnectionInterface $from, $msg): void { $data = json_decode($msg, true); if (!isset($data['type']) || !isset($data['room_id'])) { return; } $roomId = $data['room_id']; if ($data['type'] === 'join') { $from->roomId = $roomId; if (!isset($this->rooms[$roomId])) { $this->rooms[$roomId] = []; } $this->rooms[$roomId][$from->resourceId] = $from; } elseif ($data['type'] === 'message' && $from->roomId !== null) { $messageData = [ 'room_id' => $roomId, 'user_id' => $data['user_id'], 'user' => $data['user'], 'message' => $data['message'] ]; foreach ($this->rooms[$roomId] as $client) { $client->send(json_encode($messageData)); } } } public function onClose(ConnectionInterface $conn): void { if ($conn->roomId !== null && isset($this->rooms[$conn->roomId][$conn->resourceId])) { unset($this->rooms[$conn->roomId][$conn->resourceId]); } unset($this->clients[$conn->resourceId]); } public function onError(ConnectionInterface $conn, \Exception $e): void { $conn->close(); } |
これら四つのメソッドを追記すれば完了よ
(処理の中核となるのは「onMessage」メソッドね)





C菜
もうこのコマンドクラスを実行しちゃっても大丈夫ですか~?

B美
テスト実行してみましょうか
bin/cake web_socket_server[Enter] |


A子
なんか入力待ち状態($が出た状態)に戻らないんだけど…(苦笑)

B美
当然、無限ループ状態になってるわよ

A子

B美
([Ctrl]キーと[c]キーを同時に押すってこと)
もちろん、それをやるとWebSocketサーバが止まっちゃうんだけど…

C菜
なんとかならないんですか~?

B美
実行時のコマンドの末尾に「&」を付け加えるだけ
bin/cake web_socket_server &[Enter] |
一見さっきと同じく止まってるように見えるけど、[Enter]キーを押すと復帰するわ
(もちろん、「MATE端末」を閉じてもWebSocketサーバが停止することはないから…)

↓


A子
([Ctrl]+[c]じゃダメだよね?)

B美
あれってLinuxの「プロセス番号」なのよ
(プログラムを実行すると、そのプログラムには重複しない「プロセス番号」が自動的に割り当てられるの)

C菜

B美
あ、ただ「プロセス番号を覚えてないといけない」とか、「忘れちゃったらどうしよう」…なんて不安になるわよね
でも大丈夫!
「ポート番号からプロセス番号を調べる」こともできるのよ
su[Enter]
lsof -i :50080[Enter] |
これで50080番ポートを使っているプログラムの「プロセス番号」が判明するわ


B美
kill 29310[Enter]
exit[Enter] |
もちろん、29310というのは決まった数字じゃない(常に変わる)わよ
(言うまでもないだろうけど…)


A子
(あと「kill」って物騒な言葉よね(苦笑))
てか、これってシェルスクリプトで自動化することはできないの?

B美
#!/bin/sh
PORT=50080 PID=$(lsof -i :$PORT -t) if [ -n "$PID" ]; then echo "WebSocketサーバを終了します..." kill "$PID" echo "終了しました。" else echo "ポート $PORT を使用しているプロセスは見つかりませんでした。" fi |
これをrootユーザで作って、実行権を与えておきなさい

A子

C菜
(ファイル名は「websocket_close.sh」にしますね)
su -[Enter]
nano websocket_close.sh[Enter] (上で示したスクリプトをコピペして上書き保存) chmod +x websocket_close.sh[Enter] exit[Enter] |
で良いと思います~
で、このスクリプトを実行(WebSocketサーバを終了)する際は
su -[Enter]
./websocket_close.sh[Enter] exit[Enter] |
ということになりますね~

↓


A子

B美
あなたもきちんと復習しておきなさいね

A子
まぁ、それはともかく…
まだチャットサービスの実装は終わってないんでしょ?

B美
あとはControllerとViewを作っていきましょう

C菜

B美
まぁ、かなり中身を削除することになるんだけど…(苦笑)
(必要なのは「index」メソッドだけだし)

A子
それじゃ、やってみよう
bin/cake bake controller chat[Enter] |
これで「ChatController.php」ができるはず…
あと、「templates」の中に「Chat」ディレクトリを新規作成しておこう



B美
「src/Controller」の中に作られた「ChatController.php」だけど、「index」メソッド以外は全て削除してね
んで、「index」メソッドの中身は以下のように書き換えるの
public function index($room_id = null)
{ //引数がなければトップページへリダイレクト if (is_null($room_id)) { return $this->redirect(['controller' => 'Top', 'action' => 'index']); } $this->set(compact('room_id')); } |
ポイントは「URLにチャットルーム番号を指定して、それをView側へと渡す」ってだけ

C菜


B美

A子
まじで今回は大変だった(苦笑)