WebSocket实战:OkHttp WebSocket客户端、使用Flow封装WebSocket消息、心跳机制
聊到现代Android网络层,HTTP请求只是基本功。真正让应用“活”起来的,是实时通信能力。WebSocket就是那个让服务器能主动找你的技术。
我最早接触WebSocket是在做一个即时通讯模块时。当时用轮询,每3秒发一次请求,用户量一上来,服务器直接扛不住。换成WebSocket后,连接数从每秒几千次请求降到了几十个长连接,效果立竿见影。
今天我们就来实战一下:用OkHttp实现WebSocket客户端,再用Kotlin Flow把消息封装成响应式流,最后加上心跳机制保活。
WebSocket vs HTTP:到底差在哪?
说白了,HTTP是“你问我答”的模式。客户端发请求,服务器给响应,完事。如果你想拿新数据,就得再问一次。
WebSocket不一样。它先通过HTTP握手建立连接,然后双方都可以随时发消息。服务器有新数据了,直接推给你。你想想看,这对聊天、股票行情、游戏同步这类场景有多重要。
我习惯把HTTP比作打电话问路,WebSocket比作对讲机。前者每次都要拨号,后者挂上频道就能随时说话。
OkHttp WebSocket客户端:从握手到收发
OkHttp本身就内置了WebSocket支持,不需要额外引入库。我们直接看代码。
class WebSocketClient(
private val url: String,
private val client: OkHttpClient = OkHttpClient.Builder()
.readTimeout(0, TimeUnit.MILLISECONDS) // WebSocket需要长连接,超时设为0
.build()
) {
private var webSocket: WebSocket? = null
fun connect(listener: WebSocketListener) {
val request = Request.Builder()
.url(url)
.build()
webSocket = client.newWebSocket(request, listener)
}
fun send(message: String): Boolean {
return webSocket?.send(message) ?: false
}
fun close(code: Int = 1000, reason: String = "normal close") {
webSocket?.close(code, reason)
}
}
这里有个坑,我一开始就踩过。OkHttp的readTimeout默认是10秒。WebSocket是长连接,10秒没数据就断开了。所以必须设为0,表示不超时。
WebSocketListener是核心回调接口,我们需要实现这几个方法:
val listener = object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
println("连接已建立")
}
override fun onMessage(webSocket: WebSocket, text: String) {
println("收到消息: $text")
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
println("连接失败: ${t.message}")
}
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
webSocket.close(code, reason)
}
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
println("连接已关闭")
}
}
嗯,这里要注意:onClosing和onClosed是两个不同的阶段。前者是对方发起关闭请求,你还有机会做清理;后者是连接已经彻底断了。
使用Flow封装WebSocket消息
回调的方式用起来不够爽。在Kotlin的世界里,Flow才是王道。我们可以把WebSocket的消息流封装成Flow,让调用方用响应式的方式消费数据。
我封装的时候,主要考虑三个点:连接状态、收到的消息、错误信息。把它们都塞进一个密封类里:
sealed class WebSocketEvent {
data class Connected(val response: Response) : WebSocketEvent()
data class Message(val text: String) : WebSocketEvent()
data class Error(val throwable: Throwable) : WebSocketEvent()
object Closed : WebSocketEvent()
}
然后创建一个Flow版本的WebSocket客户端:
class FlowWebSocketClient(private val url: String) {
private val client = OkHttpClient.Builder()
.readTimeout(0, TimeUnit.MILLISECONDS)
.build()
fun connect(): Flow<WebSocketEvent> = callbackFlow {
val request = Request.Builder().url(url).build()
val listener = object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
trySend(WebSocketEvent.Connected(response))
}
override fun onMessage(webSocket: WebSocket, text: String) {
trySend(WebSocketEvent.Message(text))
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
trySend(WebSocketEvent.Error(t))
}
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
trySend(WebSocketEvent.Closed)
channel.close()
}
}
val webSocket = client.newWebSocket(request, listener)
awaitClose {
webSocket.close(1000, "flow closed")
}
}
}
这里用了callbackFlow,它是专门用来把回调式API转成Flow的。注意awaitClose块,当Flow被取消时,这里会执行清理工作。
调用方用起来就清爽多了:
val client = FlowWebSocketClient("wss://example.com/ws")
lifecycleScope.launch {
client.connect().collect { event ->
when (event) {
is WebSocketEvent.Connected -> println("连上了")
is WebSocketEvent.Message -> handleMessage(event.text)
is WebSocketEvent.Error -> handleError(event.throwable)
is WebSocketEvent.Closed -> println("断开了")
}
}
}
你看,用Flow之后,整个数据流变得非常清晰。而且天然支持协程取消,不会泄漏。
心跳机制:让连接活得更久
WebSocket连接不是永久有效的。网络波动、NAT超时、运营商策略,都可能导致连接被静默断开。你这边还觉得连着,实际上服务器早就不认你了。
我遇到过最坑的一次:用户反馈消息发不出去,但App显示连接正常。查了半天,原来是移动4G网络下,NAT表项5分钟就过期了。从那以后,心跳机制就成了我WebSocket代码里的标配。
心跳的原理很简单:客户端定时发一个ping帧,服务器回复pong帧。如果一段时间没收到pong,就认为连接断了,触发重连。
OkHttp的WebSocket支持原生ping/pong:
class HeartbeatWebSocketClient(
private val url: String,
private val heartbeatInterval: Long = 30_000L, // 30秒一次心跳
private val heartbeatTimeout: Long = 10_000L // 10秒没收到pong就算超时
) {
private var webSocket: WebSocket? = null
private var heartbeatJob: Job? = null
private var lastPongTime: Long = 0L
fun connect(scope: CoroutineScope) {
val request = Request.Builder().url(url).build()
val listener = object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
lastPongTime = System.currentTimeMillis()
startHeartbeat(scope, webSocket)
}
override fun onMessage(webSocket: WebSocket, text: String) {
// 处理业务消息
}
override fun onPong(webSocket: WebSocket, payload: ByteString) {
lastPongTime = System.currentTimeMillis()
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
stopHeartbeat()
// 触发重连逻辑
}
}
webSocket = OkHttpClient.Builder()
.readTimeout(0, TimeUnit.MILLISECONDS)
.build()
.newWebSocket(request, listener)
}
private fun startHeartbeat(scope: CoroutineScope, ws: WebSocket) {
heartbeatJob = scope.launch {
while (isActive) {
delay(heartbeatInterval)
val elapsed = System.currentTimeMillis() - lastPongTime
if (elapsed > heartbeatTimeout) {
// 超时了,关闭当前连接
ws.close(1001, "heartbeat timeout")
stopHeartbeat()
// 触发重连
break
}
// 发送ping帧
ws.send(ByteString.encodeUtf8("ping"))
}
}
}
private fun stopHeartbeat() {
heartbeatJob?.cancel()
heartbeatJob = null
}
}
这里有个细节:onPong回调是OkHttp自动处理的,你不需要手动回复pong。我们只需要记录收到pong的时间,用来判断是否超时。
心跳间隔怎么设?我一般建议30秒到60秒。太频繁浪费流量,太慢又可能被运营商掐断。你可以根据实际网络环境调整。
整体架构图
下面这张图展示了WebSocket客户端的完整工作流程:
避坑指南
我总结几个实战中容易踩的坑:
- 不要在主线程创建WebSocket。OkHttp的WebSocket内部会开线程,但回调默认在子线程。如果你要更新UI,记得切到主线程。
- 重连要有退避策略。我曾经直接写死1秒重连,结果网络一波动,客户端疯狂重连,服务器直接被打挂了。用指数退避,第一次等1秒,第二次2秒,第三次4秒,最大不超过30秒。
- Flow的取消要处理好。如果用
callbackFlow,一定要在awaitClose里关闭WebSocket。否则协程取消了,连接还挂着,内存泄漏。 - 心跳不要和业务消息混在一起。我见过有人用业务消息当心跳,结果业务一停,心跳也停了。心跳应该是独立的ping/pong帧,和业务完全解耦。
ws://或wss://开头。生产环境一定要用wss://,否则数据明文传输,安全风险很大。
好了,WebSocket实战这块就聊到这。核心就是三件事:用OkHttp建立连接、用Flow封装消息流、用心跳保活。你想想看,这三件事做好了,一个稳定可靠的实时通信层就有了。
我个人习惯把WebSocket封装成一个独立的模块,和网络请求库分开。这样职责清晰,测试也方便。你在项目里也可以试试这种思路。
公众号:蓝海资料掘金营,微信deep3321