事情是这样的
在QQ群中有位网友在解析通讯包时,用了很多字符串处理,出了一些问题。俺的建议是:通讯处理可以用byte[]、可以用stream、可以用结构体,尽量少用字符串。
有网友问完为啥用结构体?俺的回答时,实际上很多网络通讯的程序都在使用结构体来生成或者解析或解析通讯包,特别是c++ 和 Delphi ,实际上C#也有很多是这么用的。下面就是一个例子。
一个简单的通讯定义例子
通讯包定义:【Header】 + 【数据】 + 【Foot】
头和尾的定义如下:
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct Header
{
public byte Verson; //版本号
public UInt32 SequenceNo;//流水号
public UInt32 Cmd;//命令
public Int32 ParamInt1;//参数1
public Int32 ParamInt2;//参数2
public Int32 ParamInt3;//参数3
public Int32 ParamInt4;//参数4
public Int32 ParamInt5;//参数5
public int DataLen;//数据长度
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct Foot
{
public UInt32 Timestamp;//时间戳
public UInt16 ChkSum;//16位校验和
}
//【Header】 + 【数据】 + 【Foot】
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct Header
{
public byte Verson; //版本号
public UInt32 SequenceNo;//流水号
public UInt32 Cmd;//命令
public Int32 ParamInt1;//参数1
public Int32 ParamInt2;//参数2
public Int32 ParamInt3;//参数3
public Int32 ParamInt4;//参数4
public Int32 ParamInt5;//参数5
public int DataLen;//数据长度
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct Foot
{
public UInt32 Timestamp;//时间戳
public UInt16 ChkSum;//16位校验和
}
数据部分由业务需求决定,可以是文本、json、xml、音视频等等。
数据生成和解析测试(方法一)
200万次 数据生成和解析 大约0.1秒左右 (本文的所有测试为 64位 Release版本)。
数据的生成
下面的方法中最大的特点就是先分配内存 byte[] buff = new byte[Len];
然后用fixed 对头、尾进行赋值。
private unsafe byte[] MakeData(UInt32 SequenceNo, UInt32 Cmd, Int32 ParamInt1, Int32 ParamInt2, byte[] Data)
{
int HeaderLen = sizeof(Header);
int DataLen = Data.Length;
int FootLen = sizeof(Foot);
int Len = HeaderLen + DataLen + FootLen;
byte[] buff = new byte[Len];
fixed (byte* p = &buff[0])
{
Header* head = (Header*)p;
(*head).Verson = 1;
(*head).SequenceNo = SequenceNo;
(*head).Cmd = Cmd;
(*head).ParamInt1 = ParamInt1;
(*head).ParamInt2 = ParamInt2;
(*head).DataLen = DataLen;
}
Array.Copy(Data, 0,buff, HeaderLen, DataLen);
fixed (byte* p = &buff[HeaderLen + DataLen])
{
Foot* foot = (Foot*)p;
(*foot).Timestamp = 1;
(*foot).ChkSum = 0xffff;
}
return buff;
}
数据的解析
这里使用了一个文本做为数据进行测试。注释掉了 gb2312.GetString ,只测试数据的解析。
通过 Cmd = (*head).Cmd; 可以看出 ,数据定义中的成员可以直接访问,不需要一个一个的去解析数值。
服务端和客户端的结构体的定义一致,有非常多非常多的成员都没关系,直接访问就ok。早年间很多通讯文档就是这样的。通讯文档里有 C++或 Delphi的结构体的定义,开发时直接引入工程就可以了。有点像Protocol Buffers ,但比Protocol Buffers 更快。
private unsafe void button1_Click(object sender, EventArgs e)
{
Encoding gb2312 = Encoding.GetEncoding("GB2312");
byte[] Data = gb2312.GetBytes( "63号命令的文本63号命令的文本63号命令的文本63号命令的文本63号命令的文本63号命令的文本63号命令的文本63号命令的文本");
Stopwatch timer = new Stopwatch();
timer.Start();
for (int m = 0; m < 10000 * 200; m++)
{
//生成一个测试数据
byte[] buff = MakeData(1, 63, 1, 100, Data);
//从数据中读取数据
int HeaderLen = sizeof(Header);
int DataLen = 0;
int FootLen = sizeof(Foot);
UInt32 Cmd = 0;
fixed (byte* p = &buff[0])
{
Header* head = (Header*)p;
DataLen = (*head).DataLen;
Cmd = (*head).Cmd;
}
// string Text = gb2312.GetString(buff, HeaderLen, DataLen);// 检查文本
}
timer.Stop();
MessageBox.Show("方法1: 200万次 数据生成和解析 耗时"+ timer.Elapsed.Duration().TotalSeconds.ToString());
}
数据生成和解析测试(方法二)
可能有细心的同学看到了 方法一执行速度快 的原因了。无它,方法一用了 unsafe 。下面再讲一个无需 unsafe的方法。
好像速度慢了很多,那是俺还没有优化的原因。 不过,优化了应该也比方法一慢不少,毕竟方法一 unsafe了(这等于开挂了)。
数据的生成
一段平平淡淡的代码,没啥特点。
private byte[] MakeData2(UInt32 SequenceNo, UInt32 Cmd, Int32 ParamInt1, Int32 ParamInt2, byte[] Data)
{
int HeaderLen = Marshal.SizeOf(typeof(Header));
int DataLen = Data.Length;
int FootLen = Marshal.SizeOf(typeof(Foot));
int Len = HeaderLen + DataLen + FootLen;
byte[] buff = new byte[Len];
Header head;
head.Verson = 1;
head.SequenceNo = SequenceNo;
head.Cmd = Cmd;
head.ParamInt1 = ParamInt1;
head.ParamInt2 = ParamInt2;
head.ParamInt3 = 0;
head.ParamInt4 = 0;
head.ParamInt5 = 0;
head.DataLen = DataLen;
IntPtr head_buffer = Marshal.AllocHGlobal(HeaderLen);
Marshal.StructureToPtr(head, head_buffer, false);
Marshal.Copy(head_buffer, buff, 0, HeaderLen);
Marshal.FreeHGlobal(head_buffer);
Array.Copy(Data, 0, buff, HeaderLen, DataLen);
Foot foot;
foot.Timestamp = 1;
foot.ChkSum = 0xffff;
IntPtr foot_buffer = Marshal.AllocHGlobal(FootLen);
Marshal.StructureToPtr(foot, foot_buffer, false);
Marshal.Copy(foot_buffer, buff, HeaderLen + DataLen, FootLen);
Marshal.FreeHGlobal(foot_buffer);
return buff;
}
数据的解析
同样是一段平平淡淡的代码,没啥特别的。只是用了结构体进行数据的生成和解析。这种写法唯一的好处就是,结构体定义好了,一切就很简单,不管成员有多少。其实这个方法也不是特别慢,一般的场景 1.6秒处理 200万条数据 也可以了。因为真正的瓶颈不在这里,瓶颈在网络、业务、存储等等。
private void button2_Click(object sender, EventArgs e)
{
Encoding gb2312 = Encoding.GetEncoding("GB2312");
byte[] Data = gb2312.GetBytes("63号命令的文本63号命令的文本63号命令的文本63号命令的文本63号命令的文本63号命令的文本63号命令的文本63号命令的文本");
int HeaderLen = Marshal.SizeOf(typeof(Header));
int FootLen = Marshal.SizeOf(typeof(Foot));
Stopwatch timer = new Stopwatch();
timer.Start();
for (int m = 0; m < 10000 * 200; m++)
{
//生成一个测试数据
byte[] buff = MakeData2(1, 63, 1, 100, Data);
IntPtr structPtr = Marshal.AllocHGlobal(HeaderLen);
Marshal.Copy(buff, 0, structPtr, HeaderLen);
Header head= Marshal.PtrToStructure<Header>(structPtr);
Marshal.FreeHGlobal(structPtr);
int DataLen = head.DataLen;
UInt32 Cmd = head.Cmd;
//string Text = gb2312.GetString(buff, HeaderLen, DataLen);// 检查文本
}
timer.Stop();
MessageBox.Show("方法2: 200万次 数据生成和解析 耗时" + timer.Elapsed.Duration().TotalSeconds.ToString());
}
另外
为啥大家都喜欢在定义中加个 public byte Verson; //版本号 ?
因为可以上下兼容。例如多版本的协议并行,客户端的版本可以是 1.3.23 、也可能是 2.2.4、也可能是 2.5.1。 服务端根据Verson 走不同结构体就ok 。