深入浅出C#结构体——以太网心跳包为例

软件发布|下载排行|最新软件

当前位置:首页IT学院IT技术

深入浅出C#结构体——以太网心跳包为例

JerryMouseLi   2020-03-31 我要评论
[toc] #1.应用背景 底端设备有大量网络报文(字节数组):心跳报文,数据采集报文,告警报文上报。需要有对应的报文结构去解析这些字节流数据。 #2.结构体解析 由此,我第一点就想到了用结构体去解析。原因有以下两点: ##2.1.结构体存在栈中 类属于引用类型,存在堆中;结构体属于值类型,存在栈中,在一个对象的主要成员为数据且数据量不大的情况下,使用结构会带来更好的性能。 ##2.2.结构体不需要手动释放 属于非托管资源,系统自动管理生命周期,局部方法调用完会自动释放,全局方法会一直存在。 #3.封装心跳包结构体 心跳协议报文如下: ![](https://img2020.cnblogs.com/blog/1606616/202003/1606616-20200331174259949-754271350.jpg) 对应结构体封装如下: ``` [StructLayout(LayoutKind.Sequential, Pack = 1)] // 按1字节对齐 public struct TcpHeartPacket { [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] //结构体内定长数组 public byte[] head; public byte type; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)] public byte[] length; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)] public byte[] Mac; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 104)] public byte[] data;//数据体 [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] public byte[] tail; } ``` #4.结构体静态帮助类 主要实现了字节数组向结构体转换方法,以及结构体向字节数组的转换方法。 ``` public class StructHelper { //// /// 结构体转byte数组 /// /// 要转换的结构体 /// 转换后的byte数组 public static byte[] StructToBytes(Object structObj) { //得到结构体的大小 int size = Marshal.SizeOf(structObj); //创建byte数组 byte[] bytes = new byte[size]; //分配结构体大小的内存空间 IntPtr structPtr = Marshal.AllocHGlobal(size); //将结构体拷到分配好的内存空间 Marshal.StructureToPtr(structObj, structPtr, false); //从内存空间拷到byte数组 Marshal.Copy(structPtr, bytes, 0, size); //释放内存空间 Marshal.FreeHGlobal(structPtr); //返回byte数组 return bytes; } /// /// byte数组转结构体 /// /// byte数组 /// 结构体类型 /// 转换后的结构体 public static object BytesToStuct(byte[] bytes, Type type) { //得到结构体的大小 int size = Marshal.SizeOf(type); //byte数组长度小于结构体的大小 if (size > bytes.Length) { //返回空 return null; } //分配结构体大小的内存空间 IntPtr structPtr = Marshal.AllocHGlobal(size); try { //将byte数组拷到分配好的内存空间 Marshal.Copy(bytes, 0, structPtr, size); //将内存空间转换为目标结构体 return Marshal.PtrToStructure(structPtr, type); } finally { //释放内存空间 Marshal.FreeHGlobal(structPtr); } } } ``` #5.New出来的结构体是存在堆中还是栈中? 有同事说new出来的都会放在堆里,我半信半疑。怎么去确定,new出来的结构体到底放在哪里有两种方式,一种是使用Visual Studio的调试工具查看,这种方法找了好久没找到怎么去查看,路过的高手烦请指点下;第二种方法就是查看反编译dll的IL(Intermediate Language)语言。查看最终是以怎样的方式去实现的。不懂IL想了解IL的可以看[此篇](https://www.cnblogs.com/zery/p/3366175.html)文章 ##5.1.不带形参的结构体构造 + 调用代码 ``` //初始化结构体 TcpHeartPacket tcpHeartPacket = new TcpHeartPacket(); //将上报的心跳报文ReceviveBuff利用结构体静态帮助类StructHelper的BytesToStuct方法将字节流转化成结构体 tcpHeartPacket = (TcpHeartPacket)StructHelper.BytesToStuct(ReceviveBuff, tcpHeartPacket.GetType()); ``` ![](https://img2020.cnblogs.com/blog/1606616/202003/1606616-20200331174322867-1027096871.jpg) 从对应的IL代码可以看出只是initobj,并没有newobj,其中newobj表示分配内存,完成对象初始化;而initobj表示对值类型的初始化。 + newobj用于分配和初始化对象;而initobj用于初始化值类型。因此,可以说,newobj在堆中分配内存,并完成初始化;而initobj则是对栈上已经分配好的内存,进行初始化即可,因此值类型在编译期已经在栈上分配好了内存。 + newobj在初始化过程中会调用构造函数;而initobj不会调用构造函数,而是直接对实例置空。 + newobj有内存分配的过程;而initobj则只完成数据初始化操作。 initobj 的执行结果是,将tcpHeartPacket中的引用类型初时化为null,而基元类型则置为0。 综上,new 结构体(无参情况)是放在栈中的,只是做了null/0初始化。 ##5.2.带形参的结构体构造 接下来看下带形参的结构体存放位置。 简化版带形参的结构体如下: ``` public struct TcpHeartPacket { public TcpHeartPacket(byte _type) { type = _type; } public byte type; } ``` 调用如下: ``` //带形参结构体new初始化 TcpHeartPacket tcpHeartPacket = new TcpHeartPacket(0x1); //类的new做对比 IWorkThread __workThread = new WorkThread(); ``` IL代码如下: ![](https://img2020.cnblogs.com/blog/1606616/202003/1606616-20200331174339954-306972164.jpg) >形成了鲜明的对比,new带参的结构体。IL只是去call(调用)ctor(结构体的构造函数),而下面的new类则直接就是newobj,实例化了一个对象存到堆空间去了。 综合5.1,5.2表明结构体的new确实是存在栈里的,而类的new是存在堆里的。 #6.性能测试 测试结果如下: ![](https://img2020.cnblogs.com/blog/1606616/202003/1606616-20200331174354774-566947801.png) ![](https://img2020.cnblogs.com/blog/1606616/202003/1606616-20200331175428556-295266237.jpg) 使用结构体解析包需要几十个微妙,其实效率还是很差的。我用类封装成包,解析了,只需要几个微妙,性能差5到10倍。 #7.原因分析 主要时间消耗在了BytesToStuct方法,代码详见4 + 心跳包里面用了很多byte[]字节数组,而字节数组本身需要在堆里开辟空间; + 该方法进行了装箱拆箱操作; + 分配内存在堆上,还是在堆上进行了copy操作; 拆装箱的IL代码如下: ![](https://img2020.cnblogs.com/blog/1606616/202003/1606616-20200331174410872-781976551.jpg) >装箱使用的box指令,取消装箱是 unbox.any 指令 #8.下一期:结构体与类封装的心跳包性能对比测试 当数据比较大的时候,结构体这种数据复制机制会带来较大的开销。也难怪微软给出的准则中有一条:“当类型定义大于16字节时不要选用struct”。最终我也选择了类来封装以太网包的解析,性能可以达到微妙级,会在下一篇文章《结构体与类封装的心跳包性能对比测试》中作详细描述。 #9.IL工具使用分享 + 使用ildasm工具 [VS2013外部工具中添加ildasm.exe](https://blog.csdn.net/jackson0714/articlehttps://img.qb5200.com/download-x/details/44627161) + 使用dnSpy工具 [dnSpy的github地址](https://github.com/cnxyhttps://img.qb5200.com/download-x/dnSpy/)
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接:https://www.cnblogs.com/JerryMouseLi/p/12606920.html

Copyright 2022 版权所有 软件发布 访问手机版

声明:所有软件和文章来自软件开发商或者作者 如有异议 请与本站联系 联系我们