スマフォアプリ(java/kotlin/Swift/Unity)からサーバ(java)のmariadbを操作する.6




●前提

同時接続は多数
サーバ言語:java+mariaDB+JDBCドライバ
MySQLのrootユーザー名root,passwordは1234abcde
クライアント言語:android-java/android-kotlin/iOS-Swift/unityC#
通信ポート:3456




今回はkotlinでandroid版の記述をします。kotlinの初歩がわかれば読めると思います。activity_main.xmlは変わっていませんので、前のページの物をそのまま使ってください。
androidでsocketなのでスレッドを生成して(socketは非同期通信なので)そこでjavaのsocketライブラリを使っています(今現在、androidライブラリにsocketライブラリはありません)。

ちょっと手続き型っぽく無理矢理書きましたが、kotlin風に書くならばresDataを委任プロパティで監視して変更があったら高階関数を使ってコールバックするかな。swiftならプロパティオブザーバ(willSet/didSet)かな。


    var resData: String by Delegates.observable("") {
        prop,prev,next ->
            onSetScore(next)
    }


kotlinは知らない人が見たら「何じゃこりゃ(松田優作)」って言う書き方があるので、android-kotlinで基礎を学んだらkotlinだけの本を一冊読めば良いかなと思います。なかなか奥の深い言語です。

swiftとkotlinでどちらが究極的にコード量を短く出来るかは、ルールによります(swiftは関数の引数にラベル付けないと行けないルールだとkotlinかな))。

kotlinはinline宣言で関数をマクロ化してコンパイル時に展開されるので、スピードがクリティカルな部分はinlineで実装すれば良いでしょう(本当にクリティカルなら別スレッドにすれば良いと思います)。実際にバイトコードに展開されたコードを見ましたが綺麗ですね。それとラムダ式と無名関数はラベルを大量に吐き出します。こればjavaとの相互運用のためでしょうが無いと思います。ラムダ式と無名関数がないkotlinなんて、kotlinじゃあありませんし。

swiftだと裏技でも無いのですがobjective-cの機能を併用をすれば#defineマクロが使えますが、#defineマクロで関数を積極的に使うべきでは無いと思います。

kotlinだと関数ポインタを設定できるので、それはswiftに対してはアドバンテージですが、swiftはinoutで引数の値を変えられます。

どちらも、変態的、もとい、言語仕様マックスまで使えば、総タイプ数はあまり変わらないと思います。

プロトコルと言うとプログラマならば一番先に思い出すのは通信プロトコルだと思いますが、kotlin/javaのインターフェースはswiftではプロトコルと言います。c++だと多重継承や純粋仮想関数で実現した物をインターフェースクラスと言います。アダプタークラスはandroidでは必須ですよね。この様に結構言語間、実行環境間で言葉が違うのは混乱しますよね。

kotlinもswiftも多重継承が出来ませんが、kotlinのインターフェースやswiftのプロトコル(swiftではkotlinのインターフェースをプロトコルと言います)で、多重継承っぽい組み方は出来ます。

どちらにしろ、kotlinもswiftもプロパティが相当強いので便利です。

次のswiftは多分RxSwift使います。だったら、javaやkotlinもRx使えよって言う言葉は無視します。

そう言えばkotlinはaltJS(要するにbabel等でjavascriptに変換できる言語)に対応になったのですね〜。TypeScriptやECMAとどういう関係になるんでしょうかねぇ。CoffesSriptもaltJSっすね〜。ReactNativeでaltJSにkotlin使ったら、ふつーにandroidstudioでkotlin書きますよね^^;

久しぶりにc99で組んでみましたが、関数のオーバーロードが無いのが一番きついですね。可変長引数の関数やマクロで分けることが可能ではありますが、それだと引数の個数だけで分けられる所詮は疑似マイクロオーバーロードなので、c++に変更しました^^;

AndroidManifest.xmlに以下の分を追加してください。今回はそんなところで。

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

[MainActivity.kt]


package com.example.apk0011

import android.content.Context
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import android.widget.Toast

class MainActivity : AppCompatActivity() {

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

        // 接続ボタンを押されたら接続する
        val btnConnect = findViewById<Button>(R.id.btnConnect)
        btnConnect.setOnClickListener {
            SocketClient.connect()
        }

        // 切断ボタンを押されたら切断する
        val btnDisconnect = findViewById<Button>(R.id.btnDisconnect)
        btnDisconnect.setOnClickListener {
            SocketClient.disconnect()
        }

        // 指定された順位から指定された人数分のスコア表示
        val btnScore = findViewById<Button>(R.id.btnScore)
        btnScore.setOnClickListener {
            hideInput()        // 入力を消す
            val editTop = findViewById<EditText>(R.id.editTop)
            val editCount = findViewById<EditText>(R.id.editCount)
            val cmdScore = "2," + editTop.text.toString() + "," + editCount.text.toString()
            Toast.makeText(applicationContext, cmdScore, Toast.LENGTH_LONG).show()
            val regex = Regex("2,[1-9][0-9]{0,},[1-9][0-9]{0,1}");
            if(regex.containsMatchIn(cmdScore)) {
                // 返り値の初期化
                SocketClient.initStr()
                SocketClient.sendStr(cmdScore)
                // 返り値を取得(大体一秒だけ待つ
                val textView = findViewById<TextView>(R.id.txtResult);
                textView.text = "スコア取得中";
                val scoreLine = SocketClient.getStr(100)
                when(scoreLine){
                    ""   -> textView.text = "スコア取得失敗!(タイムアウト)"    // タイムアウトした場合
                    "-1" -> textView.text = "スコア取得失敗!(接続されていません)"    // そもそも接続されていなかった場合
                    else -> {
                        // スコア取り出して成型。何度も呼ばれるようだったらStringの拡張関数か関数にしてもいいかもしれません
                        val arry = scoreLine.split(",")
                        var scoreOut = arry[0] + "人目\n"
                        val end = arry.size - 1			// arryの要素数はforの中で変わらないし、.sizeの計算量がO(1)と期待できないため
                        for (i in 1..end step 2) scoreOut += "score=" + arry[i] + " name =" + arry[i + 1] + "\n"
                        textView.text = scoreOut
                    }
                }
                // 返り値の初期化
                SocketClient.initStr()
            }
        }

        // スコアの登録
        val btnInsert = findViewById<Button>(R.id.btnInsert)
        btnInsert.setOnClickListener {
            hideInput()        // 入力を消す
            val editScore = findViewById<EditText>(R.id.editScore)
            val editName = findViewById<EditText>(R.id.editName)
            val cmdInsert = "3," + editScore.text.toString() + "," + editName.text.toString()
            Toast.makeText(applicationContext, cmdInsert, Toast.LENGTH_LONG).show()
            val regex = Regex("3,[0-9]{1,},[^ ]{1}.{0,31}");
            if(regex.containsMatchIn(cmdInsert)){
                SocketClient.sendStr(cmdInsert)
             }
        }
    }

    // 入力を消す
    fun hideInput() {
        val view = currentFocus
        if (view != null) {
            val manager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
            manager.hideSoftInputFromWindow(view.windowToken, 0)
        }
    }

    // 画面が破棄される前の処理
    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        val txtResult = findViewById<TextView>(R.id.txtResult)
        outState.putString("txtResult",txtResult.text.toString() )
    }

    // 画面の復元の処理
    override fun onRestoreInstanceState(savedState: Bundle) {
        super.onRestoreInstanceState(savedState)
        val txtResult = findViewById<TextView>(R.id.txtResult)
        txtResult.text = savedState.getString("txtResult") ?:""
    }

}

[SocketClient.kt]


import android.util.Log

import java.io.InputStream
import java.net.Socket
import java.io.BufferedReader
import java.io.InputStreamReader
import java.io.PrintWriter
import java.io.IOException

import android.os.SystemClock.sleep

// internal 指定で package内からのみ使用可能
internal object SocketClient {
    private const val SERVER_URL = "219.117.194.51";         // うちの会社のIPアドレス。"localhost" とかでも実験は出来ます
    private const val SERVER_PORT = 3456;
    private var socket: Socket? = null  // socket
    private lateinit var inr: BufferedReader
    private lateinit var inp: InputStream
    private lateinit var outw: PrintWriter
    private var cmdStr: String = ""
    private var resData: String = ""

    // socket 接続させているか(プロパティ)
    val isConnect: Boolean
        get() = if (socket == null || !socket!!.isConnected) {
            false
        } else true


    // スレッドを生成して接続
    fun connect() {

        //スレッド生成
        object : Thread() {
            override fun run() {
                Log.d("socket", "接続します1")
                // Log.d("socket","スレッド開始");
                // 接続
                try {
                    Log.d("socket", "接続します")
                    socket = Socket(SERVER_URL, SERVER_PORT)
                    Log.d("socket", "接続しました" + socket?.remoteSocketAddress)
                    inp = socket!!.getInputStream()
                    inr = BufferedReader(InputStreamReader(inp))
                    outw = PrintWriter(socket!!.getOutputStream(), true)
                } catch (e: IOException) {
                    Log.d("socket", "接続失敗(IOException)")
                } catch (e: Exception) {
                    Log.d("socket", "接続失敗(Exception)")
                }

                // 送受信ループ
                try {
                    while (socket != null && socket!!.isConnected) {
                        // データの送信
                        if (cmdStr!="") {
                            outw.write(cmdStr)
                            outw.flush()
                            cmdStr=""
                        }
                        //データの受信
                        if (inp.available() != 0) {
                            //受信データがあれば、BufferedReaderのreadLine()で1行読取。
                            resData = inr.readLine()
                        }
                    }

                    //               } catch (IOException e) {
                    //Log.d("socket","送受信失敗(IOException)");
                } catch (e: Exception) {
                    Log.d("socket", "送受信失敗(Exception)")
                } finally {
                    disconnect()
                }
                // Log.d("socket","スレッド終了");
            }
        }.start()
    }

    // 文字列を送る
    fun sendStr(strCmd: String) {
        try {
            //データの送信
            if (socket != null && socket!!.isConnected) {
                cmdStr = strCmd + "\n"
            } else {
                Log.d("socket", "送信失敗(Exception)")
                disconnect()
            }
        } catch (e: Exception) {
            Log.d("socket", "送信失敗(Exception)")
            disconnect()
        }

    }

    // 文字列初期化
    fun initStr() {
        cmdStr = ""
        resData = ""
    }

    // 文字列をもらう(タイムアウト付き)
    fun getStr(num: Int): String {
        var count = num;
        if (socket != null && socket!!.isConnected) {
            do {
                if (resData != "") {
                    Log.d("socket", resData)
                    return resData
                }
                sleep(10)
                count--;
            } while (count > 0)
        }
        if(num==count) return "-1"			// そもそもsocketに接続できていなかった場合は"-1"を返す
        return ""
    }

    //切断
    fun disconnect() {
        initStr()
        try {
            socket!!.close()
            socket = null
            Log.d("socket", "切断成功")
        } catch (e: Exception) {
            Log.d("socket", "切断失敗(Exception)")
            socket = null
        }
    }
}


次はswiftでiOS端末から

戻る