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

531 lines
14 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 协议帧头字段概念详解
## 一、协议帧头的概念
### 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