WebSocket 和长轮询

2020-07-21
2 min read

客户端的网络请求大部分都建立在请求/响应模式的 HTTP/HTTPS 协议之下,AJAX 技术使页面看起来更加动态。尽管如此,所有的 HTTP 连接仍由客户端控制,需要用户交互或定时轮询(periodic polling)才能从服务端加载新数据。

通过长轮询,客户端打开与服务端的 HTTP 连接,并保持连接直到服务端返回响应,只要服务端有新的数据,它就会发送响应。然而这些技术有共同的问题:它们带有 HTTP 的开销,并不适合低延时应用的需求。

WebSocket 简介

WebSocket 规范定义了一个 API:在客户端和服务端之间建立 socket 连接。简而言之,客户端和服务端之间建立持久连接,双方都可以随时发送数据。

使用 OkHttp 实现 WebSocket

添加依赖

// network
implementation("com.squareup.okhttp3:okhttp:4.8.0")
implementation("com.squareup.okhttp3:mockwebserver:4.8.0")

客户端创建 WebSocket 连接

/**
 * 连接 WebSocket 服务
 *
 * @param hostname 服务端域名地址
 * @param port WebSocket 服务端口号
 */
private fun connectWebSocket(hostname: String,port: Int) {
  val httpClient = OkHttpClient.Builder()
      .pingInterval(PING_INTERVAL, TimeUnit.SECONDS)
      .build()

  // Android 9 已禁止明文传输网络数据,此处使用 ws,可以参考:https://stackoverflow.com/a/50834600
  val url = "ws://${hostname}:${port}"
  Logger.i(TAG, "connectWebSocket", "connect url -> $url")

  val request = Request.Builder()
      .url(url)
      .build()

  httpClient.newWebSocket(request, object : WebSocketListener() {
    override fun onOpen(
      webSocket: WebSocket,
      response: Response
    ) {
      super.onOpen(webSocket, response)
      Logger.i(TAG, "onOpen", "response -> ${response.message}")
      // While web socket is opened create our interval work
      createIntervalWork(webSocket)
    }

    override fun onMessage(
      webSocket: WebSocket,
      text: String
    ) {
      super.onMessage(webSocket, text)
      Logger.i(TAG, "onMessage", "text -> $text")
      if (text.isNotEmpty()) {
        mHandler.post {
          mAdapter.addMessage("pong -> $text")
        }
      }
    }

    override fun onMessage(
      webSocket: WebSocket,
      bytes: ByteString
    ) {
      super.onMessage(webSocket, bytes)
      Logger.i(TAG, "onMessage", "bytes -> $bytes")
      mHandler.post {
        mAdapter.addMessage(bytes.toString())
      }
    }

    override fun onClosed(
      webSocket: WebSocket,
      code: Int,
      reason: String
    ) {
      super.onClosed(webSocket, code, reason)
      Logger.i(TAG, "onClosed", "reason -> $reason")
    }

    override fun onClosing(
      webSocket: WebSocket,
      code: Int,
      reason: String
    ) {
      super.onClosing(webSocket, code, reason)
      Logger.i(TAG, "onClosing", "reason -> $reason")
    }

    override fun onFailure(
      webSocket: WebSocket,
      t: Throwable,
      response: Response?
    ) {
      super.onFailure(webSocket, t, response)
      Logger.i(TAG, "onFailure", "${t.message}")
    }
  })
}

客户端 Mock WebSocket 服务

/**
 * Mock websocket server
 */
fun mockWebServer(): MockWebServer? {
  val mockWebServer: MockWebServer? = MockWebServer()

  mockWebServer?.enqueue(MockResponse().withWebSocketUpgrade(object : WebSocketListener() {
    override fun onOpen(
      webSocket: WebSocket,
      response: Response
    ) {
      super.onOpen(webSocket, response)
      Logger.i("MockWebServer", "onOpen", "response -> ${response.message}")
    }

    override fun onMessage(
      webSocket: WebSocket,
      text: String
    ) {
      super.onMessage(webSocket, text)
      Logger.i("MockWebServer", "onMessage", "text -> $text")
      // send back message to client
      webSocket.send(text)
    }

    override fun onMessage(
      webSocket: WebSocket,
      bytes: ByteString
    ) {
      super.onMessage(webSocket, bytes)
      Logger.i("MockWebServer", "onMessage", "bytes -> $bytes")
      // send back message to client
      webSocket.send(bytes)
    }

    override fun onClosed(
      webSocket: WebSocket,
      code: Int,
      reason: String
    ) {
      super.onClosed(webSocket, code, reason)
      Logger.i("MockWebServer", "onClosed", "reason -> $reason")
    }

    override fun onClosing(
      webSocket: WebSocket,
      code: Int,
      reason: String
    ) {
      super.onClosing(webSocket, code, reason)
      Logger.i("MockWebServer", "onClosing", "reason -> $reason")
    }

    override fun onFailure(
      webSocket: WebSocket,
      t: Throwable,
      response: Response?
    ) {
      super.onFailure(webSocket, t, response)
      Logger.i("MockWebServer", "onFailure", "exception -> ${t.message}")
    }
  }))
  return mockWebServer
}