Chap.05 WebSocketを使ったリアルタイム通信
前回まではSQLiteを使ったデータの永続化と Web APIを使ったデータの共有について書きました。 ですがHTTPはリアルタイムなデータ共有には不向きです。 (ポーリングやSSEなどの方法はありますが……)
そこで、今回はWebSocketを 使ったリアルタイム性の高い通信について書いていきます。
このような通信はインタラクティブな作品を制作する際によく役立つと思うので、 参考にしていただけたらと思います。
今回はこのようなサンプルスケッチを用意しました。
WebSocketとは?
前回までに登場したHTTPは、通信の度にTCPで接続し直します。 ですがWebSocketは一度接続すると、その接続を使って双方向にデータを送信することができます。 これにより、双方向にリアルタイムな通信を行うことができます。
ではサンプルコードを紹介していきます。
環境
今回もクライアントにProcessing、 サーバサイドにNode.jsを使って紹介しようと思います。
使用するライブラリは以下の通りです。
- Processing用WebSocketライブラリ
- Node.js用WebSocketライブラリ
サーバ
こちらがサーバのコードになります。
まず、クライアントが接続してきたら クライアント毎にUUIDを割り当てます。 このUUIDは、スケッチ側でどのユーザがどの線を描いている途中かを識別するために使います。
次に、クライアントからメッセージを受信したら、受け取ったデータをJSONにパースします。 データのフォーマットはJSONである必要はないのですが、 今回は柔軟で理解しやすいデータを受け渡しするためにJSONとしました。
最後に、パースしたデータに先ほどのUUIDを付与して、 接続中の全てのクライアントに受け取ったデータを送信します。
送信したクライアント自身には返信しないように作ることもできるのですが、 コードを簡略化するために受け取った全てのデータを全てのクライアントに送信するよに設計しました。
const crypto = require('crypto') const WebSocket = require('ws').WebSocket const WebSocketServer = require('ws').WebSocketServer const wss = new WebSocketServer({ port: 5001 }) wss.on('connection', ws => { ws.uuid = crypto.randomUUID() ws.on('message', message => { const event = JSON.parse(message.toString('utf-8')) event.painter_id = ws.uuid console.log(event) wss.clients.forEach(client => { if (client.readyState === WebSocket.OPEN) { client.send(JSON.stringify(event)) } }) }) }) console.log(`ws://localhost:${wss.options.port}`)
スケッチ
こちらがサーバにマウスの軌跡の情報を送信し、 受け取った線の情報を描くスケッチのサンプルになります。
ドローイングが完了した線(終端が確定した線)は lines
に格納され、
ドローイング中の線(終端が未確定の線)は unfinishedLines
に格納されます。
unfinishedLines
は HashMap
により管理していて、サーバ側で付与したUUIDになります。
次に各イベントについて説明していきます。
まず、 mousePressed()
mouseDragged()
mouseReleased()
の内部の処理は以下のようになっています。
mousePressed()
: 描画開始のメッセージを送信するmouseDragged()
: 点を追加するメッセージを送信するmouseReleased()
: 描画終了のメッセージを送信する
メッセージはそれぞれ具体的に以下ようになっています。
// 描画開始のメッセージ
{
"type": "start"
}
// 点を追加するメッセージ
{
"type": "add",
"point": {
"x": 10.0,
"y": 20.0
}
}
// 描画終了のメッセージ
{
"type": "end"
}
このように、メッセージに共通のキーを持たせることで、 メッセージの判定が楽になります。
次は、 webSocketEvent
について説明します。
webSocketEvent
はWebSocketのメッセージを受け取ったら呼び出されます。
受け取ったメッセージは parseJSONObject()
でパースして type
を判定した結果、
以下のような処理を行います。
start
:unfinishedLines
に新規の線を追加するadd
:painterId
を元にunfinishedLines
から対応する線を検索して点を追加するend
:painterId
を元にunfinishedLines
から対応する線を取り出してlines
に追加する
import websockets.*; import org.eclipse.jetty.websocket.api.Session; WebsocketClient client; String WS_URI = "ws://localhost:5001"; ArrayList<Line> lines; HashMap<String, Line> unfinishedLines; void setup() { size(640, 480); client = new WebsocketClient(this, WS_URI); lines = new ArrayList<Line>(); unfinishedLines = new HashMap<String, Line>(); } void draw() { background(0); for (Line line : unfinishedLines.values()) { line.draw(); } for (Line line : lines) { line.draw(); } } void mousePressed() { JSONObject event = new JSONObject(); event.setString("type", "start"); client.sendMessage(event.toString()); } void mouseDragged() { JSONObject event = new JSONObject(); event.setString("type", "add"); JSONObject point = new JSONObject(); point.setFloat("x", mouseX); point.setFloat("y", mouseY); event.setJSONObject("point", point); client.sendMessage(event.toString()); } void mouseReleased() { JSONObject event = new JSONObject(); event.setString("type", "end"); client.sendMessage(event.toString()); } void webSocketEvent(String msg) { JSONObject event = parseJSONObject(msg); println(event); String type = event.getString("type"); String painterId = event.getString("painter_id"); switch (type) { case "start": { unfinishedLines.put(painterId, new Line()); break; } case "add": { Line line = unfinishedLines.get(painterId); JSONObject point = event.getJSONObject("point"); float x = point.getFloat("x"); float y = point.getFloat("y"); line.points.add(new PVector(x, y)); break; } case "end": { Line line = unfinishedLines.remove(painterId); lines.add(line); break; } default: break; } }
class Line { ArrayList<PVector> points; Line() { this.points = new ArrayList<PVector>(); } void draw() { stroke(255); noFill(); strokeWeight(3); beginShape(); for (PVector p : points) { vertex(p.x, p.y); } endShape(); } }
まとめ
WebSocketのような双方向の通信は、通信の仕様が複雑になりがちです。 そういった場合は、今回のように 「メッセージのフォーマットとしてJSONを使う」 「全てのメッセージに共通するキーを持たせる」 などの工夫が有効です。 また、今回は全てのメッセージを全てのクライアントに送信するという仕様にしましたが、 このような仕様はシンプルに済ませられる反面、 同時接続数が多いシステムの場合はメッセージの送受信数が増えてしまい負荷が増加してしまいます。 それぞれメリットもデメリットもあるので、 制作したい作品に合わせた仕様を考えていただけたらと思います。
次回はこちらに書いていこうと思います。