KotlinでWebSocketClientを作ってみる

# 検証環境
この記事の内容は、以下の環境で検証した。
* Java:open jdk 1.8.0_152
* Kotlin 1.2.10
* Android Studio 3.0.2
* CompileSdkVersion:26

# はじめに
AndroidアプリでWebSocketを使った通信をするには、Java EEのWebSocketを参照実装しているTyrusを使うのがセオリーだとは思います。しかし、ネットワーク接続はUIスレッドで出来ないため、接続処理のスレッドを立てたり、常時接続状態を確認するためのスレッドを立てたりと煩雑な実装が必要になります。そこで、簡単に実装できるJava-WebSocketライブラリーを使います。
この記事では、Java-WebSocketを利用したクライアントの実装方法について説明します。
 **サーバの実装方法は各自で調べてください。** 

# Java-WebSocketとは
Java-WebSocketとは、JavaScriptのwebsocketsによく似た実装が可能なライブラリーです。
サーバもクライアントもクラスを継承して、抽象メソッドをオーバーライドするだけで実装が完了します。

# アプリの全体像
この記事で作成するアプリの全体像は以下のとおりです。
簡単なチャットアプリです。

![アプリの全体像.png](https://qiita-image-store.s3.amazonaws.com/0/99960/96917a7c-af2d-d72c-b754-a65d9d2c58e3.png)


# アプリ作成に関係する各種ファイル
アプリを作成するために修正や作成したファイルは以下のとおりです。

|ファイル名|実装内容|新規作成 or 修正|
|:---|:---|:---|
|build.gradle|dependenciesにJava-WebSocketライブラリーを追記します。|修正|
|AndroidManifest.xml|外部ネットワーク接続の許可情報を追記します。|修正|
|activity_main.xml|画面のレイアウトを記述します。|修正|
|MyWebSocketClient.kt|Java-WebSocketライブラリーのクラスを継承して実装したクラスを作成します。|新規作成|
|MainActivity.kt|ボタンが押された時の処理を実装します。|修正|

# コードの説明
## build.gradle
dependenciesにJava-WebSocketを追記します。

```groovy:build.gradle
dependencies {
    compile "org.java-websocket:Java-WebSocket:1.3.8"
}
```
## AndroidManifest.xml
ネットワークに接続するため、uses-permissionを追記する必要があります。

```xml:AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET" />
```

## activity_main.xml
画面には以下のビューが含まれています。

|ビュー|内容|種類|
|:--|:--|:--|
|コンテンツの表示ビュー|チャットの内容などを表示します。|TextView|
|発言内容の入力ビュー|発言する内容を入力をします。|EditText|
|送信ボタン|サーバに発言内容を送信します。|Button|
|接続ボタン|サーバに接続します。|Button|
|切断ボタン|サーバから切断します。|Button|

```xml:activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="jp.co.casareal.websocketclientkotlin.MainActivity">

    <TextView
        android:id="@+id/messageList"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1.0"
        android:textSize="20dp" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <EditText
            android:id="@+id/editSendMessage"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1.0"
            android:textSize="20dp" />

        <Button
            android:id="@+id/btnSendMessage"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="送信" />
    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <Button
            android:id="@+id/btnConnect"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1.0"
            android:text="接続" />

        <Button
            android:id="@+id/btnDisconnect"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1.0"
            android:enabled="false"
            android:text="切断" />
    </LinearLayout>

</LinearLayout>
```

## MyWebSocketClient.kt
WebSocketのクライアントそのもののクラスです。
このクラスは「org.java_websocket.client.WebSocketClient」を継承しています。
コンストラクタで接続するURIクラスを渡します。また、MyWebSocketClientクラス内でfindViewByIdメソッドを呼び出すため、コンストラクタでActivityクラスを引数に受け取っています。

WebSocketClientクラスでは抽象メソッドをオーバーライドする必要があります。
メソッドの詳細は以下のとおりです。

|メソッド名|詳細|
|:--|:--|
|onOpen|サーバに接続すると呼び出されます。|
|onClose|サーバから切断すると呼び出されます。|
|onError|サーバとの接続でエラーが発生すると呼び出されます。|
|onMessage|サーバからメッセージを受信すると呼び出されます。|

今回は各メソッドでは、基本的にはログを出力し、onMessageメソッドでは受け取った文字列をTextViewに追記しているだけです。
しかし、これらのメソッドは全てUIスレッド上で動作していないため、TextViewの追記処理だけは、ActivityのrunOnUiThreadメソッドを利用し、UIスレッド上で動作するようにしています。

```kotlin:MyWebSocketClient.kt
package jp.co.casareal.websocketclientkotlin

import android.app.Activity
import android.util.Log
import android.widget.TextView
import org.java_websocket.client.WebSocketClient
import org.java_websocket.handshake.ServerHandshake
import java.lang.Exception
import java.net.URI


/**
 * Created by naoikotaro on 2018/03/27.
 */
class MyWebSocketClient(val activity: Activity, uri: URI) : WebSocketClient(uri) {


    private val contentView: TextView by lazy {
        activity.findViewById<TextView>(R.id.messageList)
    }

    private val breakLine = System.lineSeparator()


    override fun onOpen(handshakedata: ServerHandshake?) {
        Log.i(javaClass.simpleName, "WSサーバに接続しました。")
        Log.i(javaClass.simpleName, "スレッド：「${Thread.currentThread().name}」で実行中")

    }

    override fun onClose(code: Int, reason: String?, remote: Boolean) {
        Log.i(javaClass.simpleName, "WSサーバから切断しました。reason:${reason}")
        Log.i(javaClass.simpleName, "スレッド：「${Thread.currentThread().name}」で実行中")
    }

    override fun onMessage(message: String?) {
        Log.i(javaClass.simpleName, "メッセージを受け取りました。")
        Log.i(javaClass.simpleName, "スレッド：「${Thread.currentThread().name}」で実行中")
        activity.runOnUiThread {
            contentView.append("$message")
            contentView.append("$breakLine")
            Log.i(javaClass.simpleName, "メッセージをTextViewに追記しました。")
            Log.i(javaClass.simpleName, "スレッド：「${Thread.currentThread().name}」で実行中")

        }
    }

    override fun onError(ex: Exception?) {
        Log.i(javaClass.simpleName, "エラーが発生しました。", ex)
        Log.i(javaClass.simpleName, "スレッド：「${Thread.currentThread().name}」で実行中")
    }
}
```

## MainActivity.kt
このクラスでは、ボタンが押された時の処理を中心に実装しています。
また、クラスのフィールドとして、MyWebSocketClientを定義しています。併せて、オブジェクトの生成も行っています。
各ボタンの処理内容は以下のとおりです。


|ボタン|処理内容|
|:--|:--|
|送信ボタン|WebSocketClient#sendメソッドを呼び出してサーバへ発言内容を送信しています。|
|接続ボタン|WebSocketClient#connectメソッドを呼び出してサーバへ接続しています。|
|切断ボタン|WebSocketClient#closeメソッドを呼び出してサーバから切断しています。|

```kotlin:MainActivity.kt
package jp.co.casareal.websocketclientkotlin

import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*
import java.net.URI

class MainActivity : AppCompatActivity() {

    private val uri = URI("ws://10.0.2.2:4567/chat")
    private val client = MyWebSocketClient(this, uri)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    override fun onResume() {
        super.onResume()

        // 接続ボタンが押下された時
        btnConnect.setOnClickListener {
            client.connect()
            it.isEnabled = false
            btnDisconnect.isEnabled = true
        }

        // 切断ボタンが押下された時
        btnDisconnect.setOnClickListener {
            client.close()
            btnConnect.isEnabled = true
            btnDisconnect.isEnabled = false
        }

        // 送信ボタンが押下された時
        btnSendMessage.setOnClickListener {
            client.send(editSendMessage.text.toString())
            messageList.append(editSendMessage.text.toString())
        }

    }
}
```

# まとめ
チャットというとなんとなくハードルが高いイメージがありますが、意外と簡単に作成できます。
また、この記事を作成するにあたり、様々なライブラリーを調べました。
どのライブラリーも似たようなメソッドがあり、似たような役割を担っていました。
まずは、このJava-WebSocketでWebSocketのクライアントを理解した後に、他のライブラリーを見てみると、思いの外、他のライブラリーも早く理解できるんではないかと思います。

# 参考
https://github.com/TooTallNate/Java-WebSocket