READMEには書いてないけど、AndroidAsyncでUDP通信ができるらしいことがわかったので、簡単なUDPメッセージをやりとりするプログラムを書いてみました。
最近流行りのKotlinで書いてますが、もちろんJavaでも利用可能です。
UDP以外の通信の例は公式のREADMEを読んでください。HTTPのサーバー/クライアント、WebSocketのサーバー/クライアント、socket.ioクライアントとしての通信ができます。
UDPメッセージを受け取る
41234番ポートで待ち受けて、受け取ったメッセージを文字列としてログに流すだけのシンプルなプログラムです。
class MyActivity: Activity() {
private lateinit var socket: AsyncDatagramSocket
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 41234番ポートで待ち受けるUDPサーバーを作成 ※
socket = AsyncServer.getDefault().openDatagram(InetSocketAddress(41234), true)
// UDPメッセージのコールバック
// このコールバックはメインスレッドではなくバックグラウンドスレッドから呼び出されるので注意すること!
socket.setDataCallback { emitter, bb ->
// byte[]に変換
val data = bb.allByteArray
// とりあえずログへ出力
println("received ${String(data)}")
}
}
override fun onDestroy() {
super.onDestroy()
// サーバーの後片付け
socket.disconnect()
AsyncServer.getDefault().stop()
}
}
※ この第二引数のbooleanは内部的にServerScoket.setReuseAddressに渡されています。このドキュメントによればこの値は次のような意味です。
TCP接続をクローズする場合、接続クローズ後の一定期間、その接続がタイム・アウト状態(通常、TIME_WAIT状態または2MSL待機状態と呼ばれる)にとどまる可能性があります。既知のソケット・アドレスまたはポートを使用するアプリケーションの場合、ソケット・アドレスまたはポートに関連する接続がタイム・アウト状態にあると、ソケットを必要なSocketAddressにバインドできないことがあります。
bind(SocketAddress)を使用してソケットをバインドする前にSO_REUSEADDRを有効にすると、以前の接続がタイムアウト状態でもソケットをバインドできます。
これをfalseにしておくとタイミングによってはポートが空いてそうなのに使えないことがあるということでしょうかね?trueにしておいたほうが、原因不明な動作不良は起こりにくそう…?
UDPメッセージを送る
今度は逆にUDPメッセージを送信するプログラムです。タッチイベントの内容を文字列化して送っています。
class MyActivity: Activity() {
private lateinit var socket: AsyncDatagramSocket
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 空いているポートを使ってソケットを開く
socket = AsyncServer.getDefault().openDatagram()
}
override fun onTouchEvent(event: MotionEvent): Boolean {
// タッチイベントを文字列化して、byte[]にする
val data = event.toString().toByteArray()
// byte[]をByteBuffer化
val buffer = ByteBuffer.wrap(data)
// 宛先ホスト名とポート番号を指定して送信する
socket.send("192.168.1.20", 41234, buffer)
return true
}
override fun onDestroy() {
super.onDestroy()
// クライアントの後片付け
socket.disconnect()
AsyncServer.getDefault().stop()
}
}
上記二つを見比べてもらえばわかるとおり、UDP通信ではAPI上での明確なサーバー/クライアントの区別は存在せず、サーバーとして準備したソケットをクライアントとして使うことも可能です。Webのプログラミングから入った人だと最初はちょっと違和感があるかもしれませんね。
ローカルネットワーク内にブロードキャストする
UDPではブロードキャストアドレスを宛先に指定することで、ローカルネットワーク内の全ての端末にメッセージを送ることができます。これを使ってローカルネットワーク内で自動的に接続相手のIPアドレスやポート番号等の接続に必要な情報を取得したりアプリの制御メッセージを一斉送信したりすることができます。
ブロードキャストアドレスについては各自で調べていただくとして、AndroidAsyncでブロードキャストの送受信を行う方法を書きます。
先程の例で使用しているAsyncDatagramSocket
はデフォルトではブロードキャスト送信ができません。宛先にブロードキャストアドレスを指定しても単純に無視され何も起きません。ブロードキャスト送信をするためには、自分が作ったUDPソケットがブロードキャストアドレスを宛先として使用するということを予め明示しなければいけません。
これを行うためのJavaの標準的なAPIとしてはDatagramSocket.setBroadcastがありますが、AndroidAsyncは内部的にこのクラスを使っているものの隠蔽しているためこのAPIを直接呼び出すことができません。
なんとかならないかなと思っていろいろ見ていたら、一応このコードでちょっと無理やり呼び出すことができました。
val udpSocket = AsyncServer.getDefault().openDatagram()
(udpSocket.socket as DatagramSocket).broadcast = true // ブロードキャストを有効にする
これで宛先をブロードキャストアドレスに指定してメッセージを送信できるようになります。