C#中結構體定義並轉換位元組陣列 C#中結構體定義並轉換位元組陣列
ref: https://www.cnblogs.com/dafanjoy/p/7818126.html
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; }
最近的專案在做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; }