WebSocket(Tyrus) + libGDX + KotlinでChatアプリを作る

## はじめに

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](https://tyrus.java.net/)

## 使うための前準備

Project Tyrusを利用するためにはdependencyを追加する必要があります。
以下2つを追加します。

```groovy
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のサブプロジェクト的な扱いになっているようですし。


[Project Grizzly](https://grizzly.java.net/)



## Tyrusの使い方(サーバーサイド)

Tyrusの使い方はとても簡単です。


### サーバーエンドポイントの実装

サーバーエンドポイントの実装はアノテーションベースの実装が提供されてるのでそちらを利用します。

具体的には、@ServerEndpointでURLマッピングを行い、@OnOpen、@OnMessage、@OnClose、@OnErrorでそれぞれWebSocketの通信をサーバーでうけた場合のライフサイクルイベント処理を実装します。


```kotlin

@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のサーバーエンドポイントがアイドル状態になります。

```kotlin
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です。

```JavaScript

ws = new WebSocket('ws://localhost:8181/ws/chat/' + guestId);
ws.onmessage = function(e) {
    // メッセージが来た時の描画処理
};

ws.send("メッセージの送信処理");

```

JavaScriptで実装してしまうのも全然悪くないですが、Tyrusにはクライアント実装用のAPI(Javaのもの)が用意されています。せっかくなのでこちらも使ってみることにしました。


### クライアントエンドポイントの実装

クライアントエンドポイントの実装方式も、基本的にサーバーエンドポイントの実装と大きく変わりはありません。アノテーションベースのものが用意されているのでそれを利用します。

試していませんが、javax.websocket.Endpointを継承したクラスをクライアントエンドポイント実装として登録することも可能なようです。


```kotlin
@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というクラスが用意されているのでそれを利用します。


```kotlin
val client = ClientManager.createClient()
client.run {
    asyncConnectToServer(clientEndpoint, URI("ws://localhost:8181/ws/chat/${text}"))
}
```


これでWebSocketのサーバーと接続が行われます。
サーバーに対してメッセージを送る場合は、以下のようにします。

```kotlin
session.asyncRemote.sendText(text)
```

## demo

![out.gif](https://qiita-image-store.s3.amazonaws.com/0/122413/22b94d78-125f-81fc-3f18-ceea841ef75f.gif)



## ソースコード

[https://github.com/yyYank/chatppoimono](https://github.com/yyYank/chatppoimono)

## 参考URL

[http://backpaper0.github.io/2013/07/14/websocket.html](http://backpaper0.github.io/2013/07/14/websocket.html)
[http://backpaper0.github.io/2013/07/15/websocket_pathparam.html](http://backpaper0.github.io/2013/07/15/websocket_pathparam.html)