C#中结构体定义并转换字节数组详解
最近的项目在做socket通信报文解析的时候,用到了结构体与字节数组的转换;由于客户端采用C++开发,服务端采用C#开发,所以双方必须保证各自定义结构体成员类型和长度一致才能保证报文解析的正确性,这一点非常重要。
首先是结构体定义,一些基本的数据类型,C#与C++都是可以匹配的:
[StructLayoutAttribute(LayoutKind.Sequential,CharSet=CharSet.Ansi,Pack=1)] publicstructHead { publicushortproMagic;//包起始标记:固定0x7e7e publicushortproPackLen;//包长度:包头+数据区+包尾长度,注意不要超过最大长度限制 publiclongproSrcAddr;//源地址:不使用,填0 publicushortproSrcPort;//源地址端口:不使用,填0 publiclongproDstAddr;//目的地址:不使用,填0 publicushortproDstPort;//目的端口:不使用,填0 publicushortproCmdCode;//命令码:参见以上命令码定义 publicushortproVersion;//版本号:不使用,填1 publiccharproSerial;//报文序号:一条报文实例对应一个序号,不同报文叠加,0-255往复 publicushortproPackSum;//总包数:当包长超过最大长度限制时,需要拆包,大包拆小包总数,不拆默认1 publicushortproPackId;//当前包号:对应以上总包数的小包标识,不拆默认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++添加#pragmapack1,保证结构体中字节对齐方式一致。
二、数组的定义,结构体中每个成员的长度都是需要明确的,因为内存需要根据这个分配空间,而C#结构体中数组是无法进行初始化的,这里我们需要在成员声明时进行定义;
//////终端信息查询 /// [StructLayoutAttribute(LayoutKind.Sequential,CharSet=CharSet.Ansi,Pack=1)] publicstructPackTerminalSearch5001 { [MarshalAs(UnmanagedType.ByValTStr,SizeConst=6)] //////终端编号 /// publicstringstationCode; [MarshalAs(UnmanagedType.ByValArray,SizeConst=6)] //////回复指令 /// publicByte[]order; } //////终端信息数据 /// [StructLayoutAttribute(LayoutKind.Sequential,CharSet=CharSet.Ansi,Pack=1)] publicstructPackTerminalSearch3004 { [MarshalAs(UnmanagedType.ByValTStr,SizeConst=6)] //////终端编号 /// publicstringstationCode; //////终端IP /// publiclongterminalIP; //////终端端口 /// publicushortterminalPort; //////中心IP /// publiclongserverIP; //////测站端口 /// publicushortserverPort; //////磁盘信息数组 /// [MarshalAs(UnmanagedType.ByValArray,SizeConst=8)] publicPackDiskInfo[]diskInfoArray; } //////磁盘信息 /// [StructLayoutAttribute(LayoutKind.Sequential,CharSet=CharSet.Ansi,Pack=1)] publicstructPackDiskInfo { //////盘符 /// publicchardrive; //////总空间 /// publicdoubletotalSize; //////可用空间 /// publicdoubleusableSize; }
上面的代码需要注意的是string类型实际为Char[6]长度的数组,实际使用中只能有效的使用前5个字符,因为char[6]最后一位默认\0;
三、结构体与字节数组的互转
PackTerminalSearch5001info; info.stationCode="12345"; info.order=newbyte[6]{0x00,0x01,0x02,0x03,0x04,0x05}; Byte[]recv=StructToBytes(info); objectobj=BytesToStuct(recv,typeof(PackTerminalSearch5001)); PackTerminalSearch5001info5001=(PackTerminalSearch5001)obj; byte[]order=info5001.order; ///////结构体转byte数组 /// ///要转换的结构体 /// 转换后的byte数组 publicstaticbyte[]StructToBytes(objectstructObj) { //得到结构体的大小 intsize=Marshal.SizeOf(structObj); //创建byte数组 byte[]bytes=newbyte[size]; //分配结构体大小的内存空间 IntPtrstructPtr=Marshal.AllocHGlobal(size); //将结构体拷到分配好的内存空间 Marshal.StructureToPtr(structObj,structPtr,false); //从内存空间拷到byte数组 Marshal.Copy(structPtr,bytes,0,size); //释放内存空间 Marshal.FreeHGlobal(structPtr); //返回byte数组 returnbytes; } //////byte数组转结构体 /// ///byte数组 /// 结构体类型 /// 转换后的结构体 publicstaticobjectBytesToStuct(byte[]bytes,Typetype) { //得到结构体的大小 intsize=Marshal.SizeOf(type); //byte数组长度小于结构体的大小 if(size>bytes.Length) { //返回空 returnnull; } //分配结构体大小的内存空间 IntPtrstructPtr=Marshal.AllocHGlobal(size); //将byte数组拷到分配好的内存空间 Marshal.Copy(bytes,0,structPtr,size); //将内存空间转换为目标结构体 objectobj=Marshal.PtrToStructure(structPtr,type); //释放内存空间 Marshal.FreeHGlobal(structPtr); //返回结构体 returnobj; }
尽管在C#中结构与类有着惊人的相似度,但在实际应用中,会常常因为一些特殊之类而错误的使用它,下面几点内容是笔者认为应该注意的:
对于结构
1)可以有方法与属性
2)是密封的,不能被继承,或继承其他结构
3)结构隐式地继承自System.ValueType
4)结构有默认的无参数构造函数,可以将每个字段初始化为默认值,但这个默认的构造函数不能被替换,即使重载了带参数的构造函数
5)结构没有析构函数
6)除了const成员外,结构的字段不能在声明结构时初始化
7)结构是值类型,在定义时(尽管也使用new运算符)会分配堆栈空间,其值也存储于堆栈
8)结构主要用于小的数据结构,为了更好的性能,不要使用过于庞大的结构
9)可以像类那样为结构提供Close()或Dispose()方法
如果经常做通信方面的程序,结构体是非常有用的(为了更有效地组织数据,建议使用结构体)