# teamspeak-go 纯 Go 实现的 TeamSpeak 3 客户端协议库,零 CGO 依赖,支持 gomobile 编译为 Android `.aar`。 - GitHub: https://github.com/honeybbq/teamspeak-go - 本项目使用本地补丁版本(`go/_patches/`),通过 `go.mod` 的 `replace` 指令生效 ## Architecture ``` ┌──────────────────────────────────────────────┐ │ Application Layer │ │ (bridge.go / 用户代码) │ │ OnConnected, OnTextMessage, SendVoice ... │ ├──────────────────────────────────────────────┤ │ Client (client.go) │ │ 连接管理、事件分发、命令追踪 │ │ events.go — 事件注册 & dispatchEvent │ │ commands.go — 命令发送 & return_code 匹配 │ │ notifications.go — notify* 事件解析 │ │ api.go — 高层 API (ListChannels, SendText...)│ │ handshake.go — 加密握手 & clientinit │ │ transfer.go — 文件传输 │ │ throttle.go — 命令限速 (token bucket) │ ├──────────────────────────────────────────────┤ │ Transport (transport/) │ │ PacketHandler — UDP 收发、包分片 │ │ packet.go — 包类型定义 │ │ quicklz.go — QuickLZ 压缩解压 │ ├──────────────────────────────────────────────┤ │ Crypto (crypto/) │ │ Identity 生成、密钥交换、EAX 加密 │ ├──────────────────────────────────────────────┤ │ Handshake (handshake/) │ │ crypt_init2.go — 二次加密协商 │ │ license.go — 许可证验证 │ ├──────────────────────────────────────────────┤ │ Discovery (discovery/) │ │ SRV / TSDNS / 直连 地址解析 │ └──────────────────────────────────────────────┘ ``` ### Key Files | 文件 | 职责 | |------|------| | `client.go` | `Client` 结构体、`NewClient`、`Connect`、`Disconnect`、事件循环 (`startEventLoop`)、`notifyEvent` 入队 | | `events.go` | 事件注册 API (`OnConnected`, `OnTextMessage` 等)、`dispatchEvent` 分发到所有 handler | | `notifications.go` | `handleNotification` — 解析 `notify*` 命令,转换为结构体事件 | | `commands.go` | `handlePacket` — 包类型路由、`ExecCommand` / `ExecCommandWithResponse` — 带 `return_code` 的异步命令 | | `api.go` | 高层 API:`ListChannels`、`ListClients`、`GetClientInfo`、`SendTextMessage`、`ClientMove`、`Poke`、`SendVoice`、`WaitConnected` | | `handshake.go` | `handleHandshakeInitIV`、`handleHandshakeExpand2`、`handleInitServer`、`sendClientInit` | | `types.go` | 所有事件/数据结构体定义 | | `transfer.go` | 文件传输:`FileTransferInitUpload`、`FileTransferInitDownload`、`FileTransferDeleteFile` | | `throttle.go` | Token-bucket 命令限速器(4 tokens/s,上限 8) | ### Event Loop 所有事件在单个 goroutine 中串行处理,保证按到达顺序分发: ``` notifyEvent(evt) ← 任何 goroutine 调用 → 入队 (evtQueueMu) → signal (evtCond) ↓ startEventLoop goroutine ← 唯一消费者 → 取出全部待处理事件 → dispatchEvent(evt) ← 逐个分发到注册的 handler ``` ## Event Lifecycle 事件分为两类:**服务器通知事件**(`notify` 前缀,服务器主动推送)和**内部事件**(SDK 状态变化触发)。 ### Phase 1: Connection (连接阶段) 客户端从发起到建立连接的握手过程,涉及加密协商和身份初始化。 | 事件名 | 类型 | 作用 | 描述 | |--------|------|------|------| | `PacketTypeInit1` | 握手包 | 初始化加密通道 | 客户端发送 UDP 包后服务器返回 init1 响应,完成第一阶段密钥交换。SDK 内部处理,应用层无感 | | `clientinitiv` | 服务器响应 | 加密参数协商 | 服务器返回 `alpha`、`beta`、`omega` 加密参数,客户端调用 `InitCrypto` 初始化加密上下文 | | `initivexpand2` | 服务器响应 | 二次加密扩展 | 服务器返回许可证和扩展密钥,客户端生成临时密钥对(`clientek`)完成最终加密握手 | | `clientinit` | 客户端命令 | 发送身份信息 | 加密握手完成后,客户端发送昵称、版本、HWID、默认频道等信息,请求加入服务器 | | `initserver` | 服务器响应 | 连接建立确认 | 服务器分配客户端 ID(`clid`),连接正式建立。触发 `OnConnected` 回调 | | **`OnConnected`** | SDK 回调 | 连接成功通知 | 应用层注册的连接成功回调。SDK 自动发送 `clientupdate` 解除静音。此时可调用 `ListChannels`、`ListClients` 获取快照 | ### Phase 2: Runtime — User Events (用户事件) 服务器上用户的上下线、移动、消息等实时事件。 | 事件名 | 类型 | 作用 | 描述 | |--------|------|------|------| | **`notifycliententerview`** → `OnClientEnter` | 服务器通知 | 用户进入视野 | 有新客户端连接到服务器或进入可见范围。数据结构 `ClientInfo`:`ID`(clid)、`Nickname`、`UID`、`ChannelID`、`Type`、`ServerGroups` | | **`notifyclientleftview`** → `OnClientLeave` | 服务器通知 | 用户离开视野 | 客户端断开连接或离开可见范围。数据结构 `ClientLeftViewEvent`:`ID`、`ReasonID`(0=正常、4=频道踢出、5=服务器踢出)、`ReasonMsg` | | **`notifyclientmoved`** → `OnClientMoved` | 服务器通知 | 用户切换频道 | 客户端被移动到另一个频道。数据结构 `ClientMovedEvent`:`ID`、`TargetChannelID`、`ReasonID`、`InvokerID`、`InvokerName`、`InvokerUID`。**如果是自己被移动,这是频道切换的确认点** | | **`notifytextmessage`** → `OnTextMessage` | 服务器通知 | 收到文字消息 | 收到私聊/频道/服务器范围的文字消息。数据结构 `TextMessage`:`TargetMode`(1=私聊、2=频道、3=服务器)、`TargetID`、`InvokerID`、`InvokerName`、`InvokerUID`、`Message`、`InvokerGroups` | | **`notifyclientpoke`** → `OnPoked` | 服务器通知 | 被其他用户戳 | 有用户发送 poke 消息。数据结构 `PokeEvent`:`InvokerID`、`InvokerName`、`InvokerUID`、`Message` | | `notifyclientneededpermissions` | 服务器通知 | 权限不足 | 操作因权限不足被拒绝。携带 `permid`、`permvalue`。SDK 仅记录日志,不触发应用层回调 | ### Phase 3: Runtime — Voice (语音事件) 实时语音数据的收发,通过 UDP 二进制包传输。 | 事件名 | 类型 | 作用 | 描述 | |--------|------|------|------| | **`VoiceDataEvent`** → `OnVoiceData` | 二进制包 | 收到语音数据 | 其他客户端发送的 Opus 语音帧。数据结构 `VoiceDataEvent`:`ClientID`(发送者)、`Data`(Opus 数据)、`Codec`(4=Opus Voice、5=Opus Music) | | `SendVoice(data, codec)` | 客户端命令 | 发送语音数据 | 发送 Opus 编码的语音帧到服务器,服务器转发给同频道其他用户。通过 `handler.SendVoicePacket` 直接写入 UDP | **语音包二进制格式:** ``` Offset Size Field 0 2 packetID (big-endian) 2 2 clientID (little-endian) 4 1 codec (4=Opus Voice, 5=Opus Music) 5 N Opus encoded data ``` ### Phase 4: Runtime — File Transfer (文件传输事件) 服务器文件的上传、下载和管理。文件数据通过独立的 TCP 连接传输。 | 事件名 | 类型 | 作用 | 描述 | |--------|------|------|------| | `ftinitupload` / **`notifystartupload`** | 命令+通知 | 初始化文件上传 | 客户端请求上传文件,服务器返回 `FileUploadInfo`:`Port`(TCP 端口)、`FileTransferKey`(传输密钥)、`SeekPosition`(断点)、`ClientFileTransferID`、`ServerFileTransferID` | | `ftinitdownload` / **`notifystartdownload`** | 命令+通知 | 初始化文件下载 | 客户端请求下载文件,服务器返回 `FileDownloadInfo`:`Port`、`FileTransferKey`、`Size`、`ClientFileTransferID`、`ServerFileTransferID` | | **`notifystatusfiletransfer`** | 服务器通知 | 文件传输状态 | 传输完成或失败的状态通知。数据结构 `FileTransferStatusInfo`:`Status`(0=成功)、`Message`(错误信息)、`ClientFileTransferID` | | `ftdeletefile` | 客户端命令 | 删除服务器文件 | 删除指定频道目录下的文件,支持 `\|` 分隔的批量删除 | ### Phase 5: Runtime — Commands & Errors (命令与错误) 客户端命令的异步响应机制和错误处理。 | 事件名 | 类型 | 作用 | 描述 | |--------|------|------|------| | `return_code` | 服务器响应 | 命令执行结果 | 每个命令附带 `return_code` 参数,服务器执行完成后返回对应的 `return_code`,SDK 通过 `commandTracker` 匹配异步响应。`id=0` 表示成功,非 0 为错误码 | | **`error`** | 服务器响应 | 服务器错误 | 服务器返回的错误信息。`id`(错误码)、`msg`(错误描述)。**错误码 3329 为致命错误(如被 ban),SDK 自动断开连接** | | `clientupdate` | 客户端命令 | 更新客户端状态 | 连接成功后 SDK 自动发送,设置 `client_input_muted=0`、`client_output_muted=0` | **命令限速:** SDK 内置 token-bucket 限速器(`throttle.go`),速率 4 tokens/s,上限 8。所有 `ExecCommand` 调用自动排队等待。 ### Phase 6: Disconnect (断开阶段) 连接关闭和清理。 | 事件名 | 类型 | 作用 | 描述 | |--------|------|------|------| | `clientdisconnect` | 客户端命令 | 主动断开 | 客户端发送 `clientdisconnect reasonmsg=Shutdown`,然后关闭 UDP 连接 | | **`OnDisconnected`** | SDK 回调 | 断开连接通知 | 连接断开时触发。参数 `error`:`nil` = 正常断开,非 `nil` = 异常断开原因 | | `OnClosed` (handler) | 传输层回调 | 底层连接关闭 | UDP 连接关闭时触发,SDK 内部据此调用 `OnDisconnected`。应用层不直接使用 | ### Lifecycle Overview (生命周期总览) ``` NewClient(identity, addr, nickname, opts...) │ ├─ 启动事件循环协程 (startEventLoop) ├─ 初始化 PacketHandler、Crypto、Throttle │ Connect() │ ├─ 地址解析 (SRV / TSDNS / 直连) ├─ UDP 连接 ├─ PacketTypeInit1 握手 ├─ ← clientinitiv (加密参数) ├─ → InitCrypto ├─ ← initivexpand2 (二次加密) ├─ → clientek (临时密钥) ├─ → clientinit (身份信息) ├─ ← initserver → 连接建立 │ ├─ → OnConnected() 回调 ├─ → clientupdate (解除静音) │ │ ┌─ API 调用 ─────────────────────────────┐ │ │ ListChannels() → channellist │ │ │ ListClients() → clientlist │ │ │ GetClientInfo() → clientinfo │ │ │ SendTextMessage() → sendtextmessage │ │ │ ClientMove() → clientmove │ │ │ SendVoice() → UDP 二进制包 │ │ └─────────────────────────────────────────┘ │ │ ┌─ 服务器事件 ────────────────────────────┐ │ │ notifycliententerview → OnClientEnter │ │ │ notifyclientleftview → OnClientLeave │ │ │ notifyclientmoved → OnClientMoved │ │ │ notifytextmessage → OnTextMessage │ │ │ notifyclientpoke → OnPoked │ │ │ VoiceDataEvent → OnVoiceData │ │ │ error (id=3329) → 自动断开 │ │ └─────────────────────────────────────────┘ │ Disconnect() │ ├─ → clientdisconnect reasonmsg=Shutdown ├─ → 关闭 UDP 连接 (handler.Close) └─ → OnDisconnected(nil) 回调 ``` ## Data Types | 结构体 | 用途 | 关键字段 | |--------|------|----------| | `ClientInfo` | 客户端信息 | `ID` (uint16), `Nickname`, `UID`, `ChannelID` (uint64), `Type`, `ServerGroups` ([]string) | | `ChannelInfo` | 频道信息 | `ID` (uint64), `Name`, `ParentID` (uint64), `Description` | | `TextMessage` | 文字消息 | `TargetMode` (int), `TargetID` (uint64), `InvokerID` (uint16), `InvokerName`, `InvokerUID`, `Message`, `InvokerGroups` | | `ClientMovedEvent` | 频道移动 | `ID` (uint16), `TargetChannelID` (uint64), `ReasonID` (int), `InvokerID` (uint16), `InvokerName`, `InvokerUID` | | `ClientLeftViewEvent` | 用户离开 | `ID` (uint16), `ReasonID` (int), `ReasonMsg`, `TargetID` (uint16) | | `PokeEvent` | Poke 消息 | `InvokerID` (uint16), `InvokerName`, `InvokerUID`, `Message` | | `VoiceDataEvent` | 语音数据 | `ClientID` (uint16), `Data` ([]byte), `Codec` (byte) | | `FileUploadInfo` | 上传信息 | `Port`, `FileTransferKey`, `SeekPosition`, `ClientFileTransferID`, `ServerFileTransferID` | | `FileDownloadInfo` | 下载信息 | `Port`, `FileTransferKey`, `Size`, `ClientFileTransferID`, `ServerFileTransferID` | | `FileTransferStatusInfo` | 传输状态 | `Status` (int), `Message`, `ClientFileTransferID` | ## API Reference ### Client Construction ```go identity, _ := crypto.GenerateIdentity(8) client := teamspeak.NewClient(identity, "host:9987", "Nickname", teamspeak.WithServerPassword("pass"), teamspeak.WithDefaultChannel("/General"), teamspeak.WithDefaultChannelPassword("cpw"), teamspeak.WithLogger(logger), teamspeak.WithResolver(customResolver), ) ``` ### Connection ```go client.Connect() // 启动连接(非阻塞) client.WaitConnected(context.Background()) // 阻塞等待握手完成 client.Disconnect() // 优雅断开 client.ClientID() // 获取服务器分配的客户端 ID ``` ### Data Queries ```go channels, err := client.ListChannels() // 获取所有频道 clients, err := client.ListClients() // 获取所有在线客户端 info, err := client.GetClientInfo(clid) // 获取单个客户端详情 ``` ### Messaging ```go client.SendTextMessage(targetMode, targetID, message) // 发送文字消息 client.ClientMove(clid, channelID, password) // 移动频道 client.Poke(clid, message) // Poke 用户 ``` ### Voice ```go client.SendVoice(opusData, 4) // 发送 Opus Voice 帧 (codec=4) client.SendVoice(opusData, 5) // 发送 Opus Music 帧 (codec=5) ``` ### File Transfer ```go info, err := client.FileTransferInitUpload(channelID, "/path", "", size, false) teamspeak.UploadFileData(host, info, reader) info, err := client.FileTransferInitDownload(channelID, "/path", "") teamspeak.DownloadFileData(host, info, writer) client.FileTransferDeleteFile(channelID, []string{"/file1", "/file2"}) ``` ### Event Registration ```go client.OnConnected(func() { ... }) client.OnDisconnected(func(err error) { ... }) client.OnClientEnter(func(info teamspeak.ClientInfo) { ... }) client.OnClientLeave(func(evt teamspeak.ClientLeftViewEvent) { ... }) client.OnClientMoved(func(evt teamspeak.ClientMovedEvent) { ... }) client.OnTextMessage(func(msg teamspeak.TextMessage) { ... }) client.OnPoked(func(evt teamspeak.PokeEvent) { ... }) client.OnKicked(func(reason string) { ... }) client.OnVoiceData(func(evt teamspeak.VoiceDataEvent) { ... }) ``` ### Middleware ```go // 命令中间件:拦截/修改出站命令 client.UseCommandMiddleware(func(next func(string) error) func(string) error { return func(cmd string) error { log.Println("CMD:", cmd) return next(cmd) } }) // 事件中间件:拦截/修改入站事件 client.UseEventMiddleware(func(next func(any)) func(any) { return func(evt any) { log.Println("EVT:", evt) next(evt) } }) ``` ## Local Patches 本项目通过 `go.mod` 的 `replace` 指令使用本地补丁版本: ``` replace github.com/honeybbq/teamspeak-go => ./_patches/github.com/honeybbq/teamspeak-go ``` 补丁修复了上游库的以下问题: - 32 位目标平台的整数溢出(`math.MaxUint32 overflows int`) - 其他 gomobile 兼容性修复