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

321 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 兼容性修复