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.
14 KiB
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 为什么要在解码前提取?
性能考虑:
- 避免不必要的解码:如果路由匹配失败,就不需要完整解码
- 快速路由:直接从字节流中读取几个字节比完整解码快得多
- 内存效率:不需要为所有消息创建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 字段来源的三种方式
-
Struct字段(最常见):
- 协议有编解码器
- 消息被解码为Go struct
- 字段来自struct的字段名或标签
-
字段映射(简单协议):
- 协议简单,无需完整解码
- 字段来自原始字节流的固定位置
- 通过FieldMapping定义字段位置
-
自定义提取器(复杂场景):
- 需要特殊提取逻辑
- 通过FieldExtractor接口实现
7.2 选择建议
- 复杂协议:使用Struct字段方式(有编解码器)
- 简单协议:使用字段映射方式(性能更好)
- 特殊需求:使用自定义提取器
7.3 关键点
- 字段提取在解码之前:为了性能,先提取字段匹配,再决定是否需要完整解码
- 字段名是抽象的:可以是struct字段名、字段映射的key、或自定义名称
- 灵活性:支持多种方式,适应不同协议需求
文档版本: v1.0
最后更新: 2024