rwadurian/backend/mpc-system/services/service-party-android/GRPC_CORRECT_SOLUTION.md

11 KiB
Raw Blame History

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 配置(防止连接假死)

基于 gRPC Keepalive 官方文档

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 ->
            // 处理事件
        }
    }
}

为什么在同一路由器下也会断连

即使在同一路由器下,仍可能出现连接问题:

  1. 手机网络切换: WiFi ↔ 移动数据自动切换
  2. 省电模式: Android Doze/App Standby 限制网络
  3. TCP 空闲超时: 路由器/防火墙关闭空闲连接(通常 2-5 分钟)
  4. HTTP/2 连接老化: 长时间无活动可能被中间设备清理
  5. 应用后台: 系统限制后台网络访问

Keep-Alive 的作用: 定期发送 PING告诉路由器/防火墙"我还活着",防止连接被清理

实施计划

第 1 步: 添加 Keep-Alive 配置

修改 GrpcClient.ktdoConnect():

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.ktGrpcClient.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)
}

测试验证

  1. 正常启动 → 订阅事件 → 收到 session_started
  2. 飞行模式 30 秒 → 关闭飞行模式 → 自动重新订阅 → 收到事件
  3. 应用后台 5 分钟 → 恢复前台 → Keep-Alive 保持连接 → 收到事件
  4. 长时间空闲30 分钟)→ 创建会话 → Keep-Alive 仍然工作

参考资料

官方文档

关键 Issues

最新文章 (2026)

总结

当前问题根源

  1. 设计错误: 尝试"恢复"已关闭的流,但 gRPC 流无法恢复
  2. 缺少 Keep-Alive: 空闲连接被中间设备清理
  3. 没有自动重启: 流失败后需要手动重新发起

正确解决方案

  1. 添加 Keep-Alive 配置20s PING5s 超时)
  2. 保存流配置,失败后重新发起 RPC不是"恢复"
  3. 使用 Flow.retryWhen 自动重启流
  4. 监听网络状态,立即 resetConnectBackoff()

关键理念转变

旧思维: 连接 → 订阅流 → 断开 → 重连 → "恢复"流 ❌
新思维: 连接 → 订阅流 → 断开 → 重连 → "重新发起"流 ✅

Flow 不是持久化对象,是一次性的数据流。断开后必须重新创建。