WebSocket 长连接
问题
Android 中如何使用 WebSocket 实现实时通信?
答案
WebSocket vs HTTP
| 特性 | HTTP | WebSocket |
|---|---|---|
| 通信方式 | 请求-响应,单向 | 全双工,双向 |
| 连接 | 短连接(或 Keep-Alive) | 持久连接 |
| 开销 | 每次请求携带 Header | 握手后帧级通信,开销小 |
| 适用场景 | API 调用、资源请求 | 即时通讯、实时推送、游戏 |
OkHttp WebSocket
OkHttp 内置 WebSocket 支持:
class WebSocketManager(
private val client: OkHttpClient
) {
private var webSocket: WebSocket? = null
private var isConnected = false
fun connect(url: String) {
val request = Request.Builder()
.url(url)
.build()
webSocket = client.newWebSocket(request, object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
isConnected = true
Log.d("WS", "连接成功")
}
override fun onMessage(webSocket: WebSocket, text: String) {
Log.d("WS", "收到文本消息: $text")
// 解析 JSON 消息
val message = Json.decodeFromString<Message>(text)
handleMessage(message)
}
override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
Log.d("WS", "收到二进制消息: ${bytes.size} bytes")
}
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
Log.d("WS", "连接关闭中: $code $reason")
webSocket.close(1000, null)
}
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
isConnected = false
Log.d("WS", "连接已关闭: $code $reason")
}
override fun onFailure(
webSocket: WebSocket, t: Throwable, response: Response?
) {
isConnected = false
Log.e("WS", "连接失败", t)
// 触发重连
scheduleReconnect()
}
})
}
fun sendMessage(text: String) {
webSocket?.send(text)
}
fun close() {
webSocket?.close(1000, "正常关闭")
}
}
心跳保活机制
WebSocket 长连接需要心跳机制防止被网络中间设备断开:
class HeartbeatWebSocket(
private val client: OkHttpClient
) {
private var webSocket: WebSocket? = null
private val handler = Handler(Looper.getMainLooper())
// 心跳间隔(30秒)
private val heartbeatInterval = 30_000L
// 心跳超时(10秒内未收到 Pong)
private val heartbeatTimeout = 10_000L
private var lastPongTime = 0L
private val heartbeatRunnable = object : Runnable {
override fun run() {
// 检查上次 Pong 是否超时
if (lastPongTime > 0 &&
System.currentTimeMillis() - lastPongTime > heartbeatInterval + heartbeatTimeout
) {
// 心跳超时,认为连接断开
reconnect()
return
}
// 发送 Ping
webSocket?.send("""{"type":"ping","ts":${System.currentTimeMillis()}}""")
// 调度下一次心跳
handler.postDelayed(this, heartbeatInterval)
}
}
fun connect(url: String) {
val request = Request.Builder().url(url).build()
webSocket = client.newWebSocket(request, object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
lastPongTime = System.currentTimeMillis()
// 启动心跳
startHeartbeat()
}
override fun onMessage(webSocket: WebSocket, text: String) {
val json = JSONObject(text)
if (json.optString("type") == "pong") {
lastPongTime = System.currentTimeMillis()
} else {
handleBusinessMessage(text)
}
}
override fun onFailure(
webSocket: WebSocket, t: Throwable, response: Response?
) {
stopHeartbeat()
reconnect()
}
})
}
private fun startHeartbeat() {
handler.postDelayed(heartbeatRunnable, heartbeatInterval)
}
private fun stopHeartbeat() {
handler.removeCallbacks(heartbeatRunnable)
}
}
断线重连策略
class ReconnectWebSocket {
private var reconnectAttempt = 0
private val maxReconnectAttempt = 10
private val baseDelay = 1000L // 1 秒
private val maxDelay = 30_000L // 30 秒
fun reconnect() {
if (reconnectAttempt >= maxReconnectAttempt) {
Log.e("WS", "达到最大重连次数")
return
}
// 指数退避 + 随机抖动
val delay = min(
baseDelay * 2.0.pow(reconnectAttempt).toLong(),
maxDelay
)
val jitter = (delay * 0.2 * Math.random()).toLong()
reconnectAttempt++
Log.d("WS", "第 $reconnectAttempt 次重连,延迟 ${delay + jitter}ms")
handler.postDelayed({
connect(url)
}, delay + jitter)
}
fun onConnected() {
// 连接成功,重置计数器
reconnectAttempt = 0
}
}
结合 Lifecycle 管理
class WebSocketLifecycleObserver(
private val wsManager: WebSocketManager,
private val url: String
) : DefaultLifecycleObserver {
override fun onStart(owner: LifecycleOwner) {
wsManager.connect(url)
}
override fun onStop(owner: LifecycleOwner) {
wsManager.close()
}
}
// Activity / Fragment 中使用
lifecycle.addObserver(WebSocketLifecycleObserver(wsManager, WS_URL))
前后台切换的处理
Android 应用进入后台后,系统可能杀掉进程或限制网络。WebSocket 应在 onStop 断开,onStart 重连。对于需要后台接收消息的场景(如 IM),应使用 FCM 推送 + 前台 Service。
常见面试问题
Q1: WebSocket 和 HTTP 长轮询的区别?
答案:
| 特性 | WebSocket | HTTP 长轮询 |
|---|---|---|
| 连接 | 升级为 WebSocket 协议后持久连接 | 反复建立 HTTP 连接 |
| 方向 | 全双工(服务端主动推送) | 本质仍是客户端发起请求 |
| 延迟 | 低延迟(毫秒级) | 较高(取决于轮询间隔) |
| 开销 | 握手一次,后续帧级开销小 | 每次请求完整 HTTP Header |
| 实现 | 需要 WebSocket 服务端 | 普通 HTTP 服务端即可 |
WebSocket 适合高频实时场景(IM、游戏),长轮询适合偶尔更新的场景(通知轮询)。
Q2: WebSocket 需要心跳的原因?
答案:
- NAT 超时:运营商 NAT 设备通常 1-5 分钟不活跃即释放映射
- 防火墙/代理:中间设备可能断开空闲连接
- 检测连接状态:TCP 的 KeepAlive 间隔太长(通常 2 小时),无法及时发现断连
- 保持 App 存活:后台心跳可以防止进程被系统回收(需要前台 Service 配合)
Q3: 如何选择 WebSocket 库?
答案:
| 库 | 优势 | 适用场景 |
|---|---|---|
| OkHttp WebSocket | 无额外依赖、与 HTTP 共享连接池 | 简单实时通信 |
| Scarlet | 自动重连、Lifecycle 集成、类 Retrofit API | 复杂 WebSocket 场景 |
| Socket.IO | 自动降级(WebSocket→Polling)、房间机制 | 与 Socket.IO 服务端配合 |
| gRPC Streaming | 类型安全、双向流 | 微服务间通信 |
对于大多数 Android 应用,OkHttp 自带的 WebSocket 已经足够。