Files
2026-07-20 19:01:03 +08:00

15 KiB
Raw Permalink Blame History

步骤 02Bridge 层实现( 已完成)

实现 Go ↔ Kotlin 桥接层,包括 Go 侧 TSClient 导出、Kotlin 友好封装(TsClient.kt + TsModels.kt)、应用层桥接(TSBridge.kt)。


一、目标

  • Go 侧 TSClient 结构体及方法导出(go/teamspeak/bridge.go
  • gomobile 编译为 AARandroid/app/libs/teamspeak.aar
  • Kotlin 友好封装层(TsClient.kt + TsModels.kt
  • 应用层桥接(TSBridge.kt

二、架构总览

┌─────────────────────────────────────────────────────────────┐
│  Kotlin 应用层                                               │
│                                                             │
│  ViewModel ──→ TSBridge (object) ──→ TsClient (object)      │
│                    │                       │                 │
│                    │ TsListener 回调        │ Kotlin 友好 API  │
│                    ↑                       │                 │
│                    └── TsListener ─────────┘                 │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│  gomobile 自动生成层                                         │
│                                                             │
│  teamspeak.TSClient (Java) ← Go TSClient struct             │
│  teamspeak.EventCallback (Java) ← Go EventCallback interface│
│  teamspeak.* 数据类 (Java) ← Go 导出结构体                   │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│  Go 层                                                      │
│                                                             │
│  bridge.go (TSClient + EventCallback) ──→ teamspeak-go SDK  │
│       │                                         │           │
│       │ 事件队列 + 单消费者 goroutine              │ On* 回调  │
│       └──→ JNI 回调 ──→ EventCallback            │           │
│                                                             │
└─────────────────────────────────────────────────────────────┘

数据流

方向 路径 说明
Kotlin → Go TSBridgeTsClient → gomobile TSClient → Go Client 用户操作
Go → Kotlin Go On* → 事件队列 → JNI → EventCallbackTsClient 转换 → TsListenerTSBridge 事件推送

三、三层架构详解

3.1 Go 层(bridge.go

文件go/teamspeak/bridge.go

Go 侧 Bridge 是 gomobile 导出的核心,将 teamspeak-go SDK 封装为可供 Kotlin 调用的 TSClient 类。

导出的接口与结构

// EventCallback — gomobile 导出的回调接口
type EventCallback interface {
    OnConnected()
    OnDisconnected(message string)
    OnTextMessage(msg *TextMsg)
    OnClientEnter(client *ServerClientView)
    OnClientLeave(client *ServerClientView)
    OnClientMoved(client *ClientMoved)
    OnKicked(reason *ServerError)
    OnTalkStatusChanged(talker *TalkStatusChange)
    OnClientIDsDone()
    OnServerError(error *ServerError)
}

// TSClient — gomobile 导出的桥接客户端
type TSClient struct {
    client   *teamspeak.Client
    callback EventCallback
}

// 导出的数据结构(适配 gomobile 限制)
type ServerClientView struct { ... }  // 频道成员视图
type ClientInfo struct { ... }        // 完整客户端信息
type ChannelListItem struct { ... }   // 频道列表项
type TextMsg struct { ... }           // 文本消息
type ClientMoved struct { ... }       // 客户端移动事件
type ServerError struct { ... }       // 服务器错误
type TalkStatusChange struct { ... }  // 说话状态变化
type ChannelListResult struct { ... } // 频道列表结果(含数组)

关键设计决策

  1. 字符串传递 IDgomobile 不支持 uint64 → Kotlin Long 的安全映射,统一用 string
  2. 结构体包装gomobile 不支持 []*T,用 ChannelListResult 包装 ChannelListItem[]
  3. 错误用字符串gomobile 不支持 error 返回值,用空串=成功
  4. 事件队列SDK 内部 evtQueue + 单消费者 goroutine 串行分发,JNI 回调线程安全

3.2 Kotlin 友好封装层(TsClient.kt + TsModels.kt

文件android/app/src/main/java/com/tsmobile/app/TsClient.ktTsModels.kt

这一层将 gomobile 生成的 Java 对象转为 Kotlin 友好接口,解决:

  • 所有 API 为 getter/setter 而非属性
  • ChannelListItem[] 需要手动转 List<TsChannel>
  • 事件回调参数是 Object 类型需要强转
  • group 类型是 Int 需要映射为 ChanGroupType 枚举

TsClient 对象

object TsClient {
    // 连接
    fun connect(identity, host, nickname, password, defaultChannel): Boolean
    fun disconnect()

    // 事件监听
    fun setListener(listener: TsListener?)

    // 查询(返回 Kotlin 友好类型)
    fun getChannelList(): List<TsChannel>
    fun getClientList(): List<TsClientInfo>
    fun getClientId(): Long
    fun getClientInfo(clientId: Long): TsClientInfo?
    fun getChannelInfo(channelId: Long): TsChannel?
    fun getSelf(): TsSelf
    fun getChannelIdsByUid(uid: String, maxDepth: Int): List<Long>

    // 属性
    val serverVersion: String
    val serverIp: String
    val serverPlatform: String
    val serverName: String
    val serverCreated: Long
    val serverUptime: Long
    val maxClients: Int
    val clientsOnline: Int
    val channelsOnline: Int

    // 操作
    fun sendTextMessage(targetMode: Int, targetId: Long, msg: String): Boolean
    fun clientMove(clientId: Long, channelId: Long, password: String): Boolean
    fun clientPoke(clientId: Long, msg: String): Boolean
    fun kickClient(clientId: Long, reasonId: Int, reasonMsg: String): Boolean
    fun banClient(uid: String, timeInSeconds: Long, reasonMsg: String): Boolean
    fun channelCreate(name: String, properties: Map<String, String>, permissions: List<TSPermission>): Long
    fun channelUpdate(channelId: Long, properties: Map<String, String>): Boolean
    fun channelDelete(channelId: Long, force: Boolean): Boolean
    fun sendVoice(clientId: Long, codec: Int, data: ByteArray): Boolean
}

TsListener 接口

interface TsListener {
    fun onConnected()
    fun onDisconnected(error: String?)
    fun onClientEnter(client: TsClientInfo)
    fun onClientMoved(moved: TsClientMoved)
    fun onClientLeave(client: ServerClientView)
    fun onTalkStatusChanged(talker: TsTalker)
    fun onClientIDsDone()
    fun onTextMessage(msg: TsTextMessage)
    fun onPoked(msg: TsTextMessage)
    fun onKicked(reason: ServerError?)
    fun onServerError(error: ServerError)
    fun onChannelListChanged()
}

TsModels 数据类

// 频道
data class TsChannel(val channelListItem: ChannelListItem) {
    val channelId get() = channelListItem.getChannelID()
    val parentId get() = channelListItem.getParentChannelID()
    val name get() = channelListItem.getName()
    val order get() = channelListItem.getOrder()
    val isPermanent get() = channelListItem.getIsPermanent()
    val totalClients get() = channelListItem.getTotalClients()
    // ... 更多属性
}

// 客户端
data class TsClientInfo(val serverClientView: ServerClientView) {
    val clientId get() = serverClientView.getID()
    val channelId get() = serverClientView.getChannelID()
    val nickname get() = serverClientView.getNickname()
    val uid get() = serverClientView.getUid()
    val isTalker get() = serverClientView.getIsTalker()
    // ... 更多属性
}

// 自身信息(可写)
class TsSelf internal constructor(
    private val view: ServerClientView,
    private val info: ClientInfo?
) {
    var nickname
        get() = view.getNickname()
        set(value) { info?.setNickname(value) }
    var isTalker
        get() = view.getIsTalker()
        set(value) { info?.setIsTalker(value) }
    var inputMuted
        get() = info?.getInputMuted() ?: false
        set(value) { info?.setInputMuted(value) }
    var outputMuted
        get() = info?.getOutputMuted() ?: false
        set(value) { info?.setOutputMuted(value) }
    // ... 更多可写属性
}

// 移动事件
data class TsClientMoved(
    val theClient: ServerClientView?,
    val oldChannel: ChannelListItem?,
    val newChannel: ChannelListItem?,
    val visibility: Int
)

// 说话状态
data class TsTalker(
    val client: ServerClientView?,
    val isTalking: Boolean
)

// 文本消息
data class TsTextMessage(
    val message: String,
    val invokerUid: String,
    val invokerName: String,
    val invokerId: Long,
    val targetMode: Int,
    val targetClientId: Long,
    val targetChannelId: Long
)

// 频道组/服务器组类型
enum class ChanGroupType(val value: Int) { ... }
enum class ChannelGroupType(val value: Int) { ... }

3.3 应用层桥接(TSBridge.kt

文件android/app/src/main/java/com/tsmobile/app/TSBridge.kt

TSBridge 是 ViewModel 层访问 Go 桥接的唯一入口,采用 object 单例模式。

核心职责

  1. 连接管理:封装 TsClient.connect() / disconnect()
  2. 监听注册:在 connect() 时自动设置 TsListener
  3. 查询转发:所有查询方法委托给 TsClient
  4. 操作转发:所有操作方法委托给 TsClient

完整 API

object TSBridge {
    // === 连接管理 ===
    fun connect(identity: Any, host: String, nickname: String,
                password: String = "", defaultChannel: String = "",
                listener: TSBridgeListener? = null): Boolean
    fun disconnect()

    // === 查询 ===
    fun getChannelList(): List<TsChannel>
    fun getClientList(): List<TsClientInfo>
    fun getClientId(): Long
    fun getSelf(): TsSelf
    fun getChannelInfo(channelId: Long): TsChannel?
    fun getClientInfo(clientId: Long): TsClientInfo?
    fun getChannelIdsByUid(uid: String): List<Long>

    // === 操作 ===
    fun sendTextMessage(targetMode: Int, targetId: Long, msg: String): Boolean
    fun clientMove(clientId: Long, channelId: Long, password: String = ""): Boolean
    fun clientPoke(clientId: Long, msg: String): Boolean
    fun kickClient(clientId: Long, reasonId: Int, reasonMsg: String): Boolean
    fun banClient(uid: String, timeInSeconds: Long, reasonMsg: String): Boolean
    fun channelCreate(name: String, properties: Map<String, String> = emptyMap(),
                      permissions: List<TSPermission> = emptyList()): Long
    fun channelUpdate(channelId: Long, properties: Map<String, String>): Boolean
    fun channelDelete(channelId: Long, force: Boolean = false): Boolean
    fun sendVoice(clientId: Long, codec: Int, data: ByteArray): Boolean

    // === 服务器属性 ===
    val serverVersion: String
    val serverIp: String
    val serverPlatform: String
    val serverName: String
    // ...
}

// 回调接口(简化版,供 ViewModel 使用)
interface TSBridgeListener {
    fun onConnected()
    fun onDisconnected(error: String?)
    fun onClientEnter(client: TsClientInfo)
    fun onClientMoved(moved: TsClientMoved)
    fun onClientLeave(client: ServerClientView)
    fun onTalkStatusChanged(talker: TsTalker)
    fun onClientIDsDone()
    fun onTextMessage(msg: TsTextMessage)
    fun onPoked(msg: TsTextMessage)
    fun onKicked(reason: ServerError?)
    fun onServerError(error: ServerError)
}

四、数据流转详解

4.1 连接流程

ViewModel: TSBridge.connect(identity, host, nickname, password)
    ↓
TSBridge: TsClient.connect(identity, host, nickname, password)
    ↓
TsClient: TSClient.connect(...)  // gomobile Java 对象
    ↓
Go: TSClient.Connect() → NewClient → registerHandlers → Connect → WaitConnected
    ↓
Go SDK: 收到 welcome → 触发 OnConnected
    ↓
Go Bridge: EventCallback.OnConnected()  // JNI
    ↓
TsClient: listener?.onConnected()  // Kotlin 友好回调
    ↓
TSBridge: listener?.onConnected()
    ↓
ViewModel: _state.update { it.copy(connected = true) }

4.2 查询流程

ViewModel: TSBridge.getChannelList()
    ↓
TSBridge: TsClient.getChannelList()
    ↓
TsClient: TSClient.requestChannelList()  // gomobile
    ↓
TsClient: result.getChannels().map { TsChannel(it) }  // 转为 Kotlin 类型
    ↓
TSBridge: 返回 List<TsChannel>
    ↓
ViewModel: _state.update { it.copy(channels = list) }

4.3 事件推送流程

TeamSpeak 服务器: notifyclientmoved
    ↓
Go SDK: handleNotification → evtQueue → startEventLoop
    ↓
Go Bridge: EventCallback.OnClientMoved(clientMoved)
    ↓ JNI
TsClient: TsListener.onClientMoved(TsClientMoved(view, old, new, vis))
    ↓
TSBridge: listener?.onClientMoved(moved)
    ↓
ViewModel: 处理移动事件,更新状态

五、gomobile 约束与应对

约束 影响 应对方案
不能导出 []string 服务器组列表无法直接传递 TsClientInfo 封装为逗号分隔字符串
不能导出 []*T 频道/客户端列表无法返回切片 ChannelListResult 包装 + TsClientList
不能导出 map[string]string 属性列表无法传递 channelCreate/channelUpdate 接受 Map,内部转 gomobile 类型
不能导出 error 方法无法返回错误 Boolean 返回值(true=成功)
uint64 映射为 long 频道 ID 可能溢出 统一用 string 传递 ID
回调在 JNI 线程 不能直接操作 UI TsListener 回调 → ViewModel + StateFlow 中转
所有字段为 getter/setter Kotlin 不友好 TsModels 包装为 val/var 属性

六、文件清单

文件 说明
go/teamspeak/bridge.go Go 侧桥接,导出 TSClient + EventCallback
android/app/libs/teamspeak.aar gomobile 编译产物
android/app/src/main/java/com/tsmobile/app/TsClient.kt Kotlin 友好封装(TsClient 对象)
android/app/src/main/java/com/tsmobile/app/TsModels.kt Kotlin 数据类(TsChannel, TsClientInfo 等)
android/app/src/main/java/com/tsmobile/app/TSBridge.kt 应用层桥接(单例)

七、验收标准

# 验证项 验证方法
1 bridge.go 可编译 cd go && go build ./teamspeak 无报错
2 gomobile 生成 AAR gomobile bind ... 成功
3 TsClient.connect() 可调用 编译通过,连接测试服务器成功
4 TsListener 回调正常 连接后收到 onConnectedonClientEnter
5 查询返回 Kotlin 类型 getChannelList() 返回 List<TsChannel>
6 操作方法正常 sendTextMessageclientMove 等返回 true
7 断开连接无崩溃 disconnect() 后应用正常退出

八、参考文档

  • docs/sdk-bridge-api.md — TsClient API 完整参考
  • docs/流程/00_总览.md — 运行架构、三条核心通道
  • docs/流程/01_连接服务器.md — 连接时序、事件依赖
  • CLAUDE.md — gomobile 限制说明、JNI 线程注意事项