プラニングポーカーアプリでは、プレイヤーの参加、入札、オープンといった各種イベントをWebSocketによる双方向通信で行い、リアルタイム性を実現している。
ライブラリとしては、クライアント側(JavaScript)はsocket.io-clientを、サーバー(Flask-SocketIO)を利用している。
実はこれまでの実装には致命的な欠陥があった。1つの場を利用しているときはよいのだが、別のユーザーが別の場を開いた場合にそれぞれの場同士が干渉し合ってしまうのだ。これでは使いものにならない。
チャットアプリにおけるチャットルームのような、複数のクライアントのグルーピングを行って、WebSocketによる通信はグループ内に閉じるようにしたい。
実はFlask-SocketIOにはまさにroom
という機能が存在するので、その使い方を検証した。
WebSocket通信の名前空間をルームの単位で分割する必要があるが、今回はまさに場(Table)がその単位となるべきである。プレイヤーが場に加わるタイミングで、そのルームに加わるようにする。
Flask-SocketIOからjoin_room
関数をインポートしておく。
from flask_socketio import SocketIO, emit, join_room
場/ルームにジョインするためのエンドポイント。
@socketio.on("join")
def join(table_id):
print(f"A player is joining {table_id}")
join_room(table_id)
クライアントは場のIDは既知なので、join
イベントの引数としてtable_id
を受け取り、join_room
関数の引数として使用する(4行目)。
クライアント側から、上記のjoin
イベントを送出するように実装。
useEffect(() => {
axios.get(`/table/${props.tableId}/players`)
.then((res) => {
const players = mapPlayers(res.data.players, props.playerId);
console.log(`Got ${players.length} players by ajax call.`)
setPlayers(players);
}).then(() => {
// ルームを指定してWebSocket接続するためにイベント送出
console.log("Joining a room (web socket)")
socket.emit("join", props.tableId);
}).catch((error) => {
console.log("通信失敗")
console.log(error.status);
});
return () => {
socket.close();
}
}, []);
上記は、コンポーネントの初回レンダリング時に一度だけ実行されるEffectである(useEffect
の第2引数が空配列となっている点に注意)。8-10行目で、イベントを送出している。
これ以降、socket
オブジェクトよるイベントの送受信はすべて同一のルーム内でのやり取りとなる。クライアント側では特にルームを意識する必要はないのだが、イベント送出時は必ずtable_id
を引数にセットしてあげる必要がある。
例えば、次のゲームの開始イベント。
const handleNewGame = () => {
setTimeout(() => {
socket.emit("newgame", props.tableId);
}, 0);
}
サーバー側は以下のようになる。
@socketio.on("newgame")
def handle_newgame(table_id):
print(f"Starting a new game, broadcasting in {table_id}...")
emit("newgame_begun", room=table_id, broadcast=True)
emit関数の名前付き引数room
に、ルームの識別子(この場合、table_id
)を指定する(4行目)。
これでWebSocket通信がルームという論理的な単位でグルーピングできるようになった。
コメント
[…] […]