1. 7.3 客户端实现

服务端写完了,我们来实现客户端。examples/im_single_client/client.go 与单进程、多进程 Gate 都可直连(同一套线协议与默认 msgId);仓库中该文件还包含 GM-TLS载荷 SM4-GCM 等与服务端 flag 对齐的联调开关,本节摘录为 明文 TCP 最小路径。

1.1. 7.3.1 客户端功能

我们的客户端需要实现以下功能:

  1. 连接服务器:通过 TCP 连接到 Gateway
  2. 发送消息:登录、加入房间、离开房间、发送聊天消息
  3. 接收消息:显示服务器返回的消息
  4. 交互界面:命令行交互,用户输入聊天内容

1.2. 7.3.2 协议实现

客户端使用和服务器相同的协议格式:

// 消息格式:msgId(4) + seqId(4) + dataLen(4) + data

使用 zhenyi-base 提供的 ztcp.Clientznet 模块:

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 本节要点

  1. 协议层znet.GetNetMessage + SetDataCopy 组帧发送。
  2. 网络层:默认 ztcp.NewClient;国密与载荷加密见仓库 im_single_client 顶部注释。
  3. 交互层:stdin 命令与 msgId 可由 flag 指定,便于对准多进程示例。

7.4 多进程 Gate 联调时,保持 -addr 与 msgId 与服务端一致即可。

results matching ""

    No results matching ""