# 步骤 04:连接与首次同步 > 实现完整的连接流程:Identity 管理、Connect、WaitConnected、首次同步。 --- ## 一、目标 - [ ] Identity 生成与持久化 - [ ] ClientOption 组装 - [ ] 事件处理器注册 - [ ] Connect + WaitConnected 流程 - [ ] 首次同步(ListChannels + ListClients + ClientID) - [ ] 连接状态与错误处理 --- ## 二、任务清单 ### 4.1 Identity 管理 **目标**:实现 TeamSpeak 加密身份的生成与持久化存储。 **任务**: 1. **生成 Identity** ```kotlin // Go 侧通过 Bridge 暴露生成接口 // Kotlin 侧调用生成并获取 Identity 字符串 val identity = TSBridge.generateIdentity() ``` 2. **持久化存储** - 使用 `SharedPreferences` 或 `DataStore` 存储 Identity - Key 建议:`ts_identity` - 首次启动时生成,后续启动时读取 3. **读取与恢复** ```kotlin fun loadOrCreateIdentity(context: Context): String { val prefs = context.getSharedPreferences("ts_config", Context.MODE_PRIVATE) return prefs.getString("ts_identity", null) ?: TSBridge.generateIdentity().also { prefs.edit().putString("ts_identity", it).apply() } } ``` **注意事项**: - Identity 是客户端加密身份,必须持久化,否则每次连接会被服务器视为新用户 - 生成后不可更改,丢失需重新生成(会丢失服务器端的权限关联) ### 4.2 连接流程实现 **目标**:实现完整的连接流程,从配置组装到连接成功。 **任务**: 1. **连接参数数据类** ```kotlin data class ConnectionConfig( val address: String, // 服务器地址(IP/域名/TSDNS) val nickname: String, // 显示昵称 val password: String? = null, // 服务器密码(可选) val defaultChannel: String? = null, // 默认频道(可选) val defaultChannelPassword: String? = null // 默认频道密码(可选) ) ``` 2. **Bridge 层连接接口封装** ```kotlin // TSBridge.kt 中新增 fun connect(config: ConnectionConfig): Result { return try { val identity = loadOrCreateIdentity(context) // 调用 Go 侧 NewClient + Connect tsClient.newClient(identity, config.address, config.nickname) if (config.password != null) { tsClient.setServerPassword(config.password) } if (config.defaultChannel != null) { tsClient.setDefaultChannel(config.defaultChannel) if (config.defaultChannelPassword != null) { tsClient.setDefaultChannelPassword(config.defaultChannelPassword) } } tsClient.connect() Result.success(Unit) } catch (e: Exception) { Result.failure(e) } } ``` 3. **WaitConnected 实现** ```kotlin suspend fun waitConnected(timeout: Duration = 30.seconds): Result { return withContext(Dispatchers.IO) { try { // Go 侧阻塞等待,支持 context 取消 tsClient.waitConnected(timeout.inWholeMilliseconds) Result.success(Unit) } catch (e: Exception) { Result.failure(e) } } } ``` 4. **完整连接流程** ```kotlin // ServerViewModel.kt suspend fun connect(config: ConnectionConfig) { _connectionState.value = ConnectionState.Connecting // 1. 注册事件处理器(Connect 前必须完成) registerEventHandlers() // 2. 发起连接 val connectResult = TSBridge.connect(config) if (connectResult.isFailure) { _connectionState.value = ConnectionState.Failed(connectResult.exceptionOrNull()!!) return } // 3. 等待连接就绪 val waitResult = TSBridge.waitConnected() if (waitResult.isFailure) { _connectionState.value = ConnectionState.Failed(waitResult.exceptionOrNull()!!) return } // 4. 连接成功,等待 OnConnected 事件触发首次同步 } ``` **连接状态枚举**: ```kotlin sealed class ConnectionState { object Disconnected : ConnectionState() object Connecting : ConnectionState() object Connected : ConnectionState() // WaitConnected 成功 object Syncing : ConnectionState() // 首次同步中 object Ready : ConnectionState() // 业务就绪 data class Failed(val error: Throwable) : ConnectionState() } ``` **时序约束**: - `Connect()` 成功只表示连接流程已启动 - 发送业务命令前必须等待 `WaitConnected()` 成功 - 事件处理器必须在 `Connect()` 前注册,避免早期事件丢失 ### 4.3 事件注册 **目标**:在 Connect 前注册所有必要的事件处理器,确保不遗漏早期服务端推送。 **任务**: 1. **事件处理器注册(Go Bridge 侧)** ```go // bridge.go 中暴露注册接口 func (b *Bridge) RegisterEventHandlers() { b.client.OnConnected(func() { b.notifyEvent("connected", nil) }) b.client.OnDisconnected(func(err error) { b.notifyEvent("disconnected", map[string]interface{}{ "error": err.Error(), }) }) b.client.OnClientEnter(func(info ClientInfo) { data, _ := json.Marshal(info) b.notifyEvent("client_enter", string(data)) }) b.client.OnClientLeave(func(event ClientLeftViewEvent) { data, _ := json.Marshal(event) b.notifyEvent("client_leave", string(data)) }) b.client.OnClientMoved(func(event ClientMovedEvent) { data, _ := json.Marshal(event) b.notifyEvent("client_moved", string(data)) }) b.client.OnTextMessage(func(msg TextMessage) { data, _ := json.Marshal(msg) b.notifyEvent("text_message", string(data)) }) b.client.OnPoked(func(event PokeEvent) { data, _ := json.Marshal(event) b.notifyEvent("poked", string(data)) }) b.client.OnKicked(func(reason string) { b.notifyEvent("kicked", reason) }) } ``` 2. **Kotlin 侧事件监听** ```kotlin // TSBridge.kt fun setEventListener(listener: (String, String) -> Unit) { // 接收 Go 侧通过 JNI 回调的事件 eventCallback = listener } // ServerViewModel.kt fun registerEventHandlers() { TSBridge.setEventListener { event, data -> when (event) { "connected" -> handleConnected() "disconnected" -> handleDisconnected(data) "client_enter" -> handleClientEnter(data) "client_leave" -> handleClientLeave(data) "client_moved" -> handleClientMoved(data) "text_message" -> handleTextMessage(data) "poked" -> handlePoked(data) "kicked" -> handleKicked(data) } } } ``` 3. **事件处理器职责** | 事件 | 处理器 | 职责 | |------|--------|------| | `connected` | `handleConnected()` | 触发首次同步流程 | | `disconnected` | `handleDisconnected()` | 清理会话状态,更新 UI | | `client_enter` | `handleClientEnter()` | 增量同步:添加成员到基线 | | `client_leave` | `handleClientLeave()` | 增量同步:从基线移除成员 | | `client_moved` | `handleClientMoved()` | 增量同步:更新成员频道位置 | | `text_message` | `handleTextMessage()` | 消息归档:按 TargetMode 存储 | | `poked` | `handlePoked()` | 显示 Poke 通知 | | `kicked` | `handleKicked()` | 处理踢出,清理状态 | **注意事项**: - 事件处理器必须在 `Connect()` 之前注册 - 事件回调在 Go 的事件循环 goroutine 中串行执行,不要做耗时操作 - 需要通过事件队列串行化 JNI 回调,避免并发问题 ### 4.4 首次同步逻辑 **目标**:连接成功后,建立完整的频道基线和成员基线。 **触发时机**:收到 `OnConnected` 事件后立即执行。 **任务**: 1. **并行请求三个数据源** ```kotlin // ServerViewModel.kt private suspend fun performInitialSync() { _connectionState.value = ConnectionState.Syncing try { // 并行请求频道列表、成员列表、自身 ID val channelsDeferred = async { TSBridge.listChannels() } val clientsDeferred = async { TSBridge.listClients() } val selfIdDeferred = async { TSBridge.getClientId() } val channels = channelsDeferred.await() val clients = clientsDeferred.await() val selfId = selfIdDeferred.await() // 原子提交到状态仓库 repository.updateBaseline( channels = channels, clients = clients, selfClientId = selfId ) _connectionState.value = ConnectionState.Ready } catch (e: Exception) { _connectionState.value = ConnectionState.SyncFailed(e) // 允许重试,不进入业务就绪 } } ``` 2. **数据模型定义** ```kotlin // data/Models.kt data class ChannelInfo( val id: Long, val parentId: Long, val name: String, val order: Long = 0, val isPassword: Boolean = false, val isPermanent: Boolean = false, val maxClients: Int = -1 ) data class ClientInfo( val id: Int, val nickname: String, val channelId: Long, val uid: String, val type: Int = 0, val serverGroups: List = emptyList() ) ``` 3. **状态仓库实现** ```kotlin // data/Repository.kt class ChannelRepository { private val _channels = MutableStateFlow>(emptyList()) val channels: StateFlow> = _channels private val _clients = MutableStateFlow>(emptyList()) val clients: StateFlow> = _clients private val _selfClientId = MutableStateFlow(null) val selfClientId: StateFlow = _selfClientId // 按频道 ID 索引的成员列表 private val _channelClients = MutableStateFlow>>(emptyMap()) val channelClients: StateFlow>> = _channelClients fun updateBaseline(channels: List, clients: List, selfClientId: Int) { _channels.value = channels _clients.value = clients _selfClientId.value = selfClientId // 建立频道-成员索引 _channelClients.value = clients.groupBy { it.channelId } } } ``` 4. **Bridge 层查询**(通过 TsClient 封装) ```kotlin // TSBridge.kt — 直接返回 Kotlin 友好类型,无需 JSON 解析 fun getChannelList(): List = TsClient.getChannelList() fun getClientList(): List = TsClient.getClientList() fun getClientId(): Long = TsClient.getClientId() ``` **同步状态机**: ```kotlin sealed class SyncState { object Unsynced : SyncState() // 已连接但尚无完整数据 object Syncing : SyncState() // 调用 ListChannels 和 ListClients object Synchronized : SyncState() // 列表基线可供 UI 使用 data class SyncFailed(val error: Throwable) : SyncState() // 同步失败 } ``` **原子提交原则**: - 频道列表、成员列表、自身 ID 必须全部成功才能提交 - 任一失败则不进入业务就绪状态 - 允许重试,避免在不完整数据上执行业务操作 ### 4.5 错误处理与状态反馈 **目标**:实现完整的错误处理机制,确保用户能获得清晰的状态反馈。 **任务**: 1. **连接错误分类** ```kotlin sealed class ConnectionError : Exception() { object InvalidAddress : ConnectionError() // 地址解析失败 object AuthenticationFailed : ConnectionError() // 密码错误 object ServerFull : ConnectionError() // 服务器满员 object Banned : ConnectionError() // 被封禁 object NetworkError : ConnectionError() // 网络问题 object Timeout : ConnectionError() // 连接超时 data class Other(val message: String) : ConnectionError() } ``` 2. **错误映射与处理** ```kotlin fun mapConnectionError(error: Exception): ConnectionError { val message = error.message?.lowercase() ?: "" return when { message.contains("resolve") || message.contains("address") -> ConnectionError.InvalidAddress message.contains("password") || message.contains("auth") -> ConnectionError.AuthenticationFailed message.contains("full") || message.contains("limit") -> ConnectionError.ServerFull message.contains("ban") -> ConnectionError.Banned message.contains("timeout") -> ConnectionError.Timeout message.contains("network") || message.contains("connection") -> ConnectionError.NetworkError else -> ConnectionError.Other(error.message ?: "Unknown error") } } ``` 3. **状态反馈 UI** ```kotlin @Composable fun ConnectionStatusIndicator(state: ConnectionState) { when (state) { ConnectionState.Disconnected -> { Text("未连接", color = MaterialTheme.colorScheme.onSurfaceVariant) } ConnectionState.Connecting -> { CircularProgressIndicator(modifier = Modifier.size(24.dp)) Text("正在连接...") } ConnectionState.Connected -> { CircularProgressIndicator(modifier = Modifier.size(24.dp)) Text("已连接,正在同步...") } ConnectionState.Syncing -> { CircularProgressIndicator(modifier = Modifier.size(24.dp)) Text("正在同步数据...") } ConnectionState.Ready -> { Icon(Icons.Default.CheckCircle, tint = Color.Green) Text("就绪") } is ConnectionState.Failed -> { Icon(Icons.Default.Error, tint = Color.Red) Text("连接失败: ${state.error.getLocalizedMessage()}", color = MaterialTheme.colorScheme.error) Button(onClick = { /* 重试 */ }) { Text("重试") } } } } ``` 4. **同步失败重试机制** ```kotlin // ServerViewModel.kt private suspend fun performInitialSyncWithRetry(maxRetries: Int = 3) { var retryCount = 0 while (retryCount < maxRetries) { try { performInitialSync() return // 成功则退出 } catch (e: Exception) { retryCount++ if (retryCount >= maxRetries) { _connectionState.value = ConnectionState.SyncFailed(e) return } // 等待后重试 delay(1000L * retryCount) } } } ``` 5. **断开连接清理** ```kotlin fun disconnect() { // 1. 停止语音(如有) voiceViewModel.stopVoice() // 2. 清理会话状态 repository.clearSession() // 3. 调用 SDK 断开 TSBridge.disconnect() // 4. 更新状态 _connectionState.value = ConnectionState.Disconnected } ``` **错误日志记录**: ```kotlin private fun logConnectionError(error: ConnectionError) { Log.e(TAG, "Connection error: ${error::class.simpleName}", error) // 可选:上报到崩溃分析服务 } ``` --- ## 三、验收标准 ### 功能验收 - [ ] **Identity 持久化** - 首次启动自动生成 Identity 并存储 - 后续启动读取已有 Identity,不重复生成 - 清除应用数据后能重新生成 - [ ] **连接流程** - 输入有效地址、昵称后能成功连接服务器 - 输入错误密码时显示明确错误提示 - 连接超时时(30秒)显示超时错误 - 无网络时显示网络错误 - [ ] **首次同步** - 连接成功后自动执行首次同步 - 频道列表正确显示(包含所有频道) - 成员列表正确显示(包含所有在线用户) - 自己的客户端 ID 正确识别 - [ ] **状态反馈** - 连接过程中显示加载状态 - 同步过程中显示同步状态 - 就绪后显示就绪状态 - 错误时显示错误信息和重试按钮 - [ ] **断开连接** - 主动断开后状态正确重置 - 被动断开(网络中断)能检测并提示 - 被踢出时显示踢出原因 ### 性能验收 - [ ] 首次同步在 3 秒内完成(标准服务器,< 100 频道,< 500 用户) - [ ] 连接建立时间 < 5 秒(正常网络环境) ### 代码质量验收 - [ ] 所有网络操作在 IO 线程执行 - [ ] 事件回调通过事件队列串行化 - [ ] 无内存泄漏(正确取消协程) - [ ] 错误处理覆盖所有已知异常场景 ### 测试用例 | 场景 | 输入 | 预期结果 | |------|------|----------| | 正常连接 | 有效地址、昵称、无密码 | 连接成功,频道列表显示 | | 密码保护服务器 | 有效地址、昵称、正确密码 | 连接成功 | | 错误密码 | 有效地址、昵称、错误密码 | 显示"密码错误"提示 | | 无效地址 | 无效地址 | 显示"地址解析失败"提示 | | 网络断开 | 断开网络后连接 | 显示"网络错误"提示 | | 服务器满员 | 满员服务器 | 显示"服务器已满"提示 | | 被封禁 | 被封禁的 UID | 显示"已被封禁"提示 | | 连接超时 | 阻断 UDP 30 秒 | 显示"连接超时"提示 | | 断开重连 | 断开后重新连接 | 状态正确重置,可重新连接 | --- ## 四、参考文档 - `docs/流程/01_连接服务器.md` - 完整生命周期、时序图 - `docs/流程/08_状态同步.md` - ① 首次同步 - `docs/sdk文档-go.md` - 连接管理 API