feat(net): 协议编解码(帧/varint/消息)与服务器网络层(握手/心跳/半包处理,含测试)
This commit is contained in:
126
internal/netserver/netserver_test.go
Normal file
126
internal/netserver/netserver_test.go
Normal file
@@ -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() // 连接计数可用
|
||||
}
|
||||
Reference in New Issue
Block a user