はじめに
WebSocketを利用してとても簡素なチャットアプリを作成しました。
前提
Tyrus version 1.13
Kotlin 1.0.5
libGDX 1.9.5
Project Tyrus
Project Tyrusは、JSR 356 - Java API for WebSocketのRI(参照実装)です。
Project Tyrus
使うための前準備
Project Tyrusを利用するためにはdependencyを追加する必要があります。
以下2つを追加します。
compile group: 'org.glassfish.tyrus', name: 'tyrus-container-grizzly-server', version: tyrusVersion
compile group: 'org.glassfish.tyrus', name: 'tyrus-server', version: tyrusVersion
gradle buildして、以下のjarがローカルリポジトリに落とされているのを確認しました。
org.glassfish.grizzly:grizzly-framework:2.3.25
org.glassfish.grizzly:grizzly-http:2.3.25
org.glassfish.grizzly:grizzly-http-server:2.3.25
org.glassfish.tyrus:tyrus-client:1.13
org.glassfish.tyrus:tyrus-container-grizzly-client:1.13
org.glassfish.tyrus:tyrus-container-grizzly-server:1.13
org.glassfish.tyrus:tyrus-core:1.13
org.glassfish.tyrus:tyrus-server:1.13
org.glassfish.tyrus:tyrus-spi:1.13
アバウトに言うと、grizzlyはglassfishでも使われているWebサーバーのフレームワークです。
Tyrus自体、glassfishのサブプロジェクト的な扱いになっているようですし。
Tyrusの使い方(サーバーサイド)
Tyrusの使い方はとても簡単です。
サーバーエンドポイントの実装
サーバーエンドポイントの実装はアノテーションベースの実装が提供されてるのでそちらを利用します。
具体的には、@ServerEndpointでURLマッピングを行い、@OnOpen、@OnMessage、@OnClose、@OnErrorでそれぞれWebSocketの通信をサーバーでうけた場合のライフサイクルイベント処理を実装します。
@ServerEndpoint("/chat/{guest-id}")
class ServerEndPoint {
companion object {
private val sessions = CopyOnWriteArraySet<Session>()
}
@OnOpen
fun onOpen(@PathParam("guest-id") guestId: String, session: Session) {
println("server-[open] $guestId")
sessions.add(session)
for (s in sessions) {
s.asyncRemote.sendText("${guestId}さんが入室しました")
}
}
@OnMessage
fun onMessage(@PathParam("guest-id") guestId: String, message: String, session: Session) {
println("server-[message][$message] $session")
// broadcast
for (s in sessions) {
println("requestURI" + s.requestURI.toString())
s.asyncRemote.sendText("[$guestId] $message")
}
}
@OnClose
fun onClose(@PathParam("guest-id") guestId: String, session: Session) {
println("server-[close] " + session)
sessions.remove(session)
// broadcast
for (s in sessions) {
s.asyncRemote.sendText(guestId + "さんが退室しました")
}
}
@OnError
fun onError(session: Session, t: Throwable) {
println("server-[error] " + session)
}
}
この実装では、CopyOnWriteArraySetにユーザーセッションをキャッシュしてブロードキャストする仕組みにしていますが、ちゃんと設計していくならJavaのメモリ上でなくRedisやDBなどを利用するほうが良いと思います。
試していませんが、javax.websocket.Endpointを継承したクラスをサーバーエンドポイント実装として登録することも可能なようです。
サーバーの起動ロジック実装
そして、サーバーインスタンスを生成してスタートするだけで組み込みサーバーが起動して、
登録したWebSocketのサーバーエンドポイントがアイドル状態になります。
object Main {
@JvmStatic fun main(args: Array<String>) {
val server = Server("localhost",/*接続ホスト*/ 8181,/*ポート*/,
"/ws" /*コンテキストルート*/,mapOf(),/*設定プロパティ*/
ServerEndPoint::class.java/*サーバーエンドポイント*/
)
try {
server.start()
System.`in`.read()
} finally {
server.stop()
}
}
}
server.start()でTyrusのコンテナに対してデプロイを行ってくれます。
(このコードを例にすると)localhost:8181にアクセスすればサーバーが起動していることが分かるはずです。
以上で、サーバーサイドのWebSocketの実装が出来上がりです。あとはクライアントを用意してあげてWebSocketで通信すればOKです。
Tyrusの使い方(クライアントサイド)
WebSocketのクライアント実装でよく見るのはJavaScriptです。
ws = new WebSocket('ws://localhost:8181/ws/chat/' + guestId);
ws.onmessage = function(e) {
// メッセージが来た時の描画処理
};
ws.send("メッセージの送信処理");
JavaScriptで実装してしまうのも全然悪くないですが、Tyrusにはクライアント実装用のAPI(Javaのもの)が用意されています。せっかくなのでこちらも使ってみることにしました。
クライアントエンドポイントの実装
クライアントエンドポイントの実装方式も、基本的にサーバーエンドポイントの実装と大きく変わりはありません。アノテーションベースのものが用意されているのでそれを利用します。
試していませんが、javax.websocket.Endpointを継承したクラスをクライアントエンドポイント実装として登録することも可能なようです。
@javax.websocket.ClientEndpoint
class ClientEndpoint(val stage: Stage, val scene: ChatScene) {
val defaultOffsetY = 490
val offsetY = AtomicInteger(defaultOffsetY)
val textHeight = 22
@OnOpen
fun onOpen(session: Session, config: EndpointConfig) {
println("client-[open] " + session)
scene.session = session
}
@OnMessage
fun onMessage(message: String, session: Session) {
scene.session = session
println("client-[message][$message] $session")
println("----------")
Gdx.app.postRunnable {
// ※Textはテキスト表示用自作コンポーネント
// Y座標をずらしてテキストが並んでいくようにしている
val textObject = Text(message, 20, Position(0f, offsetY.addAndGet(-textHeight).toFloat()))
textObject.let {
it.setPosition(0f, 0f)
stage.addActor(it)
}
}
}
@OnClose
fun onClose(session: Session) {
println("client-[close] $session")
}
@OnError
fun onError(session: Session?, t: Throwable?) {
println("client-[error] ${t?.message} $session")
}
}
クライアントエンドポイントの呼び出し
クライアントの呼び出しはClientManagerというクラスが用意されているのでそれを利用します。
val client = ClientManager.createClient()
client.run {
asyncConnectToServer(clientEndpoint, URI("ws://localhost:8181/ws/chat/${text}"))
}
これでWebSocketのサーバーと接続が行われます。
サーバーに対してメッセージを送る場合は、以下のようにします。
session.asyncRemote.sendText(text)
demo
ソースコード
https://github.com/yyYank/chatppoimono
参考URL
http://backpaper0.github.io/2013/07/14/websocket.html
http://backpaper0.github.io/2013/07/15/websocket_pathparam.html