11 KiB
11 KiB
gRPC 稳定连接的正确解决方案
基于官方 gRPC 文档和最佳实践研究
核心问题
当前代码的设计错误: 尝试通过 callback "恢复" (restore) 已关闭的流
gRPC 官方说法:
"You don't need to re-create the channel - just re-do the streaming RPC on the current channel."
"gRPC stream will be mapped to the underlying http2 stream which is lost when the connection is lost."
结论: 双向流无法恢复,必须重新发起 RPC 调用
为什么当前设计有问题
// 当前错误设计:
1. 订阅事件流 → Flow 开始
2. 网络断开 → Flow 关闭
3. 网络重连 → 尝试"恢复"流 ❌
4. 调用 callback → 期望流恢复 ❌
// 问题:
- Flow 已经关闭,无法恢复
- 需要重新调用 subscribeSessionEvents()
正确的设计模式
模式 1: Application-Level Stream Management (推荐)
class TssRepository {
private val streamManager = StreamManager()
init {
// 监听连接事件,自动重启流
grpcClient.connectionEvents
.filter { it is GrpcConnectionEvent.Reconnected }
.onEach {
android.util.Log.d(TAG, "Reconnected, restarting streams...")
streamManager.restartAllStreams()
}
.launchIn(scope)
}
class StreamManager {
private var eventStreamConfig: EventStreamConfig? = null
private var messageStreamConfig: MessageStreamConfig? = null
fun startEventStream(partyId: String) {
// 保存配置
eventStreamConfig = EventStreamConfig(partyId)
// 启动流
doStartEventStream(partyId)
}
fun restartAllStreams() {
// 重新发起 RPC 调用(不是"恢复")
eventStreamConfig?.let { doStartEventStream(it.partyId) }
messageStreamConfig?.let { doStartMessageStream(it.sessionId, it.partyId) }
}
private fun doStartEventStream(partyId: String) {
grpcClient.subscribeSessionEvents(partyId)
.catch { e ->
Log.e(TAG, "Event stream failed: ${e.message}")
// 如果失败,延迟后重试
delay(5000)
doStartEventStream(partyId)
}
.collect { event ->
// 处理事件
}
}
}
}
模式 2: 使用 Kotlin Flow retry + retryWhen
fun subscribeSessionEventsWithAutoRestart(partyId: String): Flow<SessionEventData> {
return flow {
// 重新发起 RPC 调用
grpcClient.subscribeSessionEvents(partyId).collect {
emit(it)
}
}.retryWhen { cause, attempt ->
android.util.Log.w(TAG, "Event stream failed (attempt $attempt): ${cause.message}")
delay(min(1000L * (attempt + 1), 30000L)) // 指数退避,最多 30 秒
true // 始终重试
}
}
Keep-Alive 配置(防止连接假死)
Android 客户端配置
val channel = AndroidChannelBuilder
.forAddress(host, port)
.usePlaintext() // 或使用 useTransportSecurity()
// Keep-Alive 配置
.keepAliveTime(10, TimeUnit.SECONDS) // 每 10 秒发送 PING
.keepAliveTimeout(3, TimeUnit.SECONDS) // 3 秒内没收到 ACK 视为死连接
.keepAliveWithoutCalls(true) // 即使没有活跃 RPC 也发送 PING
// 重试配置
.enableRetry() // 启用 unary RPC 重试
.maxRetryAttempts(5)
// 其他优化
.idleTimeout(Long.MAX_VALUE, TimeUnit.DAYS) // 不要自动关闭空闲连接
.build()
重要参数说明:
| 参数 | 建议值 | 说明 |
|---|---|---|
keepAliveTime |
10s-30s | PING 发送间隔,太短会浪费流量 |
keepAliveTimeout |
3s | 等待 ACK 超时,判定连接死亡 |
keepAliveWithoutCalls |
true | 没有活跃 RPC 时也 PING(对流很重要) |
idleTimeout |
MAX | 不要自动关闭连接 |
Android 网络状态监听(加速重连)
class GrpcClient {
fun setupNetworkMonitoring(context: Context) {
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
android.util.Log.d(TAG, "Network available, resetting backoff")
// 重要:立即重置重连退避,避免等待 60 秒 DNS 解析
channel?.resetConnectBackoff()
}
override fun onLost(network: Network) {
android.util.Log.w(TAG, "Network lost")
}
}
val request = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.build()
connectivityManager.registerNetworkCallback(request, networkCallback)
}
}
修复方案对比
❌ 当前错误方案
// 尝试"恢复"已关闭的流
fun restoreStreamsAfterReconnect() {
// 问题:Flow 已经关闭,无法恢复
// subscribeSessionEvents 返回的 Flow 已经是死的
}
✅ 正确方案 A: 保存配置 + 重新发起
// 保存流配置
private var activeEventStream: String? = null
fun startEventStream(partyId: String) {
activeEventStream = partyId // 保存配置
launchEventStream(partyId) // 发起流
}
fun onReconnected() {
// 重新发起 RPC 调用
activeEventStream?.let { launchEventStream(it) }
}
private fun launchEventStream(partyId: String) {
scope.launch {
grpcClient.subscribeSessionEvents(partyId).collect { ... }
}
}
✅ 正确方案 B: 自动重试流
fun startEventStreamWithAutoReconnect(partyId: String) {
scope.launch {
flow {
// 每次都重新发起 RPC
grpcClient.subscribeSessionEvents(partyId).collect { emit(it) }
}
.retryWhen { cause, attempt ->
Log.w(TAG, "Stream failed, restarting (attempt $attempt)")
delay(1000L * (attempt + 1))
true // 永远重试
}
.collect { event ->
// 处理事件
}
}
}
为什么在同一路由器下也会断连
即使在同一路由器下,仍可能出现连接问题:
- 手机网络切换: WiFi ↔ 移动数据自动切换
- 省电模式: Android Doze/App Standby 限制网络
- TCP 空闲超时: 路由器/防火墙关闭空闲连接(通常 2-5 分钟)
- HTTP/2 连接老化: 长时间无活动可能被中间设备清理
- 应用后台: 系统限制后台网络访问
Keep-Alive 的作用: 定期发送 PING,告诉路由器/防火墙"我还活着",防止连接被清理
实施计划
第 1 步: 添加 Keep-Alive 配置
修改 GrpcClient.kt 的 doConnect():
private fun doConnect(host: String, port: Int) {
val channelBuilder = ManagedChannelBuilder
.forAddress(host, port)
.usePlaintext()
// ✅ 添加 Keep-Alive
.keepAliveTime(20, TimeUnit.SECONDS)
.keepAliveTimeout(5, TimeUnit.SECONDS)
.keepAliveWithoutCalls(true)
// ✅ 永不超时
.idleTimeout(Long.MAX_VALUE, TimeUnit.DAYS)
channel = channelBuilder.build()
}
第 2 步: 修改流管理模式
选项 A: 最小改动(推荐先试)
修改 TssRepository.kt:
private var shouldMonitorEvents = false
private var eventStreamPartyId: String? = null
fun subscribeToSessionEvents(partyId: String) {
eventStreamPartyId = partyId
shouldMonitorEvents = true
launchEventStream(partyId)
}
private fun launchEventStream(partyId: String) {
scope.launch {
flow {
grpcClient.subscribeSessionEvents(partyId).collect { emit(it) }
}
.retryWhen { cause, attempt ->
if (!shouldMonitorEvents) return@retryWhen false // 停止重试
Log.w(TAG, "Event stream failed, restarting in ${attempt}s: ${cause.message}")
delay(1000L * min(attempt, 30))
true
}
.collect { event ->
handleSessionEvent(event)
}
}
}
fun stopMonitoringEvents() {
shouldMonitorEvents = false
eventStreamPartyId = null
}
选项 B: 完整重构(更健壮)
参考"模式 1"创建 StreamManager 类。
第 3 步: 添加网络监听
修改 MainActivity.kt 或 GrpcClient.kt:
fun setupNetworkCallback(context: Context) {
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val callback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
channel?.resetConnectBackoff()
}
}
val request = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.build()
connectivityManager.registerNetworkCallback(request, callback)
}
测试验证
- ✅ 正常启动 → 订阅事件 → 收到
session_started - ✅ 飞行模式 30 秒 → 关闭飞行模式 → 自动重新订阅 → 收到事件
- ✅ 应用后台 5 分钟 → 恢复前台 → Keep-Alive 保持连接 → 收到事件
- ✅ 长时间空闲(30 分钟)→ 创建会话 → Keep-Alive 仍然工作
参考资料
官方文档
关键 Issues
- How to restart bi-directional stream after network disconnection
- Network connectivity changes on Android
最新文章 (2026)
总结
当前问题根源
- 设计错误: 尝试"恢复"已关闭的流,但 gRPC 流无法恢复
- 缺少 Keep-Alive: 空闲连接被中间设备清理
- 没有自动重启: 流失败后需要手动重新发起
正确解决方案
- ✅ 添加 Keep-Alive 配置(20s PING,5s 超时)
- ✅ 保存流配置,失败后重新发起 RPC(不是"恢复")
- ✅ 使用 Flow.retryWhen 自动重启流
- ✅ 监听网络状态,立即 resetConnectBackoff()
关键理念转变
旧思维: 连接 → 订阅流 → 断开 → 重连 → "恢复"流 ❌
新思维: 连接 → 订阅流 → 断开 → 重连 → "重新发起"流 ✅
Flow 不是持久化对象,是一次性的数据流。断开后必须重新创建。