概要
androidにWebSocketサーバを構築する方法です。以下のライブラリを使用させていただいています。
開発するための大まかな手順です。
- ライブラリを使えるようにする
build.gradle (Module)の編集 - インターネットアクセスの許可をアプリに与える
AndroidManifest.xmlの編集 - WebSocket Server用のクラスを作成する
SampleServer.ktを作成する - WebSocket Server用のクラスを使用する
MainActivityを編集する
必要に応じ自機のipを取得する - 接続テストのためにWebSocketクライアントを作成する
- 接続の確認
開発環境
Android Studioを使って開発しています。またこの記事を作成したときの開発環境を設定は次のようです。この設定でなくても動作すると思われますが参考に載せておきます。
項目 | 設定値 |
---|---|
Target SDK Version | 33 (API 33:Android Tiramisu) |
Min SDK Version | 29 (API 29:Android 10.0(Q)) |
Compile Sdk Version | 33 (API 33:Android Tiramisu) |
設定の確認は、Android Studioの Fileメニュー - Project Structure... で表示される画面の左側で Modules を選択したときの Properties, Default Config タブをクリックすると表示されます。
build.gradle (Module)
javaのwebsocketライブラリを使用します。build.gradle (Module)に以下の文を追加します。以下の implementationの一文を dependenciesの中に記述してください。dependenciesがない場合、dependenciesの項目も追加してください。
dependencies {
implementation "org.java-websocket:Java-WebSocket:1.5.1"
...
}
ファイルの編集後に、画面右上に表示される「sync now」をクリックして同期してください。
AndroidManifest.xml
AndroidManifest.xmlでインターネットアクセスの許可を追加します。2つ目のACCESS_NETWORK_STATEはAndroidで自機のipアドレスを取得するために使用します。以下の uses-permission の2行を manifest 直下に追加してください。
<manifest>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application />
</manifest>
SampleServer class
WebSocketサーバのクラスです。新しくkotlinのクラスをSampleServerという名前で作成してください。パッケージ名が com.example.package_here になっていますので適宜変えてください。
package com.example.package_here
import android.util.Log
import org.java_websocket.WebSocket
import org.java_websocket.handshake.ClientHandshake
import org.java_websocket.server.WebSocketServer
import java.net.InetSocketAddress
class SampleServer {
private val cons = ArrayList<WebSocket>()
private val server = object :WebSocketServer(InetSocketAddress(8008)) {
override fun onOpen(conn: WebSocket?, handshake: ClientHandshake?) {
conn?.send("hello")
this@SampleServer.cons.add(conn!!)
}
override fun onClose(conn: WebSocket?, code: Int, reason: String?, remote: Boolean) {
this@SampleServer.cons.remove(conn!!)
val n = this@SampleServer.cons.size
Log.d("debug", "onClose conn.size=$n")
}
override fun onMessage(conn: WebSocket?, message: String?) {
cons.forEach {
it.send("$message")
}
}
override fun onError(conn: WebSocket?, ex: Exception?) {
}
override fun onStart() {
}
}
fun start(){
server.start()
}
}
サーバへの接続要求があるときに onOpen が呼ばれます。上記ではクライアントに hello という文字を返してソケットを保存しておきます。接続が切断されると onClose が呼び出されて保存しておいたソケットを削除します。
クライアントから文字が送られると onMessage が呼ばれます。上の処理では受け取った文字を送信してきたクライアントを含めて全クライアントに送っています。
エラーが起きたとき、サーバが起動したときには onError, onStartがそれぞれ呼び出されます。
MainActivityの編集
MainActivityの一部です。SampleServerクラスの変数を作成してstartメソッドを呼び出します。
class MainActivity : AppCompatActivity() {
private val server = SampleServer()
override fun onCreate(savedInstanceState: Bundle?) {
// .....
server.start()
}
// .....
}
(必要に応じ)自機のipアドレスの取得
WebSocketサーバに接続するためには、androidのipアドレスを知る必要があります。androidの設定からipアドレスを確認する方法もありますが、ここではプログラムで取得する方法を載せておきます。MainActivityのonCreateメソッド内などに記述すると動作します。
val cm = this.applicationContext.getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager
val addresses = cm.getLinkProperties(cm.activeNetwork)?.linkAddresses
addresses?.let {
val address = it.filter { it -> it.address.address.size == 4}
// ログの出力
Log.d(TAG, address.toString())
// 画面に表示
findViewById<TextView>(R.id.tx_ip_address).apply{
text = address.toString()
}
}
JavaScriptでWebSocketクライアントを作成
WebSocketサーバが起動して動作しているか確認するために、JavaScriptで作成したWebSocketクライアントを載せます。
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body>
<div>
<label for="ip">ip address</label>
<input type="text" id="ip" value="192.168.1.1">
</div>
<div>
<label for="port">port</label>
<input type="text" id="port" value="8008">
</div>
<button id="connect">connect</button>
<input id="message_to_send" value="abc"></input>
<button id="send">send</button>
<ul id="received"></ul>
</body>
<script>
let sock = undefined;
document.getElementById("send").addEventListener("click", e => {
sock?.send(document.getElementById("message_to_send").value);
})
document.getElementById("connect").addEventListener("click", e => {
const ip = document.getElementById("ip").value;
const port = document.getElementById("port").value;
sock = new WebSocket(`ws://${ip}:${port}`);
sock.addEventListener("open", e => {
console.log(`open ${e}`)
})
sock.addEventListener("message", e => {
console.log(e);
const elem = document.createElement("li");
elem.textContent = `type:${e.type}, data:${e.data}`;
document.getElementById("received").appendChild(elem);
})
})
</script>
</html>
接続の確認
- AndroidとWebSocketのクライアントhtmlファイルを実行するPCとを同一ネットワークに接続します。
- AndroidでWebSocketサーバのアプリを起動します。
- JavaScriptの入ったHTMLを開きます。作成した html ファイルをダブルクリックして fileプロトコルで開いて大丈夫です。
- ip addressをandroidのipに変えます。ポート番号はWebSocketサーバをサンプルのまま実装していれば変える必要はありません。
- connectボタンを押します。サーバがサンプルのままなら画面に type:message, data:hello と表示されます。
- 接続に失敗したときのメッセージは、consoleに表示されます。ブラウザの開発者ツールでコンソールを開いてみてください。
- sendボタンを押すと左の入力欄に書いた文字がサーバに送られます。サーバは受け取った文字を送信したクライアントも含めて全クライアントにブロードキャストします。サーバから戻ってくる文字が画面に表示されます。
クライアントは複数でも同時接続できるので、別の画面でhtmlファイルを開いて接続してみてください。サンプルのWebSocketサーバはクライアントから受信したメッセージをすべての接続クライアントにブロードキャストしますので、上記のように片方から送ったメッセージを他方で確認することができます。
WebSocketサーバの使用例
Androidに立てたWebSocketサーバは、↑ の1,2のように他のPCやスマホとWebSocket通信を行うだけでなく、3のようなAndroidのブラウザで開いたHTMLファイルからlocalhostを接続先としてつなげることもできます。その利点は、Androidネイティブアプリケーションでなければ実行できないような処理をJavaScript側から呼び出すことができることです。近年のJavaScriptはWebUSBによってUSBアクセス、File System Access APIによってローカルファイルへのアクセスが可能になってきていてJavaScriptの利用範囲は広がっていますが、まだAndroidネイティブアプリでなければ使えないデバイスなどもあり、それをJavaScriptから扱えるようにしてWebインターフェースでのアプリを作成できるようになります。またAndroidサービスのUIをWebにするということも可能になります。
注意点
- アプリを再起動したときにWebSocketサーバの起動に失敗することがあります。おそらくポートが開放される前に起動しようとしたからです。起動に失敗すると onError が呼び出されます。そこでリトライするか、少し待ってからアプリを起動してみてください。
- MainActivityでWebSocketサーバを起動するだけだと、基本的にはバックグラウンドでサーバを実行し続けることはできません。androidで他のアプリに切り替わるとWebSocketサーバは落ちます。バックグラウンドで実行し続けるWebSocketサーバを作るには、androidのForegroundサービスを使う必要があります。
bindに失敗したときにリトライする方法
bindとは、アプリが通信をするときにポート番号をシステム側に割り当ててもらうことです。WebSocketサーバのポート番号は、WebSocketをhttpやhttpsと一緒に使うときは80番や443番を使用しますが、今回は単独なのでとりあえず8008番としています。クライアントはこの8008番へ接続して通信を開始します。
ポート番号がすでに他のアプリで使われていると bind に失敗します。今回のアプリでは、アプリが終了するときに bind していたポート番号を開放することが上手くできないので、アプリが終了してもしばらく8008番のポート番号が開放されていないことがあります。そのときにこのアプリを起動すると bind に失敗します。
本来は、onDestroy などでしっかりポート番号を開放したいところですが、なぜかonDestroyが呼ばれないので起動時に bind の失敗でサーバが起動できないときは、起動のリトライをするという方法で回避したいと思います。そこコード例を以下に示します。
package com.example.package_here
import android.util.Log
import org.java_websocket.WebSocket
import org.java_websocket.handshake.ClientHandshake
import org.java_websocket.server.WebSocketServer
import java.net.BindException
import java.net.InetSocketAddress
class WebSocketServer {
private lateinit var server:WebSocketServer
private lateinit var onMessage: (WebSocket, String) -> Unit
private lateinit var onStart: () -> Unit
private fun initWebsocketServer(){
server = object :WebSocketServer(InetSocketAddress(8008)) {
private val cons = ArrayList<WebSocket>()
override fun onOpen(conn: WebSocket?, handshake: ClientHandshake?) {
conn?.send("hello")
cons.add(conn!!)
}
override fun onClose(conn: WebSocket?, code: Int, reason: String?, remote: Boolean) {
cons.remove(conn!!)
Log.d("debug", "onClose conn.size=${cons.size}")
}
override fun onMessage(conn: WebSocket?, message: String?) {
conn?.let { message?.let{
this@WebSocketServer.onMessage(conn,message)
} }
}
override fun onError(conn: WebSocket?, ex: Exception?) {
Log.d("debug","onError $conn, $ex")
if(ex is BindException){
Thread {
Thread.sleep(5000)
initWebsocketServer()
}.start()
}
}
override fun onStart() {
this@WebSocketServer.onStart()
}
}
server.start()
}
fun start(onMessage: (WebSocket,String) -> Unit, onStart:()->Unit){
this@WebSocketServer.onMessage = onMessage
this@WebSocketServer.onStart = onStart
initWebsocketServer()
}
}
kotlinの新しいクラスをWebSocketServerという名前で作成し、1行目の package ... 以外をコピーすれば完成します。ポート番号は8008、リトライは5秒待ってから再度 bind を試みます。リトライは永遠に繰り返します。
class MainActivity : AppCompatActivity() {
private val server = WebSocketServer()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// WebSocketサーバの起動
server.start(
// メッセージを受信したときの処理, 引数はクライアントのWebSocket, 受信メッセージ
{ _, message ->
Log.d("debug", message)
},
// サーバが起動したときに呼ばれる処理
{
// 例えばサーバが起動したことを画面に表示する
findViewById<TextView>(R.id.tx_status).apply{
text = "WebSocket server started"
}
})
//....
例えば MainActivity で起動する場合、サーバ変数を作成し、startメソッドを呼び出します。引数のラムダ式は、クライアントからメッセージを受信したときと、サーバが起動したときに呼び出されるようになっています。
参考
java websocket server
ConnectivityManager
Foreground service
WebUSB
File System Access API