はじめに
WebSocket の利用方法について説明します。 サーバ側には JSR 356 Java API for WebSocket を使い、クライアント側には JavaScript を使っていきます。
簡単なエコーサーバから始め、WebSocket を使ったテトリスの実装までを行います。
ここで使用するコードは以下に置いてあります。
今回は導入編として単純なエコーサーバを作成します。
WebSocket とは
2011年に RFC 6455 として IETF により標準化された軽量な通信プロトコルです。 HTTPをアップグレードしたTCPベースのプロトコルであり、低レイテンシかつ双方向・全二重通信が可能な軽量プロトコルとなります。
HTTP ではクライアントから pull する形での通信しかできませんでした。 このような制限の中でリアルタイムな情報連携を行うには、クライアントから定期的に情報を取得しに行くか、クライアントからの pull リクエストを長時間維持するロングポーリングなどの対応が必要となります。しかし、このような方法では、双方向の通信毎に少なくはない HTTP ヘッダの情報がやり取りされるパフォーマンス上の問題があり、かつメッセージの到着順が保証されないといった問題もあります。
WebSocket は、HTTP によるハンドシェイク要求でコネクションを確立すれば、切断するまでそのコネクションが維持され、その間は双方向・全二重でメッセージ交換が可能となります。 そして、HTTPヘッダのオーバーヘッドも発生せず、プロキシやファイアウォールなどの HTTP と同じ基盤で動作します。
WebSocket は、クライアントから以下のような GET リクエストから開始します。
GET /chat HTTP/1.1 Host: example.com:8000 Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Sec-WebSocket-Version: 13
サーバ側では以下のような Switching Protocols
レスポンスを返します。
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
これにより WebSocket プロトコルでのコネクションに切り替わります。
Java API for WebSocket
JSR 356 Java API for WebSocket は、Java EE 7 の一部としてリリースされた仕様です。 基本的には、JAX-RS と同じようにアノテーション・ベースで WebSocket のエンドポイントを定義していくプログラミングモデルが提供されます。
この API は Java EE 7 の一部にはなりますが、JSR 356 の RI である tyrus では grizzly ベースのサーバ実装も提供してしているため、特に Java EE サーバを利用せずとも、Java SE で気軽に始めることもできます。
本稿では tyrus を Java SE 上で使っていくことにします。
Gradle の Kotlin DSL では、以下のような依存を追加します。
dependencies { implementation("org.glassfish.tyrus:tyrus-server:2.0.0") implementation("org.glassfish.tyrus:tyrus-container-grizzly-server:2.0.0") }
エンドポイント
Java API for WebSocket では @ServerEndpoint
によりエンドポイントを定義します。
@ServerEndpoint("/echo") public class EchoEndPoint { @OnOpen public void onOpen(Session session) { } @OnMessage public String onMessage(String message, Session session) { return message; } @OnError public void onError(Session session, Throwable cause) { } @OnClose public void onClose(Session session) { } }
@OnOpen
といったアノテーションで、各イベントに応じた処理を定義します。
上記例は、@OnMessage
でメッセージの到着に応答し、受け取ったメッセージをそのまま返却する単純なエンドポイントです。
各アクションには Session
を引数として受け取ることができます。
Session
に対して、以下のようにしてクライアントにメッセージを送信することもできます。
session.getBasicRemote().sendText("hello");
Tyrus サーバ
Java API for WebSocket の RI である Tyrus には、tyrus-server
というモジュールがあります。
tyrus-container-grizzly-server
という Grizzly ベースのコンテナを使うことで、Java SE 環境で簡単に WebSocket サーバを利用することができます。
以下のようにすることで、/websockets
を contextPath とした WebSocket サーバ が利用できます。
Server socketServer = new Server("localhost", 8025, "/websockets", null, EchoEndPoint.class); staticServer.start();
サーバの実体は、org.glassfish.tyrus.container.grizzly.server.GrizzlyServerContainer
となり、Grizzly サーバが裏で動きます。
WebSocket クライアント
WebSocket のクライアントには Java 11 で正式導入された HTTP Client もありますが、ここでは単純な Vanilla JS で実装していきます。
WebSocket
のインスタンスを作成します。
const socket = new WebSocket('ws://localhost:8025/websockets/echo');
あとは、このインスタンスに各イベントのリスナを登録するだけです。
socket.addEventListener('open', (event) => console.log('open')); socket.addEventListener('close', (event) => console.log('close')); socket.addEventListener('message', (event) => console.log(event.data));
先程のエコーエンドポイントに対応するものとして、以下のような HTML ファイルを用意します。
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"/> <title>Echo</title> </head> <body> <h3>Echo</h3> <input type="text" id="text"/> <br> <p id="msg"></p> <script> const socket = new WebSocket('ws://localhost:8025/websockets/echo'); socket.addEventListener('open', (event) => console.log('open')); socket.addEventListener('close', (event) => console.log('close')); document.getElementById('text').addEventListener('input', (event) => socket.send(event.target.value)); socket.addEventListener('message', (event) => document.getElementById('msg').textContent = event.data); </script> </body> </html>
サーバの実装
作成したHTMLファイルを扱う HTTP サーバをついでに含めます。
tyrus-container-grizzly-server
は先程説明した通り、Grizzly を含むため、静的なHTMLファイルを扱う HTTP サーバは以下のようにするだけで実装できます。
HttpServer staticServer = HttpServer.createSimpleServer("docroot", 8080); staticServer.start();
docroot
は HTTPサーバのドキュメントルートになるのでディレクトリを作成して中に先程の HTMLを入れておけば完了です。
ここまでの流れをまとめると、Gradle の Kotlin DSL は以下のようになります。
plugins { application } repositories { mavenCentral() } dependencies { implementation("org.glassfish.tyrus:tyrus-server:2.0.0") implementation("org.glassfish.tyrus:tyrus-container-grizzly-server:2.0.0") } application { mainClass.set("com.mammb.code.example.websocket.App") } tasks.named<JavaExec>("run") { standardInput = System.`in` }
アプリケーションのメインクラスは以下のようになります。
package com.mammb.code.example.websocket; import org.glassfish.grizzly.http.server.HttpServer; import org.glassfish.tyrus.server.Server; import java.awt.Desktop; import java.net.URI; public class App { public static final String RED = "\u001B[31m%s\u001B[0m"; private final HttpServer staticServer; private final Server socketServer; public App() { this.staticServer = HttpServer.createSimpleServer("docroot", 8080); this.socketServer = new Server("localhost", 8025, "/websockets", null, EchoEndPoint.class); } public void run() { try { staticServer.start(); socketServer.start(); System.out.println(String.format(RED, "Press any key to stop the server...")); browse(); System.in.read(); } catch (Exception e) { throw new RuntimeException(e); } finally { staticServer.shutdown(); socketServer.stop(); } } public static void main(String[] args) { var app = new App(); app.run(); } }
最後に WebSocket のエンドポイントは以下のようになります。
package com.mammb.code.example.websocket; import jakarta.websocket.OnClose; import jakarta.websocket.OnError; import jakarta.websocket.OnMessage; import jakarta.websocket.OnOpen; import jakarta.websocket.Session; import jakarta.websocket.server.ServerEndpoint; @ServerEndpoint("/echo") public class EchoEndPoint { @OnMessage public String onMessage(String message, Session session) { System.out.println(" ## onMessage [" + message + "][" + session.getId() + "]"); return message; } }
実行して http://localhost:8080/echo.html
にアクセスすれば以下のようになります。
入力内容は、WebSocket にてサーバに送信され、同じ内容がエコーされ、リアルタイムで下段に表示されます。
まとめ
今回は、WebSocket 用のサーバ実装について説明しました。
WebSocket のエンドポイントは、受信した内容をそのままエコーする単純なものです。
次回はエンドポイントの実装についてもう少し詳しく見ていきます。