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() // 连接计数可用 }