当前位置: 移动技术网 > IT编程>开发语言>c# > 深入浅出C#结构体——封装以太网心跳包的结构为例

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

2020年04月01日  | 移动技术网IT编程  | 我要评论

东方神女,李庆民,孟瑶银饰

1.应用背景

底端设备有大量网络报文(字节数组):心跳报文,数据采集报文,告警报文上报。需要有对应的报文结构去解析这些字节流数据。

2.结构体解析

由此,我第一点就想到了用结构体去解析。原因有以下两点:

2.1.结构体存在栈中

类属于引用类型,存在堆中;结构体属于值类型,存在栈中,在一个对象的主要成员为数据且数据量不大的情况下,使用结构会带来更好的性能。

2.2.结构体不需要手动释放

属于托管资源,系统自动管理生命周期,局部方法调用完会自动释放,全局方法会一直存在。

3.封装心跳包结构体

心跳协议报文如下:

对应结构体封装如下:

    [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
    {
        //// <summary>
        /// 结构体转byte数组
        /// </summary>
        /// <param name="structobj">要转换的结构体</param>
        /// <returns>转换后的byte数组</returns>
        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;
        }

        /// <summary>
        /// byte数组转结构体
        /// </summary>
        /// <param name="bytes">byte数组</param>
        /// <param name="type">结构体类型</param>
        /// <returns>转换后的结构体</returns>
        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的可以看文章

5.1.不带形参的结构体构造

  • 调用代码
  //初始化结构体
  tcpheartpacket tcpheartpacket = new tcpheartpacket();
  //将上报的心跳报文recevivebuff利用结构体静态帮助类structhelper的bytestostuct方法将字节流转化成结构体
  tcpheartpacket = (tcpheartpacket)structhelper.bytestostuct(recevivebuff, tcpheartpacket.gettype());


从对应的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代码如下:

形成了鲜明的对比,new带参的结构体。il只是去call(调用)ctor(结构体的构造函数),而下面的new类则直接就是newobj,实例化了一个对象存到堆空间去了。

综合5.1,5.2表明结构体的new确实是存在栈里的,而类的new是存在堆里的。

6.性能测试

测试结果如下:


使用结构体解析包需要几十个微妙,其实效率还是很差的。我用类封装成包,解析了,只需要几个微妙,性能差5到10倍。

7.原因分析

主要时间消耗在了bytestostuct方法,代码详见4

  • 心跳包里面用了很多byte[]字节数组,而字节数组本身需要在堆里开辟空间;
  • 该方法进行了装箱拆箱操作;
  • 分配内存在堆上,还是在堆上进行了copy操作;
    拆装箱的il代码如下:

装箱使用的box指令,取消装箱是 unbox.any 指令

8.下一篇:类与结构体性能对比测试——以封装网络心跳包为例

当数据比较大的时候,结构体这种数据复制机制会带来较大的开销。也难怪微软给出的准则中有一条:“当类型定义大于16字节时不要选用struct”。最终我也选择了类来封装以太网包的解析,性能可以达到微妙级,会在下一篇文章《类与结构体性能对比测试——以封装网络心跳包为例》中作详细描述。

9.il工具使用分享


版权声明:本文为博主原创文章,遵循 cc 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。

本文链接:https://www.cnblogs.com/jerrymouseli/p/12606920.html

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

  • 关于C#反射 你需要知道的

    通常,反射用于动态获取对象的类型、属性和方法等信息。今天带你玩转反射,来汇总一下反射的各种常见操作,捡漏看看有没有你不知道的。获取类型的成员type 类的 ge... [阅读全文]
  • c# 字符串操作总结

    字符串操作在任意编程语言的日常编程中都随处可见,今天来汇总一下 c# 中关于字符串的一些你可能遗忘或遗漏的知识点。逐字字符串在普通字符串中,反斜杠字符是转义字符... [阅读全文]
  • C#中静态方法和实例化方法的区别、使用

    1、定义方法的格式 访问修饰符 返回类型 方法名 (参数列表) { // 方法的主体… }2、静态方法在大多数时候,我们定义写一个方法,会把方法区分为实例化方法... [阅读全文]
  • 详解C# FileStream类

    c# filestream类在 c# 语言中文件读写流使用 filestream 类来表示,filestream 类主要用于文件的读写,不仅能读写普通的文本文件... [阅读全文]
  • 快速学习c# 枚举

    一、在学习枚举之前,首先来听听枚举的优点。  1、枚举能够使代码更加清晰,它允许使用描述性的名称表示整数值。  2、枚举使代码更易于维护,有助于确保给变量指定合... [阅读全文]
  • 浅谈C#六大设计原则

    笔者作为一个菜鸟,会尝试以简单的代码和容易理解的语句去解释这几种原则的特性和应用场景。这六种原则分别为单一职责原则、接口隔离原则、里氏替换原则、迪米特法则、依赖... [阅读全文]
  • 解答“60k”大佬的19道C#面试题(上)

    先略看题目:1 请简述async函数的编译方式2 请简述task状态机的实现和工作机制3 请简述await的作用和原理,并说明和getresult()有什么区别... [阅读全文]
  • 解答“60k”大佬的19道C#面试题(下)

    在上篇中,我解析了前 10 道题目,本篇我将尝试解析后面剩下的所有题目。姐妹篇:解析“60k”大佬的19道c#面试题(上)这些题目确实不怎么经常使用,因此在后文... [阅读全文]
  • 如何用C#验证IP是否为局域网地址

    前一阵子有【广州.net群】的客户问起这个问题,说他们需要验证客户输入的网站是否为局域网。其实局域网的ip并没有确定的定义,只要是局域网中,即可设置为任何一个i... [阅读全文]
  • Unity实现截屏以及根据相机画面截图

    在游戏开发和软件开发中,经常需要截图的功能,分带ui的截图和不带ui的截图功能。代码如下:using system.collections;using syst... [阅读全文]
验证码:
移动技术网