C#中結構體定義并轉換字節(jié)數(shù)組詳解
最近的項目在做socket通信報文解析的時候,用到了結構體與字節(jié)數(shù)組的轉換;由于客戶端采用C++開發(fā),服務端采用C#開發(fā),所以雙方必須保證各自定義結構體成員類型和長度一致才能保證報文解析的正確性,這一點非常重要。
首先是結構體定義,一些基本的數(shù)據(jù)類型,C#與C++都是可以匹配的:
[StructLayoutAttribute(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)] public struct Head { public ushort proMagic; //包起始標記:固定0x7e7e public ushort proPackLen; //包長度:包頭 + 數(shù)據(jù)區(qū) + 包尾長度,注意不要超過最大長度限制 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; //總包數(shù):當包長超過最大長度限制時,需要拆包,大包拆小包總數(shù),不拆默認1 public ushort proPackId; //當前包號:對應以上總包數(shù)的小包標識,不拆默認0 }
一、首先是 [StructLayoutAttribute(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)],這是C#引用非托管的C/C++的DLL的一種定義定義結構體的方式,主要是為了內存中排序,LayoutKind有兩個屬性Sequential和Explicit,Sequential表示順序存儲,結構體內數(shù)據(jù)在內存中都是順序存放的,CharSet=CharSet.Ansi表示編碼方式。這都是為了使用非托管的指針準備的,這兩點大家記住就可以。
需要注意的是 Pack = 1 這個特性,它代表了結構體的字節(jié)對齊方式,在實際開發(fā)中,C++開發(fā)環(huán)境開始默認是2字節(jié)對齊方式 ,拿上面報文包頭結構體為例,char類型在雖然在內存中至占用一個字節(jié),但在結構體轉為字節(jié)數(shù)組時,系統(tǒng)會自動補齊兩個字節(jié),所以如果C#這面定義為Pack=1,C++默認為2字節(jié)對齊的話,雙方結構體會出現(xiàn)長度不一致的情況,相互轉換時必然會發(fā)生錯位,所以需要大家都默認1字節(jié)對齊的方式,C#定義Pack=1,C++ 添加 #pragma pack 1,保證結構體中字節(jié)對齊方式一致。
二、數(shù)組的定義,結構體中每個成員的長度都是需要明確的,因為內存需要根據(jù)這個分配空間,而C#結構體中數(shù)組是無法進行初始化的,這里我們需要在成員聲明時進行定義;
/// <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> /// 終端信息數(shù)據(jù) /// </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> /// 磁盤信息數(shù)組 /// </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]長度的數(shù)組,實際使用中只能有效的使用前5個字符,因為char[6]最后一位默認\0;
三、結構體與字節(jié)數(shù)組的互轉
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數(shù)組 /// </summary> /// <param name="structObj">要轉換的結構體</param> /// <returns>轉換后的byte數(shù)組</returns> public static byte[] StructToBytes(object structObj) { //得到結構體的大小 int size = Marshal.SizeOf(structObj); //創(chuàng)建byte數(shù)組 byte[] bytes = new byte[size]; //分配結構體大小的內存空間 IntPtr structPtr = Marshal.AllocHGlobal(size); //將結構體拷到分配好的內存空間 Marshal.StructureToPtr(structObj, structPtr, false); //從內存空間拷到byte數(shù)組 Marshal.Copy(structPtr, bytes, 0, size); //釋放內存空間 Marshal.FreeHGlobal(structPtr); //返回byte數(shù)組 return bytes; } /// <summary> /// byte數(shù)組轉結構體 /// </summary> /// <param name="bytes">byte數(shù)組</param> /// <param name="type">結構體類型</param> /// <returns>轉換后的結構體</returns> public static object BytesToStuct(byte[] bytes, Type type) { //得到結構體的大小 int size = Marshal.SizeOf(type); //byte數(shù)組長度小于結構體的大小 if (size > bytes.Length) { //返回空 return null; } //分配結構體大小的內存空間 IntPtr structPtr = Marshal.AllocHGlobal(size); //將byte數(shù)組拷到分配好的內存空間 Marshal.Copy(bytes, 0, structPtr, size); //將內存空間轉換為目標結構體 object obj = Marshal.PtrToStructure(structPtr, type); //釋放內存空間 Marshal.FreeHGlobal(structPtr); //返回結構體 return obj; }
盡管在C#中結構與類有著驚人的相似度,但在實際應用中,會常常因為一些特殊之類而錯誤的使用它,下面幾點內容是筆者認為應該注意的:
對于結構
1)可以有方法與屬性
2)是密封的,不能被繼承,或繼承其他結構
3)結構隱式地繼承自System.ValueType
4)結構有默認的無參數(shù)構造函數(shù),可以將每個字段初始化為默認值,但這個默認的構造函數(shù)不能被替換,即使重載了帶參數(shù)的構造函數(shù)
5)結構沒有析構函數(shù)
6)除了const成員外,結構的字段不能在聲明結構時初始化
7)結構是值類型,在定義時(盡管也使用new運算符)會分配堆??臻g,其值也存儲于堆棧
8)結構主要用于小的數(shù)據(jù)結構,為了更好的性能,不要使用過于龐大的結構
9)可以像類那樣為結構提供 Close() 或 Dispose() 方法
如果經(jīng)常做通信方面的程序,結構體是非常有用的(為了更有效地組織數(shù)據(jù),建議使用結構體)
相關文章
C#事件標準命名規(guī)則及說明(包括用作事件類型的委托命名)
這篇文章主要介紹了C#事件標準命名規(guī)則及說明(包括用作事件類型的委托命名),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-02-02C#使用smtp發(fā)送帶附件的郵件實現(xiàn)方法
這篇文章主要介紹了C#使用smtp發(fā)送帶附件的郵件實現(xiàn)方法,可直接將string類型結果保存為附件,實例中備有相應的注釋便于理解,需要的朋友可以參考下2014-11-11C#使用Fody實現(xiàn)監(jiān)控方法執(zhí)行時間
這篇文章主要為大家詳細介紹了C#如何使用Fody實現(xiàn)監(jiān)控方法執(zhí)行時間,文中的示例代碼講解詳細,具有一定的學習價值,感興趣的小伙伴可以了解下2023-11-11Winform控件Picture實現(xiàn)圖片拖拽顯示效果
這篇文章主要為大家詳細介紹了Winform控件Picture實現(xiàn)圖片拖拽顯示效果,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-09-09Winform學生信息管理系統(tǒng)各子窗體剖析(3)
這篇文章主要針對Winform學生信息管理系統(tǒng)各子窗體進行剖析,感興趣的小伙伴們可以參考一下2016-05-05