C# 实现高效的 数据组包和拆包处理器,使用循环缓冲区处理

2017-01-05 11:04:17来源:oschina作者:ZSkycat人点击

第七城市


本文主要是记录实现过程和细节说明,我已经将代码封装好并开源,先祭出代码,如果对你有用,欢迎点Star。
**GitHub**:[DatagramHandler](https://github.com/ZSkycat/CShareToolbox/tree/master/Source/ZSkycat.CShareToolbox/DatagramHandler)
# 基本说明
在开发TCP通信、串口通信相关项目的时候,需要涉及到组包和封包等处理。最开始的时候,我直接使用一个数组作为缓冲区,每次取数据后都需要将数据往前挪,这是相当费时的操作,于是就决定有空写一个比较高效的类来处理啦。虽然目前的项目按原来的处理方式就已经满足需求,但是我就是想写呀,任性。O(∩_∩)O
实际使用中一般不用多线程处理,所以不考虑线程安全了。
### 文件结构
- DatagramHandler.cs:提供默认数据包处理协议,可按需修改
- CircularBuffer.cs:实现循环缓冲区功能,提供高效操作
### 默认的数据包协议
![数据包](/2014th7cj/d/file/p/20170104/v40abgffiad.png "数据包")
如图所示,整个数据包由5部分组成。
开始标识和结束标识是固定的 2 byte。
数据长度占用 4 byte,int32类型,以 LittleEndian 编码,表示数据区占用的 byte 数量。
校验值是数据长度的异或校验值,即将 4 byte 相互异或计算后的结果,用于检查长度数据是否错误。(也称为BCC校验,Block Check Character)
数据区即原本要传输的二进制数据。
### 循环缓冲区
主要特点是一个首尾相连的FIFO的数据结构,采用数组的线性存储,避免了每次读写数据时,需要将数据挪到数据首部耗费的时间。
- 使用 dataBegin 和 dataEnd 记录写入索引和取出索引
- 使用 dataLength 记录实际数据长度
- 使用 System.Buffer.BlockCopy() 高效拷贝数据
- 支持从任意位置读取数据,方便针对数据包协议处理数据,删除数据只能从数据头部开始。
- 提供自动扩充功能,该功能开启时,当缓冲区不足容纳新添加的数据时,自动将空间扩充为原来的4/3,该操作比较耗时,建议初始化定好缓冲区大小
# 实现过程
## CircularBuffer
在开发项目中实现了拆包组包功能后,觉得每次处理数据后都要调整缓冲区的做法不好,时间都浪费在挪数据上了。开始想到,我可以把数组当作一个环串起来,就像循环队列一样,我只需要知道实际数据从哪里开始、从哪里结束,便可以很优雅不用挪动数据的操作了。
接下来,我在搜索引擎查找类似的案例,看看有无什么能够启发我的。我找到了 循环缓冲区(也叫环形缓冲区,[Circular Buffer](https://en.wikipedia.org/wiki/Circular_buffer))这种数据结构,果然是前辈们已经总结出来的经验啦。
再之后,我想看看 .NET Framework 标准库里有没有什么我可以用的玩意,虽然没有找到循环缓冲区的数据结构,但是发现了新大陆 [System.Buffer](https://msdn.microsoft.com/zh-cn/library/system.buffer.aspx),一个以字节为单位的高效处理数组的静态类,如其名 Buffer 专门针对缓冲区。
我使用普通的 for、Array.Copy、Buffer.BlockCopy 进行拷贝1亿长度的int数组测试,耗时如下图,可以看到 Buffer.BlockCopy 是最快的,而在拷贝byte数组情况下,Array.Copy和Buffer.BlockCopy的耗时倒是不分上下。
![测试数据](/2014th7cj/d/file/p/20170104/gmllgzi4m1z.png "测试数据")
``` C#
// 测试代码
var list1 = new byte[99999999];
var list2 = new byte[99999999];
var random = new Random();
for (var i = 0; i < list1.Length; i++)
{
list1[i] = (byte)random.Next(byte.MaxValue);
}
watch.Restart();
for (var i = 0; i < list1.Length; i++)
{
list1[i] = list2[i];
}
watch.Stop();
Console.WriteLine($"for: {watch.Elapsed}");
watch.Restart();
Array.Copy(list1, 0, list2, 0, list1.Length);
watch.Stop();
Console.WriteLine($"Array.Copy: {watch.Elapsed}");
watch.Restart();
Buffer.BlockCopy(list1, 0, list2, 0, list1.Length);
watch.Stop();
Console.WriteLine($"Buffer.BlockCopy: {watch.Elapsed}");
```
由于拆包需要用到 IndexOf 功能来处理,所以我需要把这个功能在缓冲区中实现。一开始是打算直接使用foreach来实现,避免去判断缓冲区中数据的位置情况(缓冲区实现 IEnumerable),但是经过测试发现,耗时远远大于 Array.IndexOf。
我想知道为什么会这样,通过 [Reference Source](http://referencesource.microsoft.com/) 查询标准库源码,虽然功力不够,并不能完全看懂,但是发现代码到了最后是使用了 cpp 来实现功能的。这大概就是为什么 C# .NET 的性能能够比拟 C++ 的缘故吧,无比佩服设计者,把苦水自己喝了,留下简单的给应用开发者。
最后我使用 Array.IndexOf 来实现我需要的 IndexOf 功能。
按照 FIFO 的思想,读取(取出)数据必须是从数据头部开始的,我考虑到拆解数据包时,我不需要全部的数据,最终我选择允许从任意位置读取数据,但是删除数据还是必须从数据头部开始。
## DatagramHandler
在数据包处理上,最主要的莫过于数据包的协议,和怎么解析出完整的数据包。
在考虑了串口通讯中特别容易出现数据丢失和干扰的情况,加入了开始标识、结束标识、数据包长度标识,提供一定矫错的能力。
之后的单元测试中,发现数据包长度标识一旦错误的出现超长的情况,会导致一直等待数据到达最终导致缓冲区溢出。所以我又对数据长度加入了异或校验,同时设定数据包长度必须小于缓冲区大小,否则认为是异常数据。
解析中最主要的方法是 CheckDatagram(),负责 检查缓冲区数据,移除异常数据,返回是否存在完整数据包。当实际需求有通讯协议约定时,可以按需修改。
第七城市

最新文章

123

最新摄影

微信扫一扫

第七城市微信公众平台