iT邦幫忙

2021 iThome 鐵人賽

DAY 14
0
Mobile Development

解鎖kotlin coroutine的各種姿勢-新手篇系列 第 14

day14 channel實戰使用 with webSocket,後面離題講android接localhost

  • 分享至 

  • xImage
  •  

前言,今天寫一寫就離題了QQ,前面用ktor架websocket,在手機app接起來,複習一下channel的特性,後面離題講了手機怎麼接到localhost

正文

簡單介紹,webSocket是一個客戶端和伺服器之間進行雙向持續對話,server也可以發訊息給client,不像restful api要由client主動發出請求。

其他關於websocket的介紹,自己上網找,網上資源很多,我就直接帶code

首先,網上大多會告訴你,這是免費的,但是他的連線極其不穩,有時還連不上

//剛剛測還是連不上
ws://echo.websocket.org

那android本身其實能用MockWebServer去模擬server,但我偏不,我要用ktor自己架,我不只要自己架,我還會告訴你怎麼從實機連線到電腦的localhost

MockWebServer,好像原本是測試用途,我不喜歡這樣混用,所以用ktor架了

那用ktor要怎麼架websocket呢?
文檔,對的喔我也都是看文檔的哈哈哈哈哈

//intelliJ
fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args)

@Suppress("unused")
fun Application.module() {

    val scope = CoroutineScope(Job())
    install(WebSockets)
    routing {
        webSocket("/chat") {
            send("You are connected!")
            randomResponse(scope, this)
            for(frame in incoming) {
                frame as? Frame.Text ?: continue
                val receivedText = frame.readText()
                send("You said: $receivedText")

            }
        }
    }


}

fun randomResponse(scope: CoroutineScope, socket:DefaultWebSocketServerSession){
    val randomSample = arrayOf(
        "I am hungry",
        "Harry Potter",
        "ciao, mon amigo",
        "To be or not to be",
        "Android developer"
    )
    scope.launch {
        while(isActive){
            delay(1500)
            socket.send(randomSample[(0..4).random()])
            
            yield()
        }
    }
}

除了randomResponse以外,其他都跟文檔一樣,這整串就是,幫我開啟一個websocket,建立一個"/chat"的路徑(url path),當有人連上這個路徑時,先告訴她"You are connected!", 接著透過coroutine建立一個randomResponse方法,一直傳訊息給client,最後再用迴圈針對收到的訊息做系統回復

簡單,好懂

這邊為求方便沒有cancel coroutine scope,好孩子不要學

執行後,建議先用網頁別人寫好的websocket測試,隨便找個測試網站,給他ws://localhost:8080/chat`試試,如果收發都沒問題就能進下一步

android開發

fragment我用一個textview來接

//Android studio
class SocketFragment : Fragment() {

    private lateinit var binding:FragmentSocketBinding
    private val mAdapter = ChatAdapter()
    private val viewModel by viewModels<SocketViewModel>()

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_socket, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        binding = FragmentSocketBinding.bind(view)

       lifecycleScope.launch {
           viewModel.messageChannel.consumeEach {
               Timber.d("in fragment $it")
               binding.allMsg.text = StringBuilder(binding.allMsg.text).append(it)
               binding.allMsg.invalidate()
           }
       }

        binding.sentMsg.setOnClickListener {
            viewModel.sentMessage( binding.ed.text.toString() )
            binding.ed.setText("")
        }

    }
}

socket在viewModel實例,比較好控制生命週期

class SocketViewModel: ViewModel() {

    private var mWebSocket: WebSocket? = null
    private val mWbSocketUrl = "ws://127.0.0.1:8080/chat"
    val messageChannel = Channel<String>()

    init {
        initSocket()
    }

    fun initSocket() {
        val mClient = OkHttpClient.Builder()
            .pingInterval(10, TimeUnit.SECONDS)
            .build()
        val request: Request = Request.Builder()
            .url(mWbSocketUrl)
            .build()

        mWebSocket = mClient.newWebSocket(request, object : WebSocketListener(){
            override fun onMessage( webSocket: WebSocket,  text: String) {
                super.onMessage(webSocket, text)
                viewModelScope.launch {
                    messageChannel.send(text + "\n")
                    Timber.d("receive message $text")
                }
            }
            override fun onOpen(webSocket: WebSocket, response: Response) {
                super.onOpen(webSocket, response)
                Timber.d("success connect")
            }
            override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
                super.onClosing(webSocket, code, reason)
                mWebSocket?.close(code, reason)
                mWebSocket = null
            }
            override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
                super.onFailure(webSocket, t, response)
                Timber.e("fail connect")
                Timber.e(response?.message)
            }
        })
    }

    fun sentMessage(s:String){
        mWebSocket?.send(s)
    }

    override fun onCleared() {
        super.onCleared()
        messageChannel.cancel()
        mWebSocket?.cancel()
        mWebSocket = null
    }
}

可以看到,我用channel在viewModel和fragment之間傳訊息,記得channel的特性嗎?

  1. 可以在不同的Coroutine之間傳送訊息
  2. 保證傳輸和接收的順序

不知道的,可以先看這篇

先給效果,大概長這樣

離題一下,講個麻煩的,android開發接localhost

方法一,模擬器

private val mWbSocketUrl = "ws://10.0.2.2:8080/chat"

方法二,實機有線

在chrome的網址列輸入chrome://inspect/#devices

  1. 確定手機usb偵錯有打開

  2. 確定Remote Target有設備,可打開手機瀏覽器確定網頁也有被抓到

    大概長這樣,我是把chrome調成暗色,所以你們畫面可能會是白底的

  3. 設置port, ip address
    其實我也有些不懂,目前測試
    直播流接Discover USB devices就好
    websocket要兩個都接

接法是

Discover USB devices

Discover network targets

兩個都是接8080的就可以了,其他的是我之前接直播流用的port

private val mWbSocketUrl = "ws://127.0.0.1:8080/chat"

方法三,實機無線

private val mWbSocketUrl = "ws://電腦ip:8080/chat"

大家應該都知道網路有七層吧
network layer

不知道的,也能做,在這裡講網路分層就離題太遠了

anyway, 我有時開發會忘記帶usb線,這時我就會用無線偵錯(跳過不講無線偵錯部分),那方法二是透過chrome的開發功能連接,無線時要怎麼辦呢?

從網域連線~~~~

首先把電腦和手機連到同一個wifi,請確定wifi是可信任的,因為等等要開防火牆的port
在cmd下ipconfig拿到電腦的ip位址

window控制台

進階設置>輸入規則>新增規則

選擇

post

連線設置

設置,如果建議取消公用,然後將wifi加入至家用或工作

建立好就會這樣

電腦的ip每次都會更改,可以參考這篇設置ip,方法三我也是參考這篇的


上一篇
day13 Kotlin coroutine channel操作
下一篇
day15 job的騷操作
系列文
解鎖kotlin coroutine的各種姿勢-新手篇30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言