You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
nnet/docs/kb/user-guide/PROTOCOL_FRAME_EXPLANATION.md

14 KiB

协议帧头字段概念详解

一、协议帧头的概念

1.1 什么是协议帧头?

协议帧头Frame Header是协议消息的元数据部分,通常包含:

  • 消息类型
  • 命令码
  • 版本号
  • 长度信息
  • 序列号
  • 校验和
  • 其他控制信息

1.2 协议帧的组成

一个完整的协议帧通常由两部分组成:

┌─────────────────────────────────────────┐
│         协议帧Protocol Frame          │
├─────────────────────────────────────────┤
│  帧头Frame Header                    │
│  - 消息类型、命令码、长度等元数据        │
├─────────────────────────────────────────┤
│  帧体Frame Body / Payload            │
│  - 实际业务数据                          │
└─────────────────────────────────────────┘

1.3 示例nnet协议格式

[Magic(2B)][Version(1B)][Type(1B)][Length(4B)][Payload(NB)][Checksum(2B)]
   ↑           ↑          ↑         ↑
   帧头部分 ────┴──────────┴─────────┘

在这个例子中:

  • 帧头字段Magic、Version、Type、Length
  • 帧体Payload实际数据
  • 帧尾Checksum

二、字段的来源

2.1 字段来源的三种情况

情况1解包后的Struct字段最常见

适用场景协议有明确的编解码器消息被解码为Go struct

// 1. 定义协议帧头结构
type MessageHeader struct {
    MessageType uint8   // 消息类型(帧头字段)
    CommandCode uint8   // 命令码(帧头字段)
    Version     uint8   // 版本号(帧头字段)
    Length      uint32  // 数据长度(帧头字段)
    Sequence    uint32  // 序列号(帧头字段)
}

type Message struct {
    Header MessageHeader  // 帧头
    Body   []byte         // 帧体Payload
}

// 2. 编解码器解码后消息变成struct
func (codec *MyCodec) Decode(data []byte) (*Message, error) {
    msg := &Message{}
    // 解析帧头
    msg.Header.MessageType = data[0]
    msg.Header.CommandCode = data[1]
    msg.Header.Version = data[2]
    msg.Header.Length = binary.BigEndian.Uint32(data[3:7])
    // ...
    return msg, nil
}

// 3. 路由匹配时从解码后的struct中提取字段
router.RegisterFrameHeader(
    "MessageType",  // field: struct字段名
    "==",
    0x01,
    handler,
)

工作流程

原始字节流 → 粘包拆包 → 解码器解码 → 变成struct → 提取struct字段 → 匹配

情况2原始字节流中的固定位置

适用场景:协议简单,不需要完整解码,直接从字节流中读取

// 协议格式: [消息类型(1B)][命令码(1B)][数据长度(4B)][数据(NB)]
//            ↑ offset=0    ↑ offset=1  ↑ offset=2

// 定义字段位置映射
type FieldMapping struct {
    "message_type": {Offset: 0, Size: 1},  // 第0字节1字节
    "command_code": {Offset: 1, Size: 1},  // 第1字节1字节
    "length":       {Offset: 2, Size: 4},  // 第2字节4字节
}

// 路由匹配时,直接从原始字节流中提取
router.RegisterFrameHeader(
    "message_type",  // field: 字段名对应FieldMapping中的key
    "==",
    0x01,
    handler,
)

工作流程

原始字节流 → 根据字段位置映射提取 → 匹配(无需完整解码)

情况3通过协议特定的提取器

适用场景:协议复杂,需要自定义提取逻辑

// 定义字段提取器
type FieldExtractor interface {
    ExtractField(data []byte, fieldName string) (interface{}, error)
}

// 实现提取器
type MyProtocolExtractor struct {
    // 字段定义
    fields map[string]FieldDef
}

func (e *MyProtocolExtractor) ExtractField(data []byte, fieldName string) (interface{}, error) {
    def, ok := e.fields[fieldName]
    if !ok {
        return nil, fmt.Errorf("field %s not found", fieldName)
    }
    
    // 根据字段定义提取值
    switch def.Type {
    case "uint8":
        return data[def.Offset], nil
    case "uint32":
        return binary.BigEndian.Uint32(data[def.Offset:def.Offset+4]), nil
    // ...
    }
}

// 注册提取器到协议
protocol.RegisterFieldExtractor(extractor)

三、nnet中的实现方式

3.1 字段提取的层次

在nnet中字段提取支持多个层次按优先级从高到低

1. 解码后的Struct字段如果协议有编解码器
   ↓ 如果没有
2. 协议定义的字段映射FieldMapping
   ↓ 如果还没有
3. 自定义字段提取器FieldExtractor

3.2 实现示例

方式1基于Struct字段推荐

package main

import (
    "github.com/noahlann/nnet/pkg/nnet"
    "github.com/noahlann/nnet/pkg/protocol"
)

// 定义协议帧头结构
type GameMessageHeader struct {
    MessageType uint8   `nnet:"message_type"`  // 标签指定字段名
    CommandCode uint8   `nnet:"command_code"`
    Version     uint8   `nnet:"version"`
    Length      uint32  `nnet:"length"`
}

type GameMessage struct {
    Header GameMessageHeader
    Body   []byte
}

// 注册协议和编解码器
func init() {
    protocol.Register("game", &GameProtocol{
        codec: &GameCodec{},
    })
}

// 编解码器实现
type GameCodec struct{}

func (c *GameCodec) Decode(data []byte) (interface{}, error) {
    msg := &GameMessage{}
    // 解析帧头
    msg.Header.MessageType = data[0]
    msg.Header.CommandCode = data[1]
    msg.Header.Version = data[2]
    msg.Header.Length = binary.BigEndian.Uint32(data[3:7])
    msg.Body = data[7:]
    return msg, nil
}

func main() {
    server := nnet.NewServer(&nnet.Config{
        Addr:     "tcp://:8080",
        Protocol: "game",
    })

    // 使用struct字段名进行匹配
    // nnet会自动从解码后的struct中提取字段值
    server.Router().RegisterFrameHeader(
        "message_type",  // 对应 GameMessageHeader.MessageType
        "==",
        0x01,
        loginHandler,
    )
    
    server.Start()
}

方式2基于字段映射无需完整解码

package main

import (
    "github.com/noahlann/nnet/pkg/nnet"
    "github.com/noahlann/nnet/pkg/protocol"
)

// 定义字段映射
var SimpleProtocolFields = map[string]protocol.FieldDef{
    "message_type": {
        Offset: 0,
        Size:   1,
        Type:   "uint8",
    },
    "command_code": {
        Offset: 1,
        Size:   1,
        Type:   "uint8",
    },
    "length": {
        Offset: 2,
        Size:   4,
        Type:   "uint32",
    },
}

// 注册协议(无编解码器,使用字段映射)
func init() {
    protocol.Register("simple", &SimpleProtocol{
        fields: SimpleProtocolFields,
    })
}

func main() {
    server := nnet.NewServer(&nnet.Config{
        Addr:     "tcp://:8080",
        Protocol: "simple",
    })

    // 使用字段映射中的字段名
    // nnet会直接从原始字节流的固定位置提取值
    server.Router().RegisterFrameHeader(
        "message_type",  // 对应字段映射中的key
        "==",
        0x01,
        handler,
    )
    
    server.Start()
}

方式3自定义字段提取器

package main

import (
    "github.com/noahlann/nnet/pkg/nnet"
    "github.com/noahlann/nnet/pkg/protocol"
)

// 自定义字段提取器
type CustomFieldExtractor struct {
    // 字段定义
    fields map[string]FieldDef
}

func (e *CustomFieldExtractor) ExtractField(data []byte, fieldName string) (interface{}, error) {
    def, ok := e.fields[fieldName]
    if !ok {
        return nil, fmt.Errorf("field %s not found", fieldName)
    }
    
    // 自定义提取逻辑
    if def.Type == "uint8" {
        return data[def.Offset], nil
    } else if def.Type == "uint32" {
        return binary.BigEndian.Uint32(data[def.Offset:def.Offset+4]), nil
    }
    // ...
    return nil, fmt.Errorf("unsupported type: %s", def.Type)
}

// 注册协议和提取器
func init() {
    extractor := &CustomFieldExtractor{
        fields: map[string]FieldDef{
            "message_type": {Offset: 0, Size: 1, Type: "uint8"},
            "command_code": {Offset: 1, Size: 1, Type: "uint8"},
        },
    }
    
    protocol.Register("custom", &CustomProtocol{
        extractor: extractor,
    })
}

func main() {
    server := nnet.NewServer(&nnet.Config{
        Addr:     "tcp://:8080",
        Protocol: "custom",
    })

    // 使用自定义提取器提取字段
    server.Router().RegisterFrameHeader(
        "message_type",
        "==",
        0x01,
        handler,
    )
    
    server.Start()
}

四、字段提取的时机

4.1 数据流中的位置

接收原始字节流
    ↓
[粘包拆包] → 得到完整消息帧
    ↓
[字段提取] ← 这里提取帧头字段用于路由匹配
    ↓
[协议识别/版本识别]
    ↓
[完整解码] → 解码为struct如果需要
    ↓
[路由匹配] → 使用提取的字段值进行匹配
    ↓
[业务处理]

4.2 为什么要在解码前提取?

性能考虑

  1. 避免不必要的解码:如果路由匹配失败,就不需要完整解码
  2. 快速路由:直接从字节流中读取几个字节比完整解码快得多
  3. 内存效率不需要为所有消息创建struct对象

示例

// 场景收到1000条消息只有10条需要处理
// 方式1先解码再匹配
for each message {
    struct := decode(message)  // 1000次解码
    if match(struct) {         // 1000次匹配
        handle(struct)
    }
}

// 方式2先提取字段匹配再解码
for each message {
    field := extractField(message, "message_type")  // 只提取1字节
    if match(field) {                                // 快速匹配
        struct := decode(message)  // 只解码10次
        handle(struct)
    }
}

五、实际应用示例

5.1 游戏协议示例

// 协议格式:[消息类型(1B)][命令码(1B)][用户ID(4B)][数据长度(4B)][数据(NB)]

// 方式1定义struct推荐用于复杂协议
type GameMessageHeader struct {
    MessageType uint8   `nnet:"message_type"`
    CommandCode uint8   `nnet:"command_code"`
    UserID      uint32  `nnet:"user_id"`
    DataLength  uint32  `nnet:"data_length"`
}

// 路由匹配
router.RegisterFrameHeader("message_type", "==", 0x01, loginHandler)
router.RegisterFrameHeader("command_code", "==", 0x10, getUserInfoHandler)

// 方式2字段映射推荐用于简单协议
var GameProtocolFields = map[string]protocol.FieldDef{
    "message_type": {Offset: 0, Size: 1, Type: "uint8"},
    "command_code": {Offset: 1, Size: 1, Type: "uint8"},
    "user_id":      {Offset: 2, Size: 4, Type: "uint32"},
}

// 路由匹配(相同的方式)
router.RegisterFrameHeader("message_type", "==", 0x01, loginHandler)

5.2 物联网协议示例

// 协议格式:[设备类型(1B)][设备ID(4B)][命令(1B)][数据长度(2B)][数据(NB)]

type IoTMessageHeader struct {
    DeviceType uint8   `nnet:"device_type"`
    DeviceID   uint32  `nnet:"device_id"`
    Command    uint8   `nnet:"command"`
    DataLength uint16  `nnet:"data_length"`
}

// 根据设备类型路由
router.RegisterFrameHeader("device_type", "==", 0x01, sensorHandler)    // 传感器
router.RegisterFrameHeader("device_type", "==", 0x02, actuatorHandler)   // 执行器

// 根据设备ID范围路由
router.RegisterFrameHeader("device_id", ">=", 1000, vipDeviceHandler)    // VIP设备

六、字段名映射规则

6.1 Struct字段名映射

type MessageHeader struct {
    MessageType uint8  // Go字段名MessageType
    // 映射为message_type自动转换为小写+下划线)
}

// 路由中使用
router.RegisterFrameHeader("message_type", "==", 0x01, handler)
// 或
router.RegisterFrameHeader("MessageType", "==", 0x01, handler)  // 也支持

6.2 使用标签指定字段名

type MessageHeader struct {
    MessageType uint8  `nnet:"msg_type"`   // 指定为 msg_type
    CommandCode uint8  `nnet:"cmd"`        // 指定为 cmd
}

// 路由中使用标签指定的名称
router.RegisterFrameHeader("msg_type", "==", 0x01, handler)
router.RegisterFrameHeader("cmd", "==", 0x10, handler)

6.3 字段映射中的key

var ProtocolFields = map[string]protocol.FieldDef{
    "message_type": {Offset: 0, Size: 1},  // key就是字段名
    "command_code":    {Offset: 1, Size: 1},
}

// 路由中使用map的key
router.RegisterFrameHeader("message_type", "==", 0x01, handler)

七、总结

7.1 字段来源的三种方式

  1. Struct字段(最常见):

    • 协议有编解码器
    • 消息被解码为Go struct
    • 字段来自struct的字段名或标签
  2. 字段映射(简单协议):

    • 协议简单,无需完整解码
    • 字段来自原始字节流的固定位置
    • 通过FieldMapping定义字段位置
  3. 自定义提取器(复杂场景):

    • 需要特殊提取逻辑
    • 通过FieldExtractor接口实现

7.2 选择建议

  • 复杂协议使用Struct字段方式有编解码器
  • 简单协议:使用字段映射方式(性能更好)
  • 特殊需求:使用自定义提取器

7.3 关键点

  1. 字段提取在解码之前:为了性能,先提取字段匹配,再决定是否需要完整解码
  2. 字段名是抽象的可以是struct字段名、字段映射的key、或自定义名称
  3. 灵活性:支持多种方式,适应不同协议需求

文档版本: v1.0
最后更新: 2024