# 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 调用** ## 为什么当前设计有问题 ```kotlin // 当前错误设计: 1. 订阅事件流 → Flow 开始 2. 网络断开 → Flow 关闭 3. 网络重连 → 尝试"恢复"流 ❌ 4. 调用 callback → 期望流恢复 ❌ // 问题: - Flow 已经关闭,无法恢复 - 需要重新调用 subscribeSessionEvents() ``` ## 正确的设计模式 ### 模式 1: Application-Level Stream Management (推荐) ```kotlin 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 ```kotlin fun subscribeSessionEventsWithAutoRestart(partyId: String): Flow { 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 配置(防止连接假死) 基于 [gRPC Keepalive 官方文档](https://grpc.io/docs/guides/keepalive/) ### Android 客户端配置 ```kotlin 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 网络状态监听(加速重连) ```kotlin 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) } } ``` ## 修复方案对比 ### ❌ 当前错误方案 ```kotlin // 尝试"恢复"已关闭的流 fun restoreStreamsAfterReconnect() { // 问题:Flow 已经关闭,无法恢复 // subscribeSessionEvents 返回的 Flow 已经是死的 } ``` ### ✅ 正确方案 A: 保存配置 + 重新发起 ```kotlin // 保存流配置 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: 自动重试流 ```kotlin 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 -> // 处理事件 } } } ``` ## 为什么在同一路由器下也会断连 即使在同一路由器下,仍可能出现连接问题: 1. **手机网络切换**: WiFi ↔ 移动数据自动切换 2. **省电模式**: Android Doze/App Standby 限制网络 3. **TCP 空闲超时**: 路由器/防火墙关闭空闲连接(通常 2-5 分钟) 4. **HTTP/2 连接老化**: 长时间无活动可能被中间设备清理 5. **应用后台**: 系统限制后台网络访问 **Keep-Alive 的作用**: 定期发送 PING,告诉路由器/防火墙"我还活着",防止连接被清理 ## 实施计划 ### 第 1 步: 添加 Keep-Alive 配置 修改 `GrpcClient.kt` 的 `doConnect()`: ```kotlin 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`: ```kotlin 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`: ```kotlin 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) } ``` ## 测试验证 1. ✅ 正常启动 → 订阅事件 → 收到 `session_started` 2. ✅ 飞行模式 30 秒 → 关闭飞行模式 → 自动重新订阅 → 收到事件 3. ✅ 应用后台 5 分钟 → 恢复前台 → Keep-Alive 保持连接 → 收到事件 4. ✅ 长时间空闲(30 分钟)→ 创建会话 → Keep-Alive 仍然工作 ## 参考资料 ### 官方文档 - [gRPC Keepalive Guide](https://grpc.io/docs/guides/keepalive/) - [Android gRPC Guide](https://developer.android.com/guide/topics/connectivity/grpc) - [Performance Best Practices](https://learn.microsoft.com/en-us/aspnet/core/grpc/performance) ### 关键 Issues - [How to restart bi-directional stream after network disconnection](https://github.com/grpc/grpc-java/issues/8177) - [Network connectivity changes on Android](https://github.com/grpc/grpc-java/issues/4011) ### 最新文章 (2026) - [How to Implement gRPC Keepalive for Long-Lived Connections](https://oneuptime.com/blog/post/2026-01-08-grpc-keepalive-connections/view) ## 总结 ### 当前问题根源 1. **设计错误**: 尝试"恢复"已关闭的流,但 gRPC 流无法恢复 2. **缺少 Keep-Alive**: 空闲连接被中间设备清理 3. **没有自动重启**: 流失败后需要手动重新发起 ### 正确解决方案 1. ✅ 添加 Keep-Alive 配置(20s PING,5s 超时) 2. ✅ 保存流配置,失败后重新发起 RPC(不是"恢复") 3. ✅ 使用 Flow.retryWhen 自动重启流 4. ✅ 监听网络状态,立即 resetConnectBackoff() ### 关键理念转变 ``` 旧思维: 连接 → 订阅流 → 断开 → 重连 → "恢复"流 ❌ 新思维: 连接 → 订阅流 → 断开 → 重连 → "重新发起"流 ✅ ``` **Flow 不是持久化对象,是一次性的数据流。断开后必须重新创建。**