1. 7.3 客户端实现
服务端写完了,我们来实现客户端。examples/im_single_client/client.go 与单进程、多进程 Gate 都可直连(同一套线协议与默认 msgId);仓库中该文件还包含 GM-TLS、载荷 SM4-GCM 等与服务端 flag 对齐的联调开关,本节摘录为 明文 TCP 最小路径。
1.1. 7.3.1 客户端功能
我们的客户端需要实现以下功能:
- 连接服务器:通过 TCP 连接到 Gateway
- 发送消息:登录、加入房间、离开房间、发送聊天消息
- 接收消息:显示服务器返回的消息
- 交互界面:命令行交互,用户输入聊天内容
1.2. 7.3.2 协议实现
客户端使用和服务器相同的协议格式:
// 消息格式:msgId(4) + seqId(4) + dataLen(4) + data
使用 zhenyi-base 提供的 ztcp.Client 和 znet 模块:
client, err := ztcp.NewClient(*addr, znet.WithAsyncMode())
1.3. 7.3.3 消息发送
var seq atomic.Uint32
send := func(msgID int32, payload any) {
// 将 payload 序列化为 JSON
b, err := json.Marshal(payload)
if err != nil {
fmt.Printf("marshal payload failed: %v\n", err)
return
}
// 从池中获取消息
m := znet.GetNetMessage()
defer m.Release()
// 填充协议头
m.MsgId = msgID
m.SeqId = seq.Add(1)
m.SetDataCopy(b) // SetDataCopy 会拷贝数据到池化 buffer
// 发送
client.SendMsg(m)
}
注意这里使用的是 SetDataCopy,会拷贝数据到池化 buffer,确保发送过程中数据不会被回收。
1.4. 7.3.4 消息接收
client.SetReadCall(func(w ziface.IWireMessage) {
fmt.Printf("[recv] msgId=%d seq=%d data=%q\n",
w.GetMsgId(),
w.GetSeqId(),
string(w.GetMessageData()))
})
client.Read()
SetReadCall 设置收到消息时的回调,client.Read() 启动读取循环。
1.5. 7.3.5 命令行交互
in := bufio.NewScanner(os.Stdin)
for in.Scan() {
line := strings.TrimSpace(in.Text())
switch line {
case "":
continue
case "/quit":
return
case "/leave":
send(int32(*msgLeave), map[string]any{"room": *room})
default:
if strings.HasPrefix(line, "/join ") {
// 切换房间
nextRoom := strings.TrimSpace(strings.TrimPrefix(line, "/join "))
send(int32(*msgJoin), map[string]any{
"room": nextRoom,
"nickname": *nickname,
})
*room = nextRoom
} else {
// 发送聊天消息
send(int32(*msgSend), map[string]any{
"room": *room,
"text": line,
})
}
}
}
支持的命令:
| 命令 | 功能 |
|---|---|
| 直接输入文字 | 发送到当前房间 |
/join <房间> |
加入指定房间 |
/leave |
离开当前房间 |
/quit |
退出客户端 |
1.6. 7.3.6 完整代码
package main
import (
"bufio"
"encoding/json"
"flag"
"fmt"
"os"
"strings"
"sync/atomic"
"github.com/aiyang-zh/zhenyi-base/ziface"
"github.com/aiyang-zh/zhenyi-base/znet"
"github.com/aiyang-zh/zhenyi-base/ztcp"
)
func main() {
var (
addr = flag.String("addr", "127.0.0.1:8001", "gate addr")
userID = flag.Int64("user", 10001, "user id")
nickname = flag.String("nick", "alice", "nickname")
room = flag.String("room", "lobby", "room")
msgLogin = flag.Int("msgLogin", 1, "login request msg id")
msgJoin = flag.Int("msgJoin", 2, "join room request msg id")
msgLeave = flag.Int("msgLeave", 3, "leave room request msg id")
msgSend = flag.Int("msgSend", 4, "send room message request msg id")
)
flag.Parse()
// 连接服务器
client, err := ztcp.NewClient(*addr, znet.WithAsyncMode())
if err != nil {
panic(err)
}
defer client.Close()
// 序列号生成器
var seq atomic.Uint32
// 发送消息
send := func(msgID int32, payload any) {
b, err := json.Marshal(payload)
if err != nil {
fmt.Printf("marshal payload failed: %v\n", err)
return
}
m := znet.GetNetMessage()
defer m.Release()
m.MsgId = msgID
m.SeqId = seq.Add(1)
m.SetDataCopy(b)
client.SendMsg(m)
}
// 设置消息回调
client.SetReadCall(func(w ziface.IWireMessage) {
fmt.Printf("[recv] msgId=%d seq=%d data=%q\n",
w.GetMsgId(), w.GetSeqId(), string(w.GetMessageData()))
})
client.Read()
// 登录 + 进房(登录 JSON 带 nickname 与仓库 client 一致;Gate 侧 login handler 仅解析 userId,多余字段忽略)
send(int32(*msgLogin), map[string]any{"userId": *userID, "nickname": *nickname})
send(int32(*msgJoin), map[string]any{"room": *room, "nickname": *nickname})
fmt.Printf("connected to %s user=%d nick=%s room=%s\n", *addr, *userID, *nickname, *room)
fmt.Println("输入聊天内容回车发送;/join 房间;/leave;/quit")
// 命令行循环
in := bufio.NewScanner(os.Stdin)
for in.Scan() {
line := strings.TrimSpace(in.Text())
switch line {
case "":
continue
case "/quit":
fmt.Println("bye")
return
case "/leave":
send(int32(*msgLeave), map[string]any{"room": *room})
default:
if strings.HasPrefix(line, "/join ") {
nextRoom := strings.TrimSpace(strings.TrimPrefix(line, "/join "))
if nextRoom == "" {
fmt.Println("usage: /join <room>")
continue
}
send(int32(*msgJoin), map[string]any{"room": nextRoom, "nickname": *nickname})
*room = nextRoom
} else {
send(int32(*msgSend), map[string]any{"room": *room, "text": line})
}
}
}
}
1.7. 7.3.7 运行测试
启动服务器:
go run ./examples/im_single_demo
启动客户端(另开一个终端):
go run ./examples/im_single_client
两个客户端可以互相聊天,测试效果:
# 客户端 A
connected to 127.0.0.1:8001 user=10001 nick=alice room=lobby
hello everyone
[recv] msgId=4 seq=... data="{...\"type\":\"chat_broadcast\",\"room\":\"lobby\",...\"text\":\"hello everyone!\"...}"
# 客户端 B
connected to 127.0.0.1:8001 user=10002 nick=bob room=lobby
[recv] msgId=4 seq=... data="{...\"type\":\"chat_broadcast\"...\"nickname\":\"alice\"...}"
hi alice
[recv] msgId=4 seq=... data="{...\"type\":\"chat_broadcast\"...\"nickname\":\"bob\"...}"
1.8. 7.3.8 本节要点
- 协议层:
znet.GetNetMessage+SetDataCopy组帧发送。 - 网络层:默认
ztcp.NewClient;国密与载荷加密见仓库im_single_client顶部注释。 - 交互层:stdin 命令与 msgId 可由 flag 指定,便于对准多进程示例。
与 7.4 多进程 Gate 联调时,保持 -addr 与 msgId 与服务端一致即可。