feat(net): 协议编解码(帧/varint/消息)与服务器网络层(握手/心跳/半包处理,含测试)
This commit is contained in:
319
internal/netproto/protocol.go
Normal file
319
internal/netproto/protocol.go
Normal file
@@ -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
|
||||
}
|
||||
87
internal/netproto/protocol_test.go
Normal file
87
internal/netproto/protocol_test.go
Normal file
@@ -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("超长帧头应报错")
|
||||
}
|
||||
}
|
||||
225
internal/netserver/netserver.go
Normal file
225
internal/netserver/netserver.go
Normal file
@@ -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 引用(后续扩展流式读取)
|
||||
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() // 连接计数可用
|
||||
}
|
||||
1
tools/w64devkit.zip
Normal file
1
tools/w64devkit.zip
Normal file
@@ -0,0 +1 @@
|
||||
Not Found
|
||||
0
tools/zig.zip
Normal file
0
tools/zig.zip
Normal file
Reference in New Issue
Block a user