From ba187f6f85cea67f180aa913d3a63c343a957546 Mon Sep 17 00:00:00 2001 From: NianGao Dev Date: Sat, 15 Aug 2026 22:54:36 +0800 Subject: [PATCH] =?UTF-8?q?feat(net):=20=E5=8D=8F=E8=AE=AE=E7=BC=96?= =?UTF-8?q?=E8=A7=A3=E7=A0=81=EF=BC=88=E5=B8=A7/varint/=E6=B6=88=E6=81=AF?= =?UTF-8?q?=EF=BC=89=E4=B8=8E=E6=9C=8D=E5=8A=A1=E5=99=A8=E7=BD=91=E7=BB=9C?= =?UTF-8?q?=E5=B1=82=EF=BC=88=E6=8F=A1=E6=89=8B/=E5=BF=83=E8=B7=B3/?= =?UTF-8?q?=E5=8D=8A=E5=8C=85=E5=A4=84=E7=90=86=EF=BC=8C=E5=90=AB=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/netproto/protocol.go | 319 +++++++++++++++++++++++++++ internal/netproto/protocol_test.go | 87 ++++++++ internal/netserver/netserver.go | 225 +++++++++++++++++++ internal/netserver/netserver_test.go | 126 +++++++++++ tools/w64devkit.zip | 1 + tools/zig.zip | 0 6 files changed, 758 insertions(+) create mode 100644 internal/netproto/protocol.go create mode 100644 internal/netproto/protocol_test.go create mode 100644 internal/netserver/netserver.go create mode 100644 internal/netserver/netserver_test.go create mode 100644 tools/w64devkit.zip create mode 100644 tools/zig.zip diff --git a/internal/netproto/protocol.go b/internal/netproto/protocol.go new file mode 100644 index 00000000..db935e1b --- /dev/null +++ b/internal/netproto/protocol.go @@ -0,0 +1,319 @@ +// Package netproto 网络协议 v1 编解码(网络协议.md §1–§3)。 +// +// 帧格式:长度 u32(大端)| 消息ID u16 | 压缩标志 u8 | payload; +// 整数:定长大端 / 变长 varint(LEB128);字符串:varint 长度 + UTF-8。 +package netproto + +import ( + "encoding/binary" + "errors" + "fmt" + "math" +) + +// 消息 ID(网络协议.md §3 消息目录)。 +const ( + MsgHandshake = 0x01 // C→S 握手 + MsgLoginRequest = 0x02 // C→S 登录 + MsgLoginResponse = 0x03 // S→C 登录结果 + MsgChunkData = 0x04 // S→C 区块数据 + MsgBlockChange = 0x05 // S→C 方块变更 + MsgPlayerMove = 0x06 // C→S 玩家移动 + MsgEntityMove = 0x07 // S→C 实体移动 + MsgChatMessage = 0x09 // 双向 聊天 + MsgInventorySync = 0x0A // 双向 背包 + MsgHeartbeat = 0x0E // 双向 心跳 + MsgDisconnect = 0x0F // 双向 断开 +) + +// 压缩标志(网络协议.md §1)。 +const ( + flagCompressed = 1 // payload 为 zstd 压缩块 +) + +// Frame 通用帧。 +type Frame struct { + MsgID uint16 + Flags uint8 + Payload []byte +} + +// MaxFrameSize 单帧上限 1 MiB(防御性,网络协议.md §1)。 +const MaxFrameSize = 1 << 20 + +// ErrIncomplete 半包错误:数据不足以组成完整帧(调用方应等待更多数据)。 +var ErrIncomplete = errors.New("netproto: 帧数据不完整") + +// EncodeFrame 序列化一帧为字节流。 +// 帧头 7 字节:长度 u32(= 消息ID 2 + 标志 1 + 载荷,不含长度字段本身)+ 消息ID u16 + 压缩标志 u8。 +func EncodeFrame(f Frame) []byte { + out := make([]byte, 7+len(f.Payload)) + binary.BigEndian.PutUint32(out[0:4], uint32(3+len(f.Payload))) + binary.BigEndian.PutUint16(out[4:6], f.MsgID) + out[6] = f.Flags + copy(out[7:], f.Payload) + return out +} + +// DecodeFrame 从字节流解析一帧,返回帧与剩余字节。 +// 数据不足时返回 ErrIncomplete(可用 errors.Is 判定半包,等待更多数据)。 +func DecodeFrame(data []byte) (Frame, []byte, error) { + if len(data) < 4 { + return Frame{}, nil, fmt.Errorf("%w(帧头不足 %d 字节)", ErrIncomplete, len(data)) + } + length := binary.BigEndian.Uint32(data[0:4]) + if length > MaxFrameSize { + return Frame{}, nil, fmt.Errorf("netproto: 帧长度 %d 超上限", length) + } + if int(length)+4 > len(data) { + return Frame{}, nil, fmt.Errorf("%w(需 %d 字节,剩 %d)", ErrIncomplete, length+4, len(data)) + } + f := Frame{ + MsgID: binary.BigEndian.Uint16(data[4:6]), + Flags: data[6], + Payload: data[7 : 4+int(length)], + } + return f, data[4+int(length):], nil +} + +// ---- 基础读写器(网络协议.md §2 序列化规则)---- + +// Writer 字节写入器。 +type Writer struct{ B []byte } + +// U8 写无符号 8 位。 +func (w *Writer) U8(v uint8) { w.B = append(w.B, v) } + +// U16 写无符号 16 位(大端)。 +func (w *Writer) U16(v uint16) { w.B = binary.BigEndian.AppendUint16(w.B, v) } + +// U32 写无符号 32 位(大端)。 +func (w *Writer) U32(v uint32) { w.B = binary.BigEndian.AppendUint32(w.B, v) } + +// I32 写有符号 32 位(大端)。 +func (w *Writer) I32(v int32) { w.U32(uint32(v)) } + +// F32 写 float32(位模式)。 +func (w *Writer) F32(v float32) { w.U32(math.Float32bits(v)) } + +// Varint 写变长整数(LEB128,无符号)。 +func (w *Writer) Varint(v uint32) { + for v >= 0x80 { + w.B = append(w.B, byte(v)|0x80) + v >>= 7 + } + w.B = append(w.B, byte(v)) +} + +// Str 写字符串(varint 长度 + UTF-8)。 +func (w *Writer) Str(s string) { + w.Varint(uint32(len(s))) + w.B = append(w.B, s...) +} + +// Bytes 写字节串(varint 长度 + 数据)。 +func (w *Writer) Bytes(b []byte) { + w.Varint(uint32(len(b))) + w.B = append(w.B, b...) +} + +// Reader 字节读取器。 +type Reader struct { + B []byte + Off int + Err error +} + +// NewReader 创建读取器。 +func NewReader(b []byte) *Reader { return &Reader{B: b} } + +// U8 读无符号 8 位。 +func (r *Reader) U8() uint8 { + if r.Err != nil { + return 0 + } + if r.Off+1 > len(r.B) { + r.Err = fmt.Errorf("netproto: 读取越界") + return 0 + } + v := r.B[r.Off] + r.Off++ + return v +} + +// U16 读无符号 16 位(大端)。 +func (r *Reader) U16() uint16 { + if r.Err != nil { + return 0 + } + if r.Off+2 > len(r.B) { + r.Err = fmt.Errorf("netproto: 读取越界") + return 0 + } + v := binary.BigEndian.Uint16(r.B[r.Off:]) + r.Off += 2 + return v +} + +// U32 读无符号 32 位(大端)。 +func (r *Reader) U32() uint32 { + if r.Err != nil { + return 0 + } + if r.Off+4 > len(r.B) { + r.Err = fmt.Errorf("netproto: 读取越界") + return 0 + } + v := binary.BigEndian.Uint32(r.B[r.Off:]) + r.Off += 4 + return v +} + +// I32 读有符号 32 位。 +func (r *Reader) I32() int32 { return int32(r.U32()) } + +// F32 读 float32。 +func (r *Reader) F32() float32 { return math.Float32frombits(r.U32()) } + +// Varint 读变长整数(LEB128)。 +func (r *Reader) Varint() uint32 { + if r.Err != nil { + return 0 + } + var v uint32 + for shift := 0; shift < 35; shift += 7 { + if r.Off >= len(r.B) { + r.Err = fmt.Errorf("netproto: varint 越界") + return 0 + } + b := r.B[r.Off] + r.Off++ + v |= uint32(b&0x7F) << shift + if b&0x80 == 0 { + return v + } + } + r.Err = fmt.Errorf("netproto: varint 超长") + return 0 +} + +// Str 读字符串(varint 长度 + UTF-8)。 +func (r *Reader) Str() string { + n := r.Varint() + if r.Err != nil { + return "" + } + if n > MaxFrameSize || r.Off+int(n) > len(r.B) { + r.Err = fmt.Errorf("netproto: 字符串越界(长度 %d)", n) + return "" + } + s := string(r.B[r.Off : r.Off+int(n)]) + r.Off += int(n) + return s +} + +// Bytes 读字节串。 +func (r *Reader) Bytes() []byte { + n := r.Varint() + if r.Err != nil { + return nil + } + if n > MaxFrameSize || r.Off+int(n) > len(r.B) { + r.Err = fmt.Errorf("netproto: 字节串越界(长度 %d)", n) + return nil + } + b := r.B[r.Off : r.Off+int(n)] + r.Off += int(n) + return b +} + +// ---- 常用消息载荷(网络协议.md §3)---- + +// BlockChange 方块变更(0x05)。 +type BlockChange struct { + X, Y, Z int32 + BlockID uint16 + Meta uint8 +} + +// Encode 编码方块变更。 +func (m BlockChange) Encode() []byte { + w := &Writer{} + w.I32(m.X) + w.I32(m.Y) + w.I32(m.Z) + w.U16(m.BlockID) + w.U8(m.Meta) + return w.B +} + +// DecodeBlockChange 解码方块变更。 +func DecodeBlockChange(b []byte) (BlockChange, error) { + r := NewReader(b) + m := BlockChange{ + X: r.I32(), + Y: r.I32(), + Z: r.I32(), + BlockID: r.U16(), + Meta: r.U8(), + } + return m, r.Err +} + +// PlayerMove 玩家移动(0x06,20Hz)。 +type PlayerMove struct { + X, Y, Z float32 + Yaw, Pitch float32 + OnGround bool +} + +// Encode 编码玩家移动。 +func (m PlayerMove) Encode() []byte { + w := &Writer{} + w.F32(m.X) + w.F32(m.Y) + w.F32(m.Z) + w.F32(m.Yaw) + w.F32(m.Pitch) + if m.OnGround { + w.U8(1) + } else { + w.U8(0) + } + return w.B +} + +// DecodePlayerMove 解码玩家移动。 +func DecodePlayerMove(b []byte) (PlayerMove, error) { + r := NewReader(b) + m := PlayerMove{ + X: r.F32(), + Y: r.F32(), + Z: r.F32(), + Yaw: r.F32(), + Pitch: r.F32(), + } + m.OnGround = r.U8() != 0 + return m, r.Err +} + +// Handshake 握手(0x01)。 +type Handshake struct { + ProtocolVersion int + ClientID string +} + +// Encode 编码握手。 +func (m Handshake) Encode() []byte { + w := &Writer{} + w.Varint(uint32(m.ProtocolVersion)) + w.Str(m.ClientID) + return w.B +} + +// DecodeHandshake 解码握手。 +func DecodeHandshake(b []byte) (Handshake, error) { + r := NewReader(b) + m := Handshake{ProtocolVersion: int(r.Varint()), ClientID: r.Str()} + return m, r.Err +} diff --git a/internal/netproto/protocol_test.go b/internal/netproto/protocol_test.go new file mode 100644 index 00000000..fc1aec1a --- /dev/null +++ b/internal/netproto/protocol_test.go @@ -0,0 +1,87 @@ +package netproto + +import ( + "bytes" + "math" + "testing" +) + +// TestVarint 变长整数边界(网络协议.md §2)。 +func TestVarint(t *testing.T) { + cases := []uint32{0, 1, 127, 128, 300, 16383, 16384, 1<<21 - 1, math.MaxUint32} + for _, want := range cases { + w := &Writer{} + w.Varint(want) + r := NewReader(w.B) + if got := r.Varint(); got != want || r.Err != nil { + t.Fatalf("varint %d 往返失败: got=%d err=%v", want, got, r.Err) + } + } +} + +// TestFrame 帧编解码往返(网络协议.md §1)。 +func TestFrame(t *testing.T) { + f := Frame{MsgID: MsgBlockChange, Flags: 0, Payload: BlockChange{X: 1, Y: 64, Z: -2, BlockID: 5, Meta: 0}.Encode()} + enc := EncodeFrame(f) + got, rest, err := DecodeFrame(enc) + if err != nil { + t.Fatalf("解码失败: %v", err) + } + if len(rest) != 0 { + t.Fatalf("应无剩余字节,实际 %d", len(rest)) + } + if got.MsgID != f.MsgID || !bytes.Equal(got.Payload, f.Payload) { + t.Fatalf("帧往返不一致: %+v", got) + } + // 流式粘包:两帧连续 + enc2 := EncodeFrame(Frame{MsgID: MsgHeartbeat, Payload: []byte{9, 9}}) + stream := append(append([]byte{}, enc...), enc2...) + g1, rest, err := DecodeFrame(stream) + if err != nil || g1.MsgID != MsgBlockChange { + t.Fatalf("粘包第一帧解析失败: %v", err) + } + g2, rest2, err := DecodeFrame(rest) + if err != nil || g2.MsgID != MsgHeartbeat || len(rest2) != 0 { + t.Fatalf("粘包第二帧解析失败: %v", err) + } +} + +// TestBlockChange 方块变更消息往返。 +func TestBlockChangeMsg(t *testing.T) { + in := BlockChange{X: -17, Y: 63, Z: 42, BlockID: 7, Meta: 3} + out, err := DecodeBlockChange(in.Encode()) + if err != nil { + t.Fatalf("解码失败: %v", err) + } + if out != in { + t.Fatalf("往返不一致: %+v != %+v", out, in) + } +} + +// TestPlayerMove 玩家移动消息往返。 +func TestPlayerMoveMsg(t *testing.T) { + in := PlayerMove{X: 8.5, Y: 64.0, Z: -3.25, Yaw: 1.57, Pitch: -0.5, OnGround: true} + out, err := DecodePlayerMove(in.Encode()) + if err != nil { + t.Fatalf("解码失败: %v", err) + } + if out != in { + t.Fatalf("往返不一致: %+v != %+v", out, in) + } +} + +// TestTruncated 截断数据必须报错(防崩溃,网络协议.md §8)。 +func TestTruncated(t *testing.T) { + f := Frame{MsgID: MsgHandshake, Payload: []byte{1, 2, 3, 4}} + enc := EncodeFrame(f) + if _, _, err := DecodeFrame(enc[:len(enc)-2]); err == nil { + t.Fatal("截断帧应报错") + } + // 超长帧头应报错 + bad := make([]byte, 8) + bad[0] = 0xFF + bad[1] = 0xFF + if _, _, err := DecodeFrame(bad); err == nil { + t.Fatal("超长帧头应报错") + } +} diff --git a/internal/netserver/netserver.go b/internal/netserver/netserver.go new file mode 100644 index 00000000..24763c55 --- /dev/null +++ b/internal/netserver/netserver.go @@ -0,0 +1,225 @@ +// Package netserver 服务器网络层:TCP 监听、连接会话、握手与心跳(多人同步.md、网络协议.md §4–§7)。 +// +// 线程模型(架构.md §4):每连接 1 读 + 1 写 goroutine;业务回调在主逻辑线程执行(通过 +// 事件 channel 投递),tick 内不做 IO。 +package netserver + +import ( + "context" + "errors" + "fmt" + "io" + "net" + "sync" + "sync/atomic" + "time" + + "mc/internal/logx" + "mc/internal/netproto" +) + +// 连接超时与心跳参数(网络协议.md §7)。 +const ( + readTimeout = 30 * time.Second // 无响应判定断线 + heartbeatIn = 5 * time.Second // 心跳间隔 + maxFrameSize = netproto.MaxFrameSize +) + +// Handler 服务器业务回调(由主逻辑线程实现)。 +type Handler interface { + // OnConnect 连接建立(握手前)。 + OnConnect(c *Conn) + // OnHandshake 收到握手:返回登录结果(0 成功,其他为拒绝原因码)。 + OnHandshake(c *Conn, hs netproto.Handshake) uint8 + // OnMove 玩家移动(20Hz)。 + OnMove(c *Conn, m netproto.PlayerMove) + // OnDisconnect 连接断开。 + OnDisconnect(c *Conn) +} + +// Server TCP 服务器。 +type Server struct { + addr string + log *logx.Logger + handler Handler + + ln net.Listener + mu sync.Mutex + conns map[uint64]*Conn + + nextID atomic.Uint64 +} + +// New 创建服务器(未监听)。 +func New(addr string, log *logx.Logger, h Handler) *Server { + return &Server{addr: addr, log: log, handler: h, conns: make(map[uint64]*Conn)} +} + +// Listen 绑定端口并启动接受循环(ctx 取消即关闭)。 +func (s *Server) Listen(ctx context.Context) error { + ln, err := net.Listen("tcp", s.addr) + if err != nil { + return fmt.Errorf("netserver.Listen %s: %w", s.addr, err) + } + s.ln = ln + s.log.Infof("网络监听于 %s", s.addr) + go s.acceptLoop(ctx) + return nil +} + +// Addr 返回监听地址。 +func (s *Server) Addr() string { return s.ln.Addr().String() } + +// acceptLoop 接受连接循环。 +func (s *Server) acceptLoop(ctx context.Context) { + go func() { + <-ctx.Done() + s.Close() + }() + for { + nc, err := s.ln.Accept() + if err != nil { + return // 关闭监听即退出 + } + c := newConn(s.nextID.Add(1), nc, s) + s.mu.Lock() + s.conns[c.id] = c + s.mu.Unlock() + go c.readLoop() + go c.writeLoop() + } +} + +// Close 关闭监听与全部连接(优雅退出)。 +func (s *Server) Close() { + if s.ln != nil { + _ = s.ln.Close() + } + s.mu.Lock() + conns := make([]*Conn, 0, len(s.conns)) + for _, c := range s.conns { + conns = append(conns, c) + } + s.mu.Unlock() + for _, c := range conns { + c.close() + } +} + +// Conn 一个客户端连接。 +type Conn struct { + id uint64 + conn net.Conn + srv *Server + send chan []byte + closeOnce sync.Once +} + +// newConn 创建连接。 +func newConn(id uint64, nc net.Conn, s *Server) *Conn { + return &Conn{id: id, conn: nc, srv: s, send: make(chan []byte, 64)} +} + +// ID 连接 ID(服务器分配,多人同步.md §3)。 +func (c *Conn) ID() uint64 { return c.id } + +// readLoop 读循环:解帧并分发(网络协议.md §1 帧格式)。 +// 半包(ErrIncomplete)时保留缓冲区等待更多数据;协议错误直接断开。 +func (c *Conn) readLoop() { + defer c.srv.handler.OnDisconnect(c) + buf := make([]byte, 0, 64*1024) + tmp := make([]byte, 64*1024) + for { + _ = c.conn.SetReadDeadline(time.Now().Add(readTimeout)) + n, err := c.conn.Read(tmp) + if err != nil { + return // 超时/断开:结束连接 + } + buf = append(buf, tmp[:n]...) + c.srv.log.Debugf("连接 %d 读到 %d 字节(缓冲 %d)", c.id, n, len(buf)) + if len(buf) > 2*maxFrameSize { + c.srv.log.Warnf("连接 %d 缓冲区超限,断开", c.id) + c.close() + return + } + for len(buf) >= 4 { + f, rest, err := netproto.DecodeFrame(buf) + if err != nil { + if errors.Is(err, netproto.ErrIncomplete) { + break // 半包:保留 buf,等待更多数据 + } + c.srv.log.Warnf("连接 %d 帧解析失败: %v,断开", c.id, err) + c.close() + return + } + buf = rest + c.handle(f) + } + } +} + +// handle 消息分发(读 goroutine → 业务回调,业务侧自行保证线程安全)。 +func (c *Conn) handle(f netproto.Frame) { + switch f.MsgID { + case netproto.MsgHandshake: + hs, err := netproto.DecodeHandshake(f.Payload) + if err != nil { + c.close() + return + } + code := c.srv.handler.OnHandshake(c, hs) + c.Send(netproto.Frame{MsgID: netproto.MsgLoginResponse, Payload: []byte{code}}) + if code != 0 { + c.close() + } + case netproto.MsgPlayerMove: + m, err := netproto.DecodePlayerMove(f.Payload) + if err == nil { + c.srv.handler.OnMove(c, m) + } + case netproto.MsgHeartbeat: + c.Send(netproto.Frame{MsgID: netproto.MsgHeartbeat, Payload: f.Payload}) // 回显心跳 + case netproto.MsgDisconnect: + c.close() + } +} + +// writeLoop 写循环:channel 驱动,写超时断开(网络协议.md §7)。 +func (c *Conn) writeLoop() { + for data := range c.send { + _ = c.conn.SetWriteDeadline(time.Now().Add(readTimeout)) + if _, err := c.conn.Write(data); err != nil { + c.close() + return + } + } +} + +// Send 发送一帧(非阻塞入队;队满丢弃并告警,防慢连接阻塞服务器)。 +func (c *Conn) Send(f netproto.Frame) { + select { + case c.send <- netproto.EncodeFrame(f): + default: + c.srv.log.Warnf("连接 %d 发送队列已满,丢弃帧 0x%02X", c.id, f.MsgID) + } +} + +// close 幂等关闭。 +func (c *Conn) close() { + c.closeOnce.Do(func() { + _ = c.conn.Close() + close(c.send) + c.srv.mu.Lock() + delete(c.srv.conns, c.id) + c.srv.mu.Unlock() + }) +} + +// Count 当前连接数(测试/统计)。 +func (s *Server) Count() int { + s.mu.Lock() + defer s.mu.Unlock() + return len(s.conns) +} + +var _ = io.EOF // 保持 io 引用(后续扩展流式读取) diff --git a/internal/netserver/netserver_test.go b/internal/netserver/netserver_test.go new file mode 100644 index 00000000..3f46a53e --- /dev/null +++ b/internal/netserver/netserver_test.go @@ -0,0 +1,126 @@ +package netserver + +import ( + "context" + "net" + "testing" + "time" + + "mc/internal/logx" + "mc/internal/netproto" +) + +// testHandler 记录回调的测试处理器。 +type testHandler struct { + logins chan netproto.Handshake + moves chan netproto.PlayerMove + connects chan uint64 +} + +func newTestHandler() *testHandler { + return &testHandler{ + logins: make(chan netproto.Handshake, 8), + moves: make(chan netproto.PlayerMove, 8), + connects: make(chan uint64, 8), + } +} + +func (h *testHandler) OnConnect(c *Conn) { h.connects <- c.ID() } +func (h *testHandler) OnHandshake(c *Conn, hs netproto.Handshake) uint8 { + h.logins <- hs + return 0 +} +func (h *testHandler) OnMove(c *Conn, m netproto.PlayerMove) { h.moves <- m } +func (h *testHandler) OnDisconnect(c *Conn) {} + +// startServer 启动测试服务器。 +func startServer(t *testing.T) (*Server, *testHandler, context.CancelFunc) { + t.Helper() + log, err := logx.New("", logx.LevelDebug) // 仅 stderr(避免临时文件句柄) + if err != nil { + t.Fatalf("创建日志失败: %v", err) + } + h := newTestHandler() + s := New("127.0.0.1:0", log, h) + ctx, cancel := context.WithCancel(context.Background()) + if err := s.Listen(ctx); err != nil { + t.Fatalf("监听失败: %v", err) + } + t.Cleanup(func() { + cancel() + time.Sleep(20 * time.Millisecond) + }) + return s, h, cancel +} + +// dial 建立客户端连接。 +func dial(t *testing.T, addr string) net.Conn { + t.Helper() + c, err := net.DialTimeout("tcp", addr, 3*time.Second) + if err != nil { + t.Fatalf("拨号失败: %v", err) + } + t.Cleanup(func() { _ = c.Close() }) + return c +} + +// TestHandshakeFlow 握手 → 登录响应 → 心跳回显全链路(网络协议.md §4)。 +func TestHandshakeFlow(t *testing.T) { + s, h, _ := startServer(t) + nc := dial(t, s.Addr()) + + // 发送握手 + hs := netproto.Handshake{ProtocolVersion: 1, ClientID: "测试客户端"} + _, err := nc.Write(netproto.EncodeFrame(netproto.Frame{MsgID: netproto.MsgHandshake, Payload: hs.Encode()})) + if err != nil { + t.Fatalf("发送握手失败: %v", err) + } + select { + case got := <-h.logins: + if got != hs { + t.Fatalf("握手内容不一致: %+v", got) + } + case <-time.After(3 * time.Second): + t.Fatal("未收到握手回调") + } + + // 读登录响应帧 + _ = nc.SetReadDeadline(time.Now().Add(3 * time.Second)) + buf := make([]byte, 256) + n, err := nc.Read(buf) + if err != nil { + t.Fatalf("读登录响应失败: %v", err) + } + f, _, err := netproto.DecodeFrame(buf[:n]) + if err != nil || f.MsgID != netproto.MsgLoginResponse { + t.Fatalf("登录响应异常: %+v err=%v", f, err) + } + if len(f.Payload) != 1 || f.Payload[0] != 0 { + t.Fatalf("登录结果应为 0(成功),实际 %v", f.Payload) + } + + // 心跳回显 + _, _ = nc.Write(netproto.EncodeFrame(netproto.Frame{MsgID: netproto.MsgHeartbeat, Payload: []byte{7}})) + n, _ = nc.Read(buf) + f, _, _ = netproto.DecodeFrame(buf[:n]) + if f.MsgID != netproto.MsgHeartbeat || len(f.Payload) != 1 || f.Payload[0] != 7 { + t.Fatalf("心跳回显异常: %+v", f) + } +} + +// TestMoveBroadcast 移动消息分发。 +func TestMoveBroadcast(t *testing.T) { + s, h, _ := startServer(t) + nc := dial(t, s.Addr()) + m := netproto.PlayerMove{X: 1.5, Y: 64, Z: -2, Yaw: 0.5, Pitch: 0, OnGround: true} + _, _ = nc.Write(netproto.EncodeFrame(netproto.Frame{MsgID: netproto.MsgPlayerMove, Payload: m.Encode()})) + select { + case got := <-h.moves: + if got != m { + t.Fatalf("移动数据不一致: %+v", got) + } + case <-time.After(3 * time.Second): + t.Fatal("未收到移动回调") + } + _ = s.Count() // 连接计数可用 +} diff --git a/tools/w64devkit.zip b/tools/w64devkit.zip new file mode 100644 index 00000000..85373076 --- /dev/null +++ b/tools/w64devkit.zip @@ -0,0 +1 @@ +Not Found \ No newline at end of file diff --git a/tools/zig.zip b/tools/zig.zip new file mode 100644 index 00000000..e69de29b