|
|
# 协议帧头字段概念详解
|
|
|
|
|
|
## 一、协议帧头的概念
|
|
|
|
|
|
### 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
|
|
|
|
|
|
```go
|
|
|
// 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:原始字节流中的固定位置
|
|
|
|
|
|
**适用场景**:协议简单,不需要完整解码,直接从字节流中读取
|
|
|
|
|
|
```go
|
|
|
// 协议格式: [消息类型(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:通过协议特定的提取器
|
|
|
|
|
|
**适用场景**:协议复杂,需要自定义提取逻辑
|
|
|
|
|
|
```go
|
|
|
// 定义字段提取器
|
|
|
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字段(推荐)
|
|
|
|
|
|
```go
|
|
|
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:基于字段映射(无需完整解码)
|
|
|
|
|
|
```go
|
|
|
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:自定义字段提取器
|
|
|
|
|
|
```go
|
|
|
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对象
|
|
|
|
|
|
**示例**:
|
|
|
```go
|
|
|
// 场景:收到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 游戏协议示例
|
|
|
|
|
|
```go
|
|
|
// 协议格式:[消息类型(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 物联网协议示例
|
|
|
|
|
|
```go
|
|
|
// 协议格式:[设备类型(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字段名映射
|
|
|
|
|
|
```go
|
|
|
type MessageHeader struct {
|
|
|
MessageType uint8 // Go字段名:MessageType
|
|
|
// 映射为:message_type(自动转换为小写+下划线)
|
|
|
}
|
|
|
|
|
|
// 路由中使用
|
|
|
router.RegisterFrameHeader("message_type", "==", 0x01, handler)
|
|
|
// 或
|
|
|
router.RegisterFrameHeader("MessageType", "==", 0x01, handler) // 也支持
|
|
|
```
|
|
|
|
|
|
### 6.2 使用标签指定字段名
|
|
|
|
|
|
```go
|
|
|
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
|
|
|
|
|
|
```go
|
|
|
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
|
|
|
|