LoginSignup
10
7

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-12-31

はじめに

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

Project Grizzly

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

out.gif

ソースコード

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

10
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
7