|
|
# NNet协议粘包拆包问题分析与解决方案
|
|
|
|
|
|
## 问题分析
|
|
|
|
|
|
### 当前nnet协议的问题
|
|
|
|
|
|
nnet协议的`Decode`方法目前存在以下问题:
|
|
|
|
|
|
1. **假设数据完整**:`Decode`方法假设传入的数据已经是一个完整的包
|
|
|
2. **无法处理粘包**:如果一次接收到多个包,无法正确拆分
|
|
|
3. **无法处理半包**:如果数据不完整,会直接返回错误,无法等待更多数据
|
|
|
|
|
|
```go
|
|
|
// 当前实现的问题
|
|
|
func (p *NNetProtocol) Decode(data []byte) (protocolpkg.FrameHeader, []byte, error) {
|
|
|
// 检查最小长度
|
|
|
if len(data) < 11 {
|
|
|
return nil, nil, fmt.Errorf("invalid packet length: %d", len(data))
|
|
|
}
|
|
|
// ... 如果数据不完整,直接返回错误
|
|
|
}
|
|
|
```
|
|
|
|
|
|
### NNet协议格式
|
|
|
|
|
|
```
|
|
|
[Magic(4 bytes)][Version(1 byte)][Length(4 bytes)][Data(N bytes)][Checksum(2 bytes)]
|
|
|
```
|
|
|
|
|
|
- Magic: 4字节,固定为"NNET"
|
|
|
- Version: 1字节
|
|
|
- Length: 4字节,大端序,表示Data部分的长度
|
|
|
- Data: N字节,实际数据
|
|
|
- Checksum: 2字节,大端序
|
|
|
|
|
|
**总长度 = 5(Magic+Version) + 4(Length字段) + Length(数据长度) + 2(Checksum) = 11 + Length**
|
|
|
|
|
|
## 解决方案:使用内置Unpacker
|
|
|
|
|
|
### 1. 使用LengthFieldUnpacker(推荐)
|
|
|
|
|
|
nnet协议有长度字段,最适合使用`LengthFieldUnpacker`:
|
|
|
|
|
|
```go
|
|
|
// nnet协议的长度字段配置
|
|
|
unpackerConfig := unpacker.LengthFieldUnpacker{
|
|
|
LengthFieldOffset: 5, // Magic(4) + Version(1) = 5
|
|
|
LengthFieldLength: 4, // Length字段是4字节
|
|
|
LengthAdjustment: 2, // 需要加上Checksum(2字节)
|
|
|
InitialBytesToStrip: 0, // 不跳过任何字节,保留完整包
|
|
|
}
|
|
|
|
|
|
// 创建拆包器
|
|
|
nnetUnpacker := unpacker.NewLengthFieldUnpacker(unpackerConfig)
|
|
|
```
|
|
|
|
|
|
**工作原理**:
|
|
|
- 从偏移5的位置读取4字节的长度字段
|
|
|
- 实际包长度 = 长度字段值 + 2(Checksum)
|
|
|
- 总长度 = 5(Magic+Version) + 4(Length字段) + Length(数据) + 2(Checksum)
|
|
|
|
|
|
### 2. 使用FrameHeaderUnpacker
|
|
|
|
|
|
也可以使用`FrameHeaderUnpacker`,通过自定义函数从帧头获取长度:
|
|
|
|
|
|
```go
|
|
|
// nnet协议的帧头拆包器配置
|
|
|
unpackerConfig := unpacker.FrameHeaderUnpacker{
|
|
|
HeaderLength: 9, // Magic(4) + Version(1) + Length(4) = 9
|
|
|
GetLength: func(header []byte) int {
|
|
|
// 从header中提取Length字段(偏移5-8)
|
|
|
if len(header) < 9 {
|
|
|
return 0
|
|
|
}
|
|
|
length := binary.BigEndian.Uint32(header[5:9])
|
|
|
// 总长度 = 9(帧头) + length(数据) + 2(Checksum)
|
|
|
return int(length) + 2
|
|
|
},
|
|
|
}
|
|
|
|
|
|
// 创建拆包器
|
|
|
nnetUnpacker := unpacker.NewFrameHeaderUnpacker(unpackerConfig)
|
|
|
```
|
|
|
|
|
|
## 集成到服务器
|
|
|
|
|
|
### 当前服务器代码的问题
|
|
|
|
|
|
当前`OnTraffic`方法直接调用协议的`Decode`方法,无法处理粘包拆包:
|
|
|
|
|
|
```go
|
|
|
// 当前实现(有问题)
|
|
|
data, _ := c.Peek(-1) // 读取所有数据
|
|
|
if protocol != nil {
|
|
|
parseProtocolHeader(ctx.Request(), data, protocol) // 直接解码,无法处理粘包
|
|
|
}
|
|
|
```
|
|
|
|
|
|
### 正确的实现方式
|
|
|
|
|
|
需要在协议解码之前先使用unpacker进行拆包:
|
|
|
|
|
|
```go
|
|
|
// 正确的实现
|
|
|
// 1. 获取或创建连接的unpacker(每个连接一个,保持状态)
|
|
|
unpacker := getOrCreateUnpacker(connID, protocol)
|
|
|
|
|
|
// 2. 读取新数据
|
|
|
newData, _ := c.Peek(-1)
|
|
|
|
|
|
// 3. 使用unpacker拆包(处理粘包拆包)
|
|
|
messages, remaining, err := unpacker.Unpack(newData)
|
|
|
if err != nil {
|
|
|
// 处理错误
|
|
|
}
|
|
|
|
|
|
// 4. 对每个完整的消息进行协议解码
|
|
|
for _, message := range messages {
|
|
|
// 使用协议的Decode方法解码单个完整消息
|
|
|
header, body, err := protocol.Decode(message)
|
|
|
// ... 处理
|
|
|
}
|
|
|
|
|
|
// 5. 保存剩余数据到连接的缓冲区
|
|
|
saveRemainingData(connID, remaining)
|
|
|
|
|
|
// 6. 丢弃已处理的数据
|
|
|
c.Discard(processedBytes)
|
|
|
```
|
|
|
|
|
|
## 内置Unpacker的使用
|
|
|
|
|
|
### 1. FixedLengthUnpacker(固定长度)
|
|
|
|
|
|
适用于固定长度的协议:
|
|
|
|
|
|
```go
|
|
|
unpacker := unpacker.NewFixedLengthUnpacker(1024) // 每个包固定1024字节
|
|
|
```
|
|
|
|
|
|
### 2. LengthFieldUnpacker(长度字段)
|
|
|
|
|
|
适用于有长度字段的协议(如nnet):
|
|
|
|
|
|
```go
|
|
|
config := unpacker.LengthFieldUnpacker{
|
|
|
LengthFieldOffset: 5,
|
|
|
LengthFieldLength: 4,
|
|
|
LengthAdjustment: 2,
|
|
|
InitialBytesToStrip: 0,
|
|
|
}
|
|
|
unpacker := unpacker.NewLengthFieldUnpacker(config)
|
|
|
```
|
|
|
|
|
|
### 3. DelimiterUnpacker(分隔符)
|
|
|
|
|
|
适用于用分隔符分割的协议:
|
|
|
|
|
|
```go
|
|
|
unpacker := unpacker.NewDelimiterUnpacker([]byte{'\n'}) // 使用换行符分隔
|
|
|
```
|
|
|
|
|
|
### 4. FrameHeaderUnpacker(帧头)
|
|
|
|
|
|
适用于有帧头的协议,可以自定义长度提取逻辑:
|
|
|
|
|
|
```go
|
|
|
config := unpacker.FrameHeaderUnpacker{
|
|
|
HeaderLength: 9,
|
|
|
GetLength: func(header []byte) int {
|
|
|
// 自定义长度提取逻辑
|
|
|
return int(binary.BigEndian.Uint32(header[5:9])) + 2
|
|
|
},
|
|
|
}
|
|
|
unpacker := unpacker.NewFrameHeaderUnpacker(config)
|
|
|
```
|
|
|
|
|
|
## 建议的改进方案
|
|
|
|
|
|
### 方案1:在协议层面集成Unpacker
|
|
|
|
|
|
为每个协议配置Unpacker,协议管理器负责管理:
|
|
|
|
|
|
```go
|
|
|
type Protocol interface {
|
|
|
// ... 现有方法
|
|
|
|
|
|
// Unpacker 获取协议的拆包器
|
|
|
Unpacker() unpacker.Unpacker
|
|
|
}
|
|
|
```
|
|
|
|
|
|
### 方案2:在连接层面管理Unpacker
|
|
|
|
|
|
每个连接维护自己的Unpacker实例和缓冲区:
|
|
|
|
|
|
```go
|
|
|
type Connection struct {
|
|
|
// ... 现有字段
|
|
|
unpacker unpacker.Unpacker
|
|
|
buffer []byte // 累积不完整的数据
|
|
|
}
|
|
|
```
|
|
|
|
|
|
### 方案3:在服务器层面统一处理
|
|
|
|
|
|
在`OnTraffic`中统一使用Unpacker处理所有数据:
|
|
|
|
|
|
```go
|
|
|
func (h *eventHandler) OnTraffic(c gnet.Conn) {
|
|
|
// 1. 获取连接的unpacker
|
|
|
unpacker := h.getUnpacker(connID, protocol)
|
|
|
|
|
|
// 2. 读取新数据
|
|
|
newData, _ := c.Peek(-1)
|
|
|
|
|
|
// 3. 拆包
|
|
|
messages, remaining, _ := unpacker.Unpack(newData)
|
|
|
|
|
|
// 4. 处理每个完整消息
|
|
|
for _, msg := range messages {
|
|
|
// 协议解码
|
|
|
// 路由匹配
|
|
|
// 处理请求
|
|
|
}
|
|
|
}
|
|
|
```
|
|
|
|
|
|
## 总结
|
|
|
|
|
|
1. **nnet协议当前无法正确处理粘包拆包**,需要集成Unpacker
|
|
|
2. **推荐使用LengthFieldUnpacker**,因为nnet协议有长度字段
|
|
|
3. **内置的4种Unpacker都可以使用**,根据协议特点选择
|
|
|
4. **需要在服务器层面集成Unpacker**,在协议解码之前先拆包
|
|
|
|