質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.44%
Android

Androidは、Google社が開発したスマートフォンやタブレットなど携帯端末向けのプラットフォームです。 カーネル・ミドルウェア・ユーザーインターフェイス・ウェブブラウザ・電話帳などのアプリケーションやソフトウェアをひとつにまとめて構成。 カーネル・ライブラリ・ランタイムはほとんどがC言語/C++、アプリケーションなどはJavaSEのサブセットとAndroid環境で書かれています。

TCP

TCP(Transmission Control Protocol)とは、トランスポート層のプロトコルで、コネクション型のデータサービスです。

Kotlin

Kotlinは、ジェットブレインズ社のアンドリー・ブレスラフ、ドミトリー・ジェメロフが開発した、 静的型付けのオブジェクト指向プログラミング言語です。

Q&A

解決済

1回答

4141閲覧

Android TCP通信 inputStream.readにタイムアウトを設定したい

RgrayLemon

総合スコア2

Android

Androidは、Google社が開発したスマートフォンやタブレットなど携帯端末向けのプラットフォームです。 カーネル・ミドルウェア・ユーザーインターフェイス・ウェブブラウザ・電話帳などのアプリケーションやソフトウェアをひとつにまとめて構成。 カーネル・ライブラリ・ランタイムはほとんどがC言語/C++、アプリケーションなどはJavaSEのサブセットとAndroid環境で書かれています。

TCP

TCP(Transmission Control Protocol)とは、トランスポート層のプロトコルで、コネクション型のデータサービスです。

Kotlin

Kotlinは、ジェットブレインズ社のアンドリー・ブレスラフ、ドミトリー・ジェメロフが開発した、 静的型付けのオブジェクト指向プログラミング言語です。

0グッド

1クリップ

投稿2021/10/08 13:36

編集2021/10/09 03:37

前提・実現したいこと

こちらのKotlinのコードを参考に、スマホアプリをクライアントとしてTCP通信を行うプログラムを作成しています。
socket接続とサーバーとの電文のやり取りはできたのですが、outputStreamで送信してから〇秒以内に返信がない場合、というタイムアウトを設定することができません。
socket.soTimeoutを用いればタイムアウトを設定できるそうなのですが、動作してないように見えます。(java.net.SocketTimeoutExceptionに引っ掛かりません)
どのようにすればAndroidのKotlinでTCP通信のリードタイムアウトを設定できるのでしょうか?
socket.soTimeoutを使えるようにするのでも、何かタイマーを設けるものでも、タイムアウトの判定ができればいいです。

参考(setSoTimeout)

該当のソースコード

whileで連続してサーバーに送信し続け、途中でサーバーを落とし、タイムアウト(もしくは切断)を検知できるか、という風にしています。

MainActivity

kotlin

1 2import android.os.Bundle 3import android.os.Handler 4import android.util.Log 5import android.widget.Button 6import androidx.appcompat.app.AppCompatActivity 7import kotlinx.coroutines.channels.Channel 8import java.nio.ByteBuffer 9 10 11const val MSG_CONNECTION_SUCCESS = 111 // 接続成功 12const val MSG_CONNECTION_FAILED = 222 // 接続失敗 13const val MSG_IOEXCEPTION = 333 // 例外発生 14 15class MainActivity : AppCompatActivity() { 16 private var tcpcom: ComTcpClient? = null 17 val ip = "10.0.2.2" 18 val port = "2001" 19 private val TAG = MainActivity::class.java.simpleName 20 val handler = Handler() 21 22 override fun onCreate(savedInstanceState: Bundle?) { 23 super.onCreate(savedInstanceState) 24 setContentView(R.layout.activity_main) 25 26 connect() 27 28 val runnable = object: Runnable{ 29 override fun run(){ 30 val sendArray = ByteBuffer.allocate(30) 31 sendArray.putInt(0x01020304) 32 val byteArray = ByteBuffer.allocate(30) 33 34 try { 35 tcpcom?.sendOrReceive { outputStream, inputStream -> 36 println("output") 37 outputStream.write(sendArray.array()) 38 try{ 39 println("input") 40 inputStream.read(byteArray.array()) 41 42 } catch(e: Exception){ 43 println("error") 44 } 45 } 46 } catch(e: Exception){ 47 Log.d(TAG,"Failed...") 48 } 49 handler.postDelayed(this,5000) 50 } 51 } 52 handler.post(runnable) 53 54 } 55 56 fun connect(){ 57 val channel = Channel<Int>() 58 if (!ip.isEmpty() && !port.isEmpty()) { 59 tcpcom = ComTcpClient(ip, port.toInt(), channel) 60 tcpcom?.connect() 61 } 62 } 63}

ComTCPClient
サーバー切断後もjava.net.SocketTimeoutExceptionにもIOExceptionにも引っ掛かりません……。なんならExceptionにも引っ掛かりません。

kotlin

1 2import android.util.Log 3import kotlinx.coroutines.Dispatchers 4import kotlinx.coroutines.GlobalScope 5import kotlinx.coroutines.channels.Channel 6import kotlinx.coroutines.launch 7import java.io.IOException 8import java.io.InputStream 9import java.io.OutputStream 10import java.net.Socket 11import java.net.UnknownHostException 12 13class ComTcpClient(val ip: String, val port: Int, val channel: Channel<Int>) { 14 private var socket: Socket? = null 15 private val TAG = ComTcpClient::class.java.simpleName 16 17 fun connect() { 18 GlobalScope.launch(Dispatchers.Default) { 19 Log.d(TAG, "接続開始...") 20 try { 21 socket = Socket(ip, port) 22 channel.send(MSG_CONNECTION_SUCCESS) 23 24 } catch (e: IOException) { 25 Log.e(TAG, "IOException", e) 26 channel.send(MSG_IOEXCEPTION) 27 28 } catch (e: UnknownHostException) { 29 Log.e(TAG, "UnknownHostException", e) 30 channel.send(MSG_CONNECTION_FAILED) 31 } 32 } 33 } 34 35 fun sendOrReceive(callback: (OutputStream, InputStream) -> Unit) { 36 socket?.soTimeout = 1000 37 if (socket == null) throw java.lang.IllegalStateException() 38 socket?.also { socket -> 39 GlobalScope.launch(Dispatchers.Default) { 40 try { 41 if (socket.isConnected) { 42 println("SOCKET!") 43 callback(socket.outputStream, socket.inputStream) 44 } else { 45 channel.send(MSG_CONNECTION_FAILED) 46 println("disconnect...") 47 } 48 } catch(e: java.net.SocketTimeoutException){ 49 println("Timeout!!") 50 } catch (e: IOException) { 51 channel.send(MSG_IOEXCEPTION) 52 println("socket error") 53 } 54 } 55 } 56 } 57 58 fun close() { 59 if (socket == null) throw java.lang.IllegalStateException() 60 socket?.also { socket -> 61 GlobalScope.launch(Dispatchers.Default) { 62 try { 63 if (socket.isConnected) socket.close() 64 } catch (e: IOException) { 65 channel.send(MSG_IOEXCEPTION) 66 } 67 } 68 } 69 } 70}

一応サーバー側も記載しておきます。両者はローカルループバックで接続しています。

python

1import socketserver 2 3class MyTCPHandler(socketserver.BaseRequestHandler): 4 def handle(self): 5 while(True): 6 self.data = self.request.recv(1024).strip() 7 print("{} wrote:".format(self.client_address[0])) 8 print(self.data) 9 10 send_array = (0x01020304).to_bytes(4,byteorder="big",signed="true") 11 12 self.request.sendall(send_array) 13 14if __name__ == "__main__": 15 HOST, PORT = '127.0.0.1', 2001 16 17 with socketserver.TCPServer((HOST, PORT), MyTCPHandler) as server: 18 server.serve_forever() 19

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答1

0

ベストアンサー

サーバ側は Python が分からないので java で(タイムアウトが出るように)作りましたが、ご提示のクライアントコードでタイムアウトは発生しています。

恐らく原因はそのテスト方法とコードの不備と思います。
「途中でサーバーを落とし、タイムアウト(もしくは切断)を検知できるか」と言われていますが、クライアントが read 中にソケットを切断した場合、タイムアウト(等の例外の発生)では無く read (のブロック) が戻るだけです。
必要なのはその時の read の戻り値から何が起きたのかを判断するのであり、ご提示のコードでは判断どころか戻り値の取得もしていません。

read でタイムアウトを発生させるのであれば、ソケットは繋いだままでサーバからの送信を遅らせる形でテストするべきと思います。

なお、参考とされているドキュメントは ORACLE のもので、つまり PC 用 Java のものです。
基本同じとはいえ、Android のドキュメント を参考にされたほうが良いと思います。


ハンドラでコルーチンという構造が良く分からなかったので、 java から変換する形でハンドラだけのものを作ってみました。
ByteBuffer 使用部分も素直に配列にしています。

MainActivity.kt

kotlin

1package com.teratail.a363500 2 3import android.os.Bundle 4import android.os.Handler 5import android.os.HandlerThread 6import android.util.Log 7import android.widget.Button 8import androidx.appcompat.app.AppCompatActivity 9import java.io.IOException 10import java.io.InputStream 11import java.io.OutputStream 12import java.net.InetSocketAddress 13import java.net.Socket 14import java.net.SocketAddress 15 16class MainActivity : AppCompatActivity() { 17 private lateinit var ht: HandlerThread 18 private lateinit var pinger: Pinger 19 20 override fun onCreate(savedInstanceState: Bundle?) { 21 super.onCreate(savedInstanceState) 22 setContentView(R.layout.activity_main) 23 24 ht = HandlerThread("PingerThread") 25 ht.start() 26 27 val endpoint = InetSocketAddress("10.0.2.2", 2001) //エミュレータ → ホストPC 28 pinger = Pinger(endpoint, 5000, 500, 1000) 29 30 val button = findViewById<Button>(R.id.button) //ボタンを押すと開始 31 button.setOnClickListener { pinger.start(Handler(ht.looper)) } 32 } 33 34 override fun onDestroy() { 35 super.onDestroy() 36 ht.quit() 37 pinger.stop() 38 } 39} 40 41class Pinger(val endpoint: SocketAddress, val interval: Long, val timeoutOfConnect: Int, val timeoutOfRead: Int) { 42 companion object { 43 private const val TAG = "Pinger" 44 } 45 46 private var s: Socket? = null 47 48 fun start(handler: Handler) { 49 handler.post { 50 try { 51 s = Socket().also { 52 it.connect(endpoint, timeoutOfConnect) 53 it.soTimeout = timeoutOfRead 54 val output = it.getOutputStream() 55 val input = it.getInputStream() 56 57 if (ping(output, input)) next(handler, output, input) 58 } 59 } catch (e: Exception) { 60 e.printStackTrace() 61 } 62 } 63 } 64 65 private fun next(handler: Handler, output: OutputStream, input: InputStream) { 66 handler.postDelayed({ 67 if (ping(output, input)) next(handler, output, input) 68 }, interval) 69 } 70 71 private fun ping(output: OutputStream, input: InputStream): Boolean { 72 try { 73 Log.d(TAG, "write") 74 output.write(byteArrayOf(1,2,3,4)) 75 76 Log.d(TAG, "read") 77 val buf = ByteArray(4); 78 val len = input.read(buf) 79 Log.d(TAG, "len=$len") 80 if (len < buf.size) { 81 Log.d(TAG, "切断") 82 stop() 83 return false 84 } 85 if (buf.contentEquals(byteArrayOf(1,2,3,4))) { 86 Log.d(TAG, "OK") 87 } else { 88 Log.d(TAG, "data error"); 89 } 90 } catch (e: Exception) { 91 e.printStackTrace() 92 stop() 93 return false 94 } 95 return true 96 } 97 98 fun stop() { 99 Log.d(TAG, "stop") 100 try { 101 s?.close() 102 } catch (ignore: IOException) { 103 } 104 s = null 105 } 106}

レイアウト: activity_main.xml

xml

1<?xml version="1.0" encoding="utf-8"?> 2<androidx.constraintlayout.widget.ConstraintLayout 3 xmlns:android="http://schemas.android.com/apk/res/android" 4 xmlns:app="http://schemas.android.com/apk/res-auto" 5 xmlns:tools="http://schemas.android.com/tools" 6 android:layout_width="match_parent" 7 android:layout_height="match_parent" 8 tools:context=".MainActivityJ"> 9 10 <Button 11 android:id="@+id/button" 12 android:layout_width="wrap_content" 13 android:layout_height="wrap_content" 14 android:text="START" 15 app:layout_constraintBottom_toBottomOf="parent" 16 app:layout_constraintLeft_toLeftOf="parent" 17 app:layout_constraintRight_toRightOf="parent" 18 app:layout_constraintTop_toTopOf="parent" /> 19 20</androidx.constraintlayout.widget.ConstraintLayout>

投稿2021/10/09 05:24

編集2021/10/09 12:49
jimbe

総合スコア12863

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

RgrayLemon

2021/10/09 11:35 編集

サーバーのプログラムを電文を受信してから送信するまで一定時間遅らせるように変えて試したところ、タイムアウトが生じていることが確認できました。ずっと勘違いしていたようです……ありがとうございます! ただ、このタイムアウトの目的は、電文の応答がないことによってサーバーとの接続が切れたと判断することだったので、別の方法を考える必要がありそうです。 切断に関してはjava.net.SocketExceptionなどで拾う必要があるのでしょうか?
jimbe

2021/10/09 11:50 編集

回答にも書いていますが、ソケット切断時は(例外は発生せず)ブロックしていた read が受信用に指定したパラメータの配列の大きさ未満の値を返します。(1バイトも受信していなければ -1 となります。) ですので、read の戻り値をチェックして、必要なデータ数分受信した=正常応答か、必要数に達していない( or -1 )=切断かを判断してください。 read していない状態( 5 秒の delay 中 等 ) で切断した場合、次の write → read のタイミングで read が -1 を返して切断が分かるということになると思いますので、テストしてみてください。
RgrayLemon

2021/10/10 11:07

readの戻り値を取得して-1が返るかどうかで切断を判断することもできました!ありがとうございます! Javaも似たようなものだろうとJavaのドキュメントを見ていたのですが、Androidのドキュメントに記載があったのですね。きちんと読もうと思います。streamに関してもいろいろ間違っている点がありそうなので、もう少し調べようと思います。 何はともあれ目的は達成できました。ご丁寧に詳しくありがとうございました!
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.44%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問