この記事は フラー Advent Calendar 2019 の21日目の記事です.
前日の20日目は@AtsushiIzuさんで iOS開発におけるBitrise活用事例 でした.
はじめに
私は現在,フラー株式会社でAndroidエンジニアとしてアルバイトをさせていただいております.
そこで初めてAndroidに触り,早6ヶ月以上が経過しましたが,最近大学でも研究に使うGUIのためにAndroidアプリを作っています.そのアプリでソケット通信を行うのですが,簡単に非同期処理ができて,ナウそうなCoroutinesを使ってみることにしました.
今回の内容
今回は,サーバからクライアントに決まったメッセージを送信するだけという,めちゃくちゃ簡単なアプリを作成します.
サーバ側はPythonで,クライアント側はKotlin(Android)を使用します.サーバもKotlinで書けよというツッコミはその通りすぎるので無しでお願いします.
環境
- macOS Mojave バージョン 10.14.6
- Nexus 5X API 27 (Emulator)
- Python 3.7.4
- Kotlin 1.3.50
ソケット通信とは
ソケットは一般にクライアントとサーバーの対話で使用されます。 通常のシステム構成では、一方のマシンにサーバーを、 もう一方のマシンにクライアントを置きます。 クライアントはサーバーに接続して情報を交換し、その後切断します。
引用元: https://www.ibm.com/support/knowledgecenter/ja/ssw_ibm_i_71/rzab6/howdosockets.htm
異なるコンピュータ間の通信をいい感じに行えるということです.今回は,TCPを使用したコネクション型通信を考えています.そのため,まずサーバは,クライアントがサーバを探せるようにアドレスを確立(バインド)し,クライアントの要求を待ちます.そして,クライアントからの要求が来たとき,サーバはそれに応え,応答を送信するといった感じです.
詳しくは参考資料をご確認ください!
Coroutinesとは
軽量スレッドであり,Android上で使用して非同期のコードを簡素化できるものです.
ウェブページの取得やAPIとのやり取り,DBからのデータ取得やディスクからの画像読み込みなどの重い処理はメインスレッドで行うと,その処理が終わるまでアプリが停止してしまいます.そこで,非同期処理が必要になるわけです.そして,Coroutinesを使用することにより,非同期処理を簡単に行わせることができるということですね.
詳しくは参考資料をご確認ください!!
やってみる
サーバ
サーバ側のソースコードはこんな感じです.同階層のconfig.py
には自分のIPと任意のPORT番号が定義されています.
import socket
import time
from config import IP, PORT
def main():
with socket.socket() as s:
s.bind((IP, PORT))
s.listen(1)
while True:
client, _ = s.accept()
with client:
client.sendall(b"Hello, socket.\n")
time.sleep(3)
client.sendall(b"Hello, socket!!!!!!\n")
if __name__ == "__main__":
main()
サーバでは,接続してきたクライアントに対して,最初にHello, socket.
というメッセージを送信して,3秒後にテンション高めなHello, socket!!!!!!
というメッセージを送信するだけです.
クライアント
クライアント側の処理は,
- サーバ側のIPとPORTを指定して接続
- 送られてくるメッセージ受信
- 受信したメッセージをViewに反映
という流れです.
実際にソケット通信を行っているのは以下のコードです.
package com.runn_dev.socketsample.model
import android.util.Log
import androidx.lifecycle.MutableLiveData
import java.io.BufferedReader
import java.io.InputStreamReader
import java.net.Socket
class SocketClient(private val ip: String, private val port: Int) {
private lateinit var socket: Socket
private lateinit var reader: BufferedReader
val receivedData: MutableLiveData<String> = MutableLiveData()
fun connect() {
try {
socket = Socket(ip, port)
Log.d(TAG, "connected socket")
} catch (e: Exception) {
Log.e(TAG, "$e")
}
}
fun read() {
reader = BufferedReader(InputStreamReader(socket.getInputStream()))
try {
reader.use {
while (true) {
val message = it.readLine()
if (message != null) {
receivedData.postValue(message)
Log.d(TAG, message)
} else {
break
}
Thread.sleep(SLEEP_TIME)
}
}
} catch (e: Exception) {
Log.e(TAG, "$e")
}
}
fun close() {
if (::reader.isInitialized) {
reader.close()
Log.d(TAG, "closed reader")
}
if (::socket.isInitialized) {
socket.close()
Log.d(TAG, "closed socket")
}
}
companion object {
const val TAG = "SocketClient"
const val SLEEP_TIME = 500L
}
}
これをViewModelから呼び出して処理するのですが,Androidでは上述の通り,こういった通信をメインスレッドで行うことはできません.そこでこれらの処理を別スレッドで実行させます.
private fun read() = viewModelScope.launch {
runCatching {
withContext(Dispatchers.IO) {
socketClient.connect()
socketClient.read()
}
}.onFailure {
Log.e(TAG, it.toString())
}
}
ここで,withContext(Dispatchers.IO)
を呼び出して,コルーチンを別スレッドで実行させています.今回は,ソケット通信を行う処理をしているため,Dispatchers.IO
を使用していますが,他にもいくつか種類があります.
また,KotlinではコルーチンをCoroutineScope内で実行し,コルーチンを管理する必要があります.例えば,ユーザが画面から離れた場合に処理をキャンセルするなどの処理をデベロッパが行う必要があります.しかし,viewModel内でコルーチンを使う場合,viewModelScopeを使用することによって,viewModelのライフサイクルにあわせて自動的に処理をキャンセルしてくれます.
コルーチンを Android アーキテクチャ コンポーネントに統合する際、デベロッパーは通常、ViewModel 内でコルーチンを launch する必要があります。この場所は、最も重要な処理が開始される場所であり、すべてのコルーチンを終了させるローテーションについて心配する必要がないため、自然な場所と言えます。
引用元: https://developers-jp.googleblog.com/2019/07/coroutines-on-android-part-ii-getting-started.html
GoogleもコルーチンをviewModel内で使うことを推奨しているので,基本的にはviewModelScopeを使うのが良いかなと思います.
結果
まずサーバを立ち上げます.
$ python send.py
そして,Androidアプリを立ち上げ,IPとPORTを入力して接続します.
このとき,クライアント側では,入力されたIPとPORTを遷移先のフラグメントに渡します.そして,渡されたIPとPORTを使用してサーバに接続し,LiveDataとDataBindingによってサーバから応答がある度にViewを変更します.
ちゃんと受信できてるっぽいですね(2, 3枚目).そして,ちゃんと3秒後にはテンション高めなViewに変わっています.
全ソースコードはこちら.
おわりに
めちゃくちゃ簡単にではありましたが,Coroutinesを使用した非同期処理でソケット通信をすることができました.本当はメッセージを双方向でやりとりしたかったですが,間に合わなかったので今後の課題ということにさせてください...
また,Android歴もそんなに長くないので,ここはこうしたほうが良いとかたくさんあると思います.何かあったらガンガン指摘してください!