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.

236 lines
6.1 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.

# 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字节大端序
**总长度 = 5Magic+Version + 4Length字段 + Length数据长度 + 2Checksum = 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字节的长度字段
- 实际包长度 = 长度字段值 + 2Checksum
- 总长度 = 5Magic+Version + 4Length字段 + Length数据 + 2Checksum
### 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数据 + 2Checksum
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**,在协议解码之前先拆包