From b478482f210f33351c037105ad2e73d46f3a399a Mon Sep 17 00:00:00 2001 From: 4566704 <4566704@qq.com> Date: Sun, 20 Sep 2026 12:25:35 +0800 Subject: [PATCH] =?UTF-8?q?feat(proto):=20=E8=87=AA=20go-hua=20=E8=BF=81?= =?UTF-8?q?=E5=85=A5=E9=95=BF=E5=BA=A6=E5=89=8D=E7=BC=80=E5=B0=81=E5=8C=85?= =?UTF-8?q?=E5=8D=8F=E8=AE=AE=E7=BC=96=E8=A7=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 8 字节头(4 字节 ID/校验 + 4 字节长度, 小端序) + CRC32 校验版本 - 附测试、examples/proto 例程与包 README(含 Decode reader 复用告诫) --- examples/proto/main.go | 49 ++++++++++ proto/README.md | 40 ++++++++ proto/proto.go | 213 +++++++++++++++++++++++++++++++++++++++++ proto/proto_test.go | 90 +++++++++++++++++ 4 files changed, 392 insertions(+) create mode 100644 examples/proto/main.go create mode 100644 proto/README.md create mode 100644 proto/proto.go create mode 100644 proto/proto_test.go diff --git a/examples/proto/main.go b/examples/proto/main.go new file mode 100644 index 0000000..3de002b --- /dev/null +++ b/examples/proto/main.go @@ -0,0 +1,49 @@ +// proto 包示例:通用封包协议(消息头 8 字节 = ID 4 字节 + 长度 4 字节,小端序),支持 CRC32 校验版本 +package main + +import ( + "bufio" + "fmt" + "net" + + "git.zeroonesoft.cn/golib/zogo/proto" +) + +func main() { + client, server := net.Pipe() + defer client.Close() + defer server.Close() + + // ===== 普通封包:Encode / Decode ===== + go func() { + data, err := proto.Encode([]byte("hello proto")) + if err != nil { + fmt.Println("Encode:", err) + return + } + client.Write(data) + }() + + buf, err := proto.Decode(bufio.NewReader(server)) + fmt.Println("Decode:", string(buf), "错误:", err) + + // ===== CRC32 校验封包:EncodeCrc32 / DecodeCrc32 ===== + go func() { + data, err := proto.EncodeCrc32([]byte("hello crc32")) + if err != nil { + fmt.Println("EncodeCrc32:", err) + return + } + client.Write(data) + }() + + buf, err = proto.DecodeCrc32(bufio.NewReader(server)) + fmt.Println("DecodeCrc32:", string(buf), "错误:", err) + + // 实际 TCP 服务中的用法: + // reader := bufio.NewReader(conn) + // for { + // data, err := proto.Decode(reader) // 阻塞读到完整一帧 + // handle(data) + // } +} diff --git a/proto/README.md b/proto/README.md new file mode 100644 index 0000000..81cb6ad --- /dev/null +++ b/proto/README.md @@ -0,0 +1,40 @@ +# proto + +长度前缀封包协议的编解码:8 字节头(4 字节 ID/校验 + 4 字节长度,小端序), +支持 CRC32 校验版本,配合 `bufio.Reader` 使用。 + +> 迁移自 go-hua/proto,代码保持原样。 + +## 帧格式 + +``` +普通版: + id/流ID (4) + length (4) + data (length) + 小端序 +CRC32 版:+ id (4) + length (4) + crc32 (4) + data (length) + 小端序 +``` + +## 用法 + +```go +import ( + "bufio" + "net" + + "git.zeroonesoft.cn/golib/zogo/proto" +) + +// 发送端 +wire, _ := proto.Encode([]byte("payload")) // 另有 EncodeCrc32 +conn.Write(wire) + +// 接收端(reader 需为 *bufio.Reader,可跨帧复用) +r := bufio.NewReader(conn) +data, err := proto.Decode(r) // 另有 DecodeCrc32(校验失败返回错误) +``` + +完整可运行例程:[examples/proto/main.go](../examples/proto/main.go) + +## 注意 + +- `Decode` 会阻塞直到读满一帧;reader 请与连接同生命周期复用,勿每帧新建 + (缓冲中多读的字节会丢失——与本仓库 `frame` 包迁移前问题同理)。 +- CRC32 版本与非校验版本的头部布局不同,两端必须约定同一种。 diff --git a/proto/proto.go b/proto/proto.go new file mode 100644 index 0000000..6155dcc --- /dev/null +++ b/proto/proto.go @@ -0,0 +1,213 @@ +// Package proto 提供长度前缀封包协议的编解码(8 字节头:4 字节 ID/校验 + 4 字节长度,小端序),支持 CRC32 校验版本。 +package proto + +import ( + "bufio" + "bytes" + "encoding/binary" + "errors" + "hash/crc32" +) + +// PakHeadLen 消息头长度:普通版为 ID 4 字节 + 长度 4 字节;CRC32 版为校验值 4 字节 + 长度 4 字节 +const ( + PakHeadLen int32 = 8 +) + +// Encode 编码 +func Encode(buf []byte) ([]byte, error) { + // 读取消息的长度,转换成int32类型(占4个字节) + var id int32 = 5 + var length = int32(len(buf)) + var pkg = new(bytes.Buffer) + // 写入消息头 + err := binary.Write(pkg, binary.LittleEndian, id) + if err != nil { + return nil, err + } + err = binary.Write(pkg, binary.LittleEndian, length) + if err != nil { + return nil, err + } + // 写入消息实体 + err = binary.Write(pkg, binary.LittleEndian, []byte(buf)) + if err != nil { + return nil, err + } + return pkg.Bytes(), nil +} + +// Decode 解码 +func Decode(reader *bufio.Reader) ([]byte, error) { + // 读取头部 + lengthByte, err := reader.Peek(int(PakHeadLen)) // 读取前4个字节的数据 + if err != nil { + return nil, err + } + lengthBuff := bytes.NewBuffer(lengthByte) + var id int32 = 0 + var length int32 = 0 + + err = binary.Read(lengthBuff, binary.LittleEndian, &id) + if err != nil { + return nil, err + } + err = binary.Read(lengthBuff, binary.LittleEndian, &length) + if err != nil { + return nil, err + } + + if length <= 0 { + return nil, errors.New("decode length cannot be less than 0") + } + + if length > 0x1000000 { + return nil, errors.New("decode length cannot be greater than 0x1000000") + } + + var data []byte + + // 剩余读长长度 + unread := int(PakHeadLen + length) + readLen := 0 + for { + if unread > 0x10000 { + readLen = 0x10000 + } else { + readLen = unread + } + + _, err = reader.Peek(readLen) // 先窥探数据 + if err != nil { + return nil, err + } + + // Buffered返回缓冲中现有的可读取的字节数。 + if reader.Buffered() < readLen { + return nil, err + } + + // 读取 + buf := make([]byte, readLen) + _, err = reader.Read(buf) + if err != nil { + return nil, err + } + data = append(data, buf...) + + unread -= readLen + //fmt.Println("readLen", readLen) + if unread <= 0 { + break + } + + } + + if len(data) < int(PakHeadLen) { + return nil, nil + } + + return data[PakHeadLen:], nil +} + +// EncodeCrc32 编码 Crc32校验 +func EncodeCrc32(buf []byte) ([]byte, error) { + // 读取消息的长度,转换成int32类型(占4个字节) + ieee := crc32.NewIEEE() + ieee.Write(buf) + var crc = ieee.Sum32() + var length = int32(len(buf)) + var pkg = new(bytes.Buffer) + // 写入消息头 + err := binary.Write(pkg, binary.LittleEndian, crc) + if err != nil { + return nil, err + } + err = binary.Write(pkg, binary.LittleEndian, length) + if err != nil { + return nil, err + } + // 写入消息实体 + err = binary.Write(pkg, binary.LittleEndian, []byte(buf)) + if err != nil { + return nil, err + } + return pkg.Bytes(), nil +} + +// DecodeCrc32 解码 Crc32校验 +func DecodeCrc32(reader *bufio.Reader) ([]byte, error) { + // 读取头部 + lengthByte, err := reader.Peek(int(PakHeadLen)) // 读取前4个字节的数据 + if err != nil { + return nil, err + } + lengthBuff := bytes.NewBuffer(lengthByte) + var crc uint32 = 0 + var length int32 = 0 + + err = binary.Read(lengthBuff, binary.LittleEndian, &crc) + if err != nil { + return nil, err + } + err = binary.Read(lengthBuff, binary.LittleEndian, &length) + if err != nil { + return nil, err + } + + if length <= 0 { + return nil, errors.New("decode length cannot be less than 0") + } + + if length > 0x1000000 { + return nil, errors.New("decode length cannot be greater than 0x1000000") + } + + var data []byte + + // 剩余读长长度 + unread := int(PakHeadLen + length) + readLen := 0 + for { + if unread > 0x1000 { + readLen = 0x1000 + } else { + readLen = unread + } + + _, err = reader.Peek(readLen) // 先窥探数据 + if err != nil { + return nil, err + } + + // Buffered返回缓冲中现有的可读取的字节数。 + if reader.Buffered() < readLen { + return nil, err + } + + // 读取 + buf := make([]byte, readLen) + _, err = reader.Read(buf) + if err != nil { + return nil, err + } + data = append(data, buf...) + + unread -= readLen + //fmt.Println("readLen", readLen) + if unread <= 0 { + break + } + + } + + if len(data) < int(PakHeadLen) { + return nil, nil + } + ieee := crc32.NewIEEE() + ieee.Write(data[PakHeadLen:]) + if crc != ieee.Sum32() { + return nil, errors.New("crc check error") + } + return data[PakHeadLen:], nil +} diff --git a/proto/proto_test.go b/proto/proto_test.go new file mode 100644 index 0000000..8998e55 --- /dev/null +++ b/proto/proto_test.go @@ -0,0 +1,90 @@ +package proto + +import ( + "bufio" + "bytes" + "net" + "testing" +) + +func TestEncodeDecode(t *testing.T) { + client, server := net.Pipe() + defer client.Close() + defer server.Close() + + want := []byte("hello proto") + go func() { + data, err := Encode(want) + if err != nil { + t.Errorf("Encode: %v", err) + server.Close() + return + } + server.Write(data) + }() + + got, err := Decode(bufio.NewReader(client)) + if err != nil { + t.Fatalf("Decode: %v", err) + } + if !bytes.Equal(got, want) { + t.Errorf("Decode = %q, want %q", got, want) + } +} + +func TestEncodeDecodeCrc32(t *testing.T) { + client, server := net.Pipe() + defer client.Close() + defer server.Close() + + want := []byte("hello crc32") + go func() { + data, err := EncodeCrc32(want) + if err != nil { + t.Errorf("EncodeCrc32: %v", err) + server.Close() + return + } + server.Write(data) + }() + + got, err := DecodeCrc32(bufio.NewReader(client)) + if err != nil { + t.Fatalf("DecodeCrc32: %v", err) + } + if !bytes.Equal(got, want) { + t.Errorf("DecodeCrc32 = %q, want %q", got, want) + } +} + +func TestDecodeCrc32Corrupted(t *testing.T) { + client, server := net.Pipe() + defer client.Close() + defer server.Close() + + go func() { + data, _ := EncodeCrc32([]byte("payload")) + data[len(data)-1] ^= 0xFF // 篡改数据 + server.Write(data) + }() + + _, err := DecodeCrc32(bufio.NewReader(client)) + if err == nil { + t.Error("篡改数据后 DecodeCrc32 应返回错误") + } +} + +func TestEncodeHeaderFormat(t *testing.T) { + // 头 8 字节:[0:4] 为 CRC/ID,[4:8] 为小端长度 + data, err := EncodeCrc32([]byte("abcd")) + if err != nil { + t.Fatalf("EncodeCrc32: %v", err) + } + length := int(data[4]) | int(data[5])<<8 | int(data[6])<<16 | int(data[7])<<24 + if length != 4 { + t.Errorf("长度字段 = %d, want 4", length) + } + if len(data) != int(PakHeadLen)+4 { + t.Errorf("报文总长 = %d, want %d", len(data), PakHeadLen+4) + } +}