首次推送

This commit is contained in:
sansen
2026-07-20 19:01:03 +08:00
parent ea01b9cf99
commit ef1bf61f9f
4484 changed files with 937163 additions and 1 deletions
@@ -0,0 +1,320 @@
# 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 兼容性修复