のためのアンドロイドWebSocketクライアント


WebSocketsは、モバイルアプリケーションとバックエンドの双方向通信を実現するための素晴らしい方法です.アマゾンのAPIゲートウェイサービスlaunched serverless support for WebSockets .
それをすべて設定すると多少関与している.で始まることをお勧めしますSimple WebSockets Chat backend 彼らはデモとして提供しています.私たちはどのようにAndroidから話をするにはOkHttp またはJetbrains ' newerKtor .

事前の要件
  • activate an AWS account
  • The latest version of the AWS CLI

  • wscat インストールnpm .

  • AWS SAM CLI Homebrew(Mac上での場合)を介してインストール
  • Android Studio

  • バックエンドの設定
    Serverlessデモアプリケーションのインストールはかなりまっすぐです.
    チェックアウトチャットアプリケーション:
    git clone https://github.com/aws-samples/simple-websockets-chat-app.git
    
    そして、それをあなたのアカウントに展開します.
    sam deploy --guided
    
    しばらくしたら、そのコマンドは終了します.CloudFormationスタックの出力を検査することで、プロビジョニングされたリソースを見ることができます.
    aws cloudformation describe-stacks \
        --stack-name simple-websocket-chat-app \
        --query 'Stacks[].Outputs'
    
    これは、DynamoDBテーブル、3つのラムダ関数、AWS IAMロール、およびWebSocketsサポート付きのAPIゲートウェイを作成します.
    すべてを作成したものを理解するために出力を見た後、WebSocketエンドポイント自体でゼロにしよう.クライアントが利用できるURIを見つけ、それにアクセスする必要があります.
    aws cloudformation describe-stacks \
        --stack-name simple-websocket-chat-app \
        --query 'Stacks[].Outputs[]' | \
    jq -r '.[]|select(.OutputKey=="WebSocketURI").OutputValue'
    
    次のように出力します.
    wss://azgu36n0vf.execute-api.us-east-1.amazonaws.com/Prod
    

    コマンドラインテストwscatすぐにバックエンドをテストしましょう.私たちは wscat command-line utility . 以下のコマンドは、APIゲートウェイへの長寿命接続をオープンし、サブシェルでメッセージを送受信できるようになります.
    $ wscat -c wss://azgu36n0vf.execute-api.us-east-1.amazonaws.com/Prod
    Connected (press CTRL+C to quit)
    > {"action": "sendmessage", "data": "foo"}
    < foo
    
    上記のJSON形式は、我々が展開したアプリで必要です.あなたの値を変更することができます"foo" , しかし、あなたは何も変えることができません.
    他の何かをパスしようとすると動作しません.
    > Hello, how are you?
    < {"message": "Forbidden", "connectionId":"Y0bEuc0UIAMCIiA=", "requestId":"Y0bwuGjXIAMFmEg="}
    
    しかし、複数の端末ウィンドウを開き、それらをエンドポイントに接続すれば、有効なメッセージを受け取るでしょう.

    からのAPIを呼び出す
    WebSocket APIが機能していることを知ったので、代わりにクライアントとして使用するAndroidアプリの構築を始めましょう.
    websocketsはsupported in OkHttp since 3.5 , それは2016年にずっと戻ってきた.
    WebSocketクライアントの初期化は直進です.
    val request = Request.Builder()
        .url("wss://azgu36n0vf.execute-api.us-east-1.amazonaws.com/Prod")
        .build()
    val listener = object: WebSocketListener() {
        override fun onMessage(ws: WebSocket, mess: String) {
            // Called asynchronously when messages arrive
        }
    }
    val ws = OkHttpClient()
        .newWebSocket(request, listener)
    
    メッセージをAPIゲートウェイに送るには、以下のようにします.
    ws.send(JSONObject()
        .put("action", "sendmessage")
        .put("data", "Hello from Android!")
        .toString())
    
    私達は私達のUIにボタンを追加することができますViewBinding , をクリックすると、WebSocketメッセージを起動します.
    ui.button.setOnClickListener { 
        ws.send(JSONObject()
            .put("action", "Hello from Android!")
            .put("data", command)
            .toString())
    }
    
    スレッドのすべてがOKHTTPの中で扱われるので、本当にそれにそれほど多くはありません.あなたのビューバインディングにハンドルを保存し、あなたのWebSocketクライアントにActivity :
    class MainActivity : AppCompatActivity() {
        private lateinit var ui: ActivityMainBinding
        private lateinit var ws: WebSocket
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            ui = ActivityMainBinding.inflate(layoutInflater)
            setContentView(ui.root)
            connect() // As above
        }
    

    からのAPIを呼び出す
    KTORは、あなたがCoroutinesを使用していて、あなた自身の範囲/文脈を管理しているという仮定で造られます.これは、より柔軟なツールになりますが、いくつかの追加の複雑さを追加します.
    ツールの基本的な設定は以下のようになります.
    private suspend fun connect(ktor: HttpClient, u: Url) {
        ktor.wss(Get, u.host, u.port, u.encodedPath) {
            // Access to a WebSocket session
        }
    }
    
    private /* not suspend */ fun connect() {
        val url = Url("wss://azgu36n0vf.execute-api.us-east-1.amazonaws.com/Prod")
        val ktor = HttpClient(OkHttp) {
            install(WebSockets)
        }
        lifecycleScope.launch(Dispatchers.IO) {
            connect(ktor, url)
        }
    }
    
    末尾の閉鎖の内部では、 DefaultClientWebSocketSession . それには2つの重要なメンバーがあります.
  • エー ReceiveChannel 名前ingoing , and
  • エー SendChannel 名前outgoing .
  • SendChannel and ReceiveChannel コリンの入口と出口 Channel , これは基本的にはサスペンションのキューのようです.
    WebSocketセッションの閉鎖の中にいくつかの簡単なメッセージを送受信するのはかなり些細なことです.
    ktor.wss(Get, u.host, u.port, u.encodedPath) {
        // Send a message outbound
        val json = JSONObject()
            .put("action", "sendmessage")
            .put("data", "Hello from Android!")
            .toString()
        outgoing.send(Frame.Text(json))
    
        // Receive an inbound message
        val frame = incoming.receive()
        if (frame is Frame.Text) {
            ui.status.append(frame.readText())
        }
    }
    
    しかし、我々は我々が我々のOKHTTP解決で持っていた機能の束を逃しています.つまり
  • 私たちは、同時にデータを送受信したいと思います
  • 接続が開いて閉じたときに通知を取得します.

  • KTORにおける並列送受信
    我々の目標は、最終的にメッセージの流れを派遣することですoutgoing チャンネルからのメッセージの流れを消費するingoing チャンネル、同時に.我々が使用する基本的なアプローチは、2ビットの仕事を非同期的に起動し、それらを終了するのを待つことです.
    ktor.wss(Get, u.host, u.port, u.encodedPath) {
        awaitAll(async {
            // Code that will send messages
        }, async {
            // Code that will receive messages
        })
    }
    
    送信メッセージをトリガーするイベントが必要です.UIにボタン要素を追加すると意味があります.しかし、我々はコマンドの流れとしてボタンクリックをモデル化したいと思います.
    最初にボタンクリックをマップ化する拡張機能を作成しましょうFlow<Unit> 信用StackOverflow ):
    private fun View.clicks(): Flow<Unit> = callbackFlow {
        setOnClickListener { offer(Unit) }
        awaitClose { setOnClickListener(null) }
    }
    
    今、我々はボタンからイベントの流れを聞くことができます、我々が必要とする形式にそれらを写像して、彼らを送ってください:
    ui.button.clicks()
        .map { click -> "Hello from Android!" }
        .map { message -> JSONObject()
            .put("action", "sendmessage")
            .put("data", message)
            .toString()
        }
        .map { json -> Frame.Text(json) }
        .collect { outgoing.send(it) }
    
    それは出てくる出来事によく役立つだろう.今、我々はちょうど2番目のインバウンドイベントに対応する必要がありますasync ブロック.
    この場合、結果をマップする値はあまりありません.ソケット上で受信する値は、"data" メッセージのキー.例えば、我々は得るかもしれない"Hello from Android!" :
    incoming.consumeEach { frame ->
        if (frame is Frame.Text) {
            ui.status.append(frame.readText())
        }
    }
    
    それがすべて言われて、されるとき、我々はこれのような何かで終わります:
    ktor.wss(Get, u.host, u.port, u.encodedPath) {
        awaitAll(async {
            ui.button.clicks()
                .map { click -> JSONObject()
                    .put("action", "sendmessage")
                    .put("data", "Hello from Android!")
                    .toString()
                }
                .map { json -> Frame.Text(json) }
                .collect { outgoing.send(it) }
        }, async {
            incoming.consumeEach { frame ->
                if (frame is Frame.Text) {
                    ui.status.append(frame.readText())
                }
            }
        }
    })
    

    ライフサイクルイベント
    OkHTTPは、コールバックをWebSocketListener , さまざまなライフサイクルイベントを通知する
    val listener = WebSocketListener() {
        override fun onOpen(ws: WebSocket, res: Response) {
            // when WebSocket is first opened
        }
        override fun onClosed(ws: WebSocket, code: Int, reason: String) {
            // when WebSocket is closed
        }
    }
    
    Ktorはそのように動作しません.彼らはdifferent approaches to recover those events .
    回復する最も簡単なものはソケットが開くときのイベントです.それは、中で起こる最初のものだけですwss セッションの閉鎖:
    ktor.wss(Get, u.host, u.port, u.encodedPath) {
        ui.status.append("Connected to $u!")
    }
    
    WebSocketの終了についての洞察を得るには、受信ブロックで処理を展開できます.
    incoming.consumeEach { frame ->
        when (frame) {
            is Frame.Text -> { /* as above */ }
            is Frame.Close -> {
                val reason = closeReason.await()!!.message
                ui.status.append("Closing: $reason")
            }
        }
    }
    
    失敗を捕らえるのもかなり簡単です.
    private fun connect() {
        val url = Url("wss://azgu36n0vf.execute-api.us-east-1.amazonaws.com/Prod")
        val client = HttpClient(OkHttp) {
            install(WebSockets)
        }
        lifecycleScope.launch(Dispatchers.IO) {
            try {
                connect(client, url)
            } catch (e: Throwable) {
                val message = "WebSocket failed: ${e.message}"
                ui.status.append(message)
            }
        }
    }
    

    ラッピング
    さて、OkHTTPとKTORを使用してAmazon APIゲートウェイWebSockets APIを消費する方法についての、荒々しく説明できない説明があります.🥳.
    The code for this project is available on my GitHub page .