C#中结构体定义并转换字节数组详解

 更新时间:2017年11月13日 09:02:28   作者:dafanjoy  
在写C#TCP通信程序时,发送数据时,只能发送byte数组,处理起来比较麻烦不说,如果是和VC6.0等写的程序通信的话,很多的都是传送结构体,在VC6.0中可以很方便的把一个char[]数组转换为一个结构体,而在C#却不能直接把byte数组转换为结构体,要在C#中发送结构体,应该怎么做呢?

最近的项目在做socket通信报文解析的时候,用到了结构体与字节数组的转换;由于客户端采用C++开发,服务端采用C#开发,所以双方必须保证各自定义结构体成员类型和长度一致才能保证报文解析的正确性,这一点非常重要。

       首先是结构体定义,一些基本的数据类型,C#与C++都是可以匹配的:

  [StructLayoutAttribute(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
  public struct Head
  {
    public ushort proMagic;     //包起始标记:固定0x7e7e
    public ushort proPackLen;    //包长度:包头 + 数据区 + 包尾长度,注意不要超过最大长度限制
    public long  proSrcAddr;    //源地址:不使用,填0
    public ushort proSrcPort;    //源地址端口:不使用,填0
    public long  proDstAddr;    //目的地址:不使用,填0
    public ushort proDstPort;    //目的端口:不使用,填0
    public ushort proCmdCode;    //命令码:参见以上命令码定义

    public ushort proVersion;    //版本号:不使用,填1
    public char  proSerial;     //报文序号:一条报文实例对应一个序号,不同报文叠加,0-255往复
    public ushort proPackSum;    //总包数:当包长超过最大长度限制时,需要拆包,大包拆小包总数,不拆默认1
    public ushort proPackId;     //当前包号:对应以上总包数的小包标识,不拆默认0

  }

       一、首先是 [StructLayoutAttribute(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)],这是C#引用非托管的C/C++的DLL的一种定义定义结构体的方式,主要是为了内存中排序,LayoutKind有两个属性Sequential和Explicit,Sequential表示顺序存储,结构体内数据在内存中都是顺序存放的,CharSet=CharSet.Ansi表示编码方式。这都是为了使用非托管的指针准备的,这两点大家记住就可以。

       需要注意的是 Pack = 1 这个特性,它代表了结构体的字节对齐方式,在实际开发中,C++开发环境开始默认是2字节对齐方式 ,拿上面报文包头结构体为例,char类型在虽然在内存中至占用一个字节,但在结构体转为字节数组时,系统会自动补齐两个字节,所以如果C#这面定义为Pack=1,C++默认为2字节对齐的话,双方结构体会出现长度不一致的情况,相互转换时必然会发生错位,所以需要大家都默认1字节对齐的方式,C#定义Pack=1,C++ 添加 #pragma pack 1,保证结构体中字节对齐方式一致。

       二、数组的定义,结构体中每个成员的长度都是需要明确的,因为内存需要根据这个分配空间,而C#结构体中数组是无法进行初始化的,这里我们需要在成员声明时进行定义;

  /// <summary>
  /// 终端信息查询
  /// </summary>
  [StructLayoutAttribute(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
  public struct PackTerminalSearch5001
  {
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 6)]
    /// <summary>
    /// 终端编号
    /// </summary>
    public string stationCode;

    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)]
    /// <summary>
    /// 回复指令
    /// </summary>
    public Byte[] order;
  }
  /// <summary>
  /// 终端信息数据
  /// </summary>

  [StructLayoutAttribute(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
  public struct PackTerminalSearch3004
  {
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 6)]
    /// <summary>
    /// 终端编号
    /// </summary>
    public string stationCode;
    /// <summary>
    /// 终端IP
    /// </summary>
    public long terminalIP;
    /// <summary>
    /// 终端端口
    /// </summary>
    public ushort terminalPort;
    /// <summary>
    /// 中心IP
    /// </summary>
    public long serverIP;
    /// <summary>
    /// 测站端口
    /// </summary>
    public ushort serverPort;
    /// <summary>
    /// 磁盘信息数组
    /// </summary>
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
    public PackDiskInfo[] diskInfoArray;
  }

  /// <summary>
  /// 磁盘信息
  /// </summary>
  [StructLayoutAttribute(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
  public struct PackDiskInfo
  {
    /// <summary>
    /// 盘符
    /// </summary>
    public char drive;
    /// <summary>
    /// 总空间
    /// </summary>
    public double totalSize;
    /// <summary>
    /// 可用空间
    /// </summary>
    public double usableSize;
  }


 

        上面的代码需要注意的是string类型实际为Char[6]长度的数组,实际使用中只能有效的使用前5个字符,因为char[6]最后一位默认\0;

        三、结构体与字节数组的互转

    PackTerminalSearch5001 info;
    info.stationCode = "12345";
    info.order = new byte[6] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05 };
    Byte[] recv = StructToBytes(info);

    object obj = BytesToStuct(recv, typeof(PackTerminalSearch5001));
    PackTerminalSearch5001 info5001 = (PackTerminalSearch5001)obj;
    byte[] order = info5001.order;


    //// <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);
      //将byte数组拷到分配好的内存空间
      Marshal.Copy(bytes, 0, structPtr, size);
      //将内存空间转换为目标结构体
      object obj = Marshal.PtrToStructure(structPtr, type);
      //释放内存空间
      Marshal.FreeHGlobal(structPtr);
      //返回结构体
      return obj;
    }

尽管在C#中结构与类有着惊人的相似度,但在实际应用中,会常常因为一些特殊之类而错误的使用它,下面几点内容是笔者认为应该注意的:

对于结构

1)可以有方法与属性
2)是密封的,不能被继承,或继承其他结构
3)结构隐式地继承自System.ValueType
4)结构有默认的无参数构造函数,可以将每个字段初始化为默认值,但这个默认的构造函数不能被替换,即使重载了带参数的构造函数
5)结构没有析构函数
6)除了const成员外,结构的字段不能在声明结构时初始化
7)结构是值类型,在定义时(尽管也使用new运算符)会分配堆栈空间,其值也存储于堆栈
8)结构主要用于小的数据结构,为了更好的性能,不要使用过于庞大的结构
9)可以像类那样为结构提供 Close() 或 Dispose() 方法

如果经常做通信方面的程序,结构体是非常有用的(为了更有效地组织数据,建议使用结构体)

相关文章

  • Unity多语言转换工具的实现

    Unity多语言转换工具的实现

    这篇文章主要为大家详细介绍了Unity多语言转换工具的实现,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2020-06-06
  • C#模拟MSN窗体抖动的实现代码

    C#模拟MSN窗体抖动的实现代码

    这篇文章主要介绍了C#模拟MSN窗体抖动的实现代码,非常实用的一个功能,需要的朋友可以参考下
    2014-08-08
  • C#通过指针读取文件的方法

    C#通过指针读取文件的方法

    这篇文章主要介绍了C#通过指针读取文件的方法,涉及C#针对文件的相关操作技巧,需要的朋友可以参考下
    2015-06-06
  • c# wpf如何附加依赖项属性

    c# wpf如何附加依赖项属性

    这篇文章主要介绍了c# wpf如何附加依赖项属性,帮助大家更好的理解和学习使用c#,感兴趣的朋友可以了解下
    2021-03-03
  • c#中SqlHelper封装SqlDataReader的方法

    c#中SqlHelper封装SqlDataReader的方法

    这篇文章主要介绍了c#中SqlHelper封装SqlDataReader的方法,涉及C#针对数据库相关操作封装与使用的技巧,需要的朋友可以参考下
    2015-05-05
  • C#实现动态加载dll的方法

    C#实现动态加载dll的方法

    这篇文章主要介绍了C#实现动态加载dll的方法,涉及针对动态链接库的灵活操作技巧,具有一定的参考借鉴价值,需要的朋友可以参考下
    2014-12-12
  • 如何通过IL了解C#类的构造函数浅析

    如何通过IL了解C#类的构造函数浅析

    这篇文章主要给大家介绍了关于如何通过IL了解C#类的构造函数的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2019-02-02
  • c# 获取机器唯一识别码的示例

    c# 获取机器唯一识别码的示例

    这篇文章主要介绍了c# 获取机器唯一识别码的示例,帮助大家更好的理解和学习使用c#,感兴趣的朋友可以了解下
    2021-03-03
  • NumberToUpper数字转中文详解

    NumberToUpper数字转中文详解

    本文介绍NumberToUpper数字转中文的方法,大家参考使用吧
    2013-12-12
  • C# 视频播放类

    C# 视频播放类

    c#下实现视频播放的类代码。
    2009-06-06

最新评论