首次推送
This commit is contained in:
@@ -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 兼容性修复
|
||||
Reference in New Issue
Block a user