如何在C#中通过UDP发送包含内部结构体、数组和枚举的结构体
C#自定义结构体UDP序列化与反序列化(无程序集依赖方案)
问题背景
使用Visual Studio 2019开发C# UI程序,需要通过UDP发送包含嵌套结构体、固定长度数组和枚举的自定义结构体。尝试过BinaryFormatter但存在程序集依赖问题——接收端必须引用发送端程序集才能反序列化,需要一种无需依赖的字节数组序列化/反序列化方案。
解决方案:使用Marshal手动序列化/反序列化
利用System.Runtime.InteropServices.Marshal类结合已有的[MarshalAs]特性,可以直接将结构体转换为字节数组。接收端只需定义完全一致的结构体(字段顺序、类型、MarshalAs属性必须匹配)即可完成反序列化,无需引用发送端程序集。
1. 通用序列化/反序列化工具方法
using System.Runtime.InteropServices; public static class StructConverter { // 将结构体转为字节数组 public static byte[] StructToBytes<T>(T structure) where T : struct { int size = Marshal.SizeOf(structure); byte[] buffer = new byte[size]; IntPtr ptr = Marshal.AllocHGlobal(size); try { Marshal.StructureToPtr(structure, ptr, false); Marshal.Copy(ptr, buffer, 0, size); } finally { Marshal.FreeHGlobal(ptr); } return buffer; } // 将字节数组转为结构体 public static T BytesToStruct<T>(byte[] bytes) where T : struct { int size = Marshal.SizeOf<T>(); if (bytes.Length != size) throw new ArgumentException("字节数组长度与结构体大小不匹配"); IntPtr ptr = Marshal.AllocHGlobal(size); try { Marshal.Copy(bytes, 0, ptr, size); return Marshal.PtrToStructure<T>(ptr); } finally { Marshal.FreeHGlobal(ptr); } } }
2. 修正后的发送端代码
原代码存在两处错误需修正:
loc2的初始化错误(误给loc1赋值)arc1.length和arc2.length的元素数量与SizeConst=5不匹配,需补全5个元素
namespace Sender { public struct FMS { [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)] public arc[] arcs; public UInt32 type; } public struct arc { [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)] public location[] locs; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 5)] public sbyte[] length; public UInt32 fuel; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 5)] public char[] ids; public distance dist; } public struct location { public float x_axis; public float y_axis; } public enum distance { PEAK = 0, STEP } public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void Send_Button_Click(object sender, EventArgs e) { Socket clientSocket = new Socket(SocketType.Dgram, ProtocolType.Udp); IPAddress clientIpAddress = IPAddress.Parse("127.0.0.1"); int clientPortNum = 60000; IPEndPoint clientEndPoint = new IPEndPoint(clientIpAddress, clientPortNum); clientSocket.Bind(clientEndPoint); IPAddress serverIpAddress = IPAddress.Parse("127.0.0.1"); int serverPortNum = 50000; IPEndPoint serverEndPoint = new IPEndPoint(serverIpAddress, serverPortNum); // 初始化结构体(修正原代码错误) location loc1 = new location(); loc1.x_axis = 1.0F; loc1.y_axis = 2.0F; location loc2 = new location(); loc2.x_axis = 3.0F; // 修正:原代码误赋值给loc1 loc2.y_axis = 4.0F; arc arc1 = new arc(); arc1.locs = new location[] { loc1, loc2 }; arc1.length = new sbyte[] { 1, 2, 3, 4, 5 }; // 修正:补全5个元素 arc1.fuel = 3500; arc1.ids = new char[] { 'a', 'b', 'c', 'd', 'e' }; arc1.dist = distance.PEAK; location loc3 = new location(); loc3.x_axis = 5.0F; loc3.y_axis = 6.0F; location loc4 = new location(); loc4.x_axis = 7.0F; loc4.y_axis = 8.0F; arc arc2 = new arc(); arc2.locs = new location[] { loc3, loc4 }; arc2.length = new sbyte[] { 5, 6, 7, 8, 9 }; // 修正:补全5个元素 arc2.fuel = 7000; arc2.ids = new char[] { 'x', 'c', 'g', 'f', 'a' }; arc2.dist = distance.STEP; FMS fmsObj = new FMS(); fmsObj.arcs = new arc[] { arc1, arc2 }; fmsObj.type = 1; // 序列化结构体为字节数组 byte[] bytesToSend = StructConverter.StructToBytes(fmsObj); clientSocket.SendTo(bytesToSend, serverEndPoint); clientSocket.Close(); } } }
3. 接收端代码示例
接收端只需定义完全一致的结构体(字段顺序、类型、MarshalAs属性必须和发送端完全相同),然后调用反序列化方法:
namespace Receiver { // 与发送端完全一致的结构体定义 public struct FMS { [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)] public arc[] arcs; public UInt32 type; } public struct arc { [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)] public location[] locs; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 5)] public sbyte[] length; public UInt32 fuel; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 5)] public char[] ids; public distance dist; } public struct location { public float x_axis; public float y_axis; } public enum distance { PEAK = 0, STEP } public partial class ReceiverForm : Form { private Socket serverSocket; public ReceiverForm() { InitializeComponent(); StartListening(); } private void StartListening() { serverSocket = new Socket(SocketType.Dgram, ProtocolType.Udp); IPAddress serverIp = IPAddress.Parse("127.0.0.1"); int serverPort = 50000; IPEndPoint serverEndPoint = new IPEndPoint(serverIp, serverPort); serverSocket.Bind(serverEndPoint); // 异步接收数据 byte[] buffer = new byte[Marshal.SizeOf<FMS>()]; EndPoint remoteEndPoint = new IPEndPoint(IPAddress.Any, 0); serverSocket.BeginReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref remoteEndPoint, ReceiveCallback, buffer); } private void ReceiveCallback(IAsyncResult ar) { try { byte[] buffer = (byte[])ar.AsyncState; EndPoint remoteEndPoint = new IPEndPoint(IPAddress.Any, 0); int bytesRead = serverSocket.EndReceiveFrom(ar, ref remoteEndPoint); // 反序列化字节数组为结构体 FMS receivedFms = StructConverter.BytesToStruct<FMS>(buffer); // 在UI线程更新显示(如果需要) this.Invoke(new Action(() => { // 处理接收到的结构体数据 MessageBox.Show($"接收到Type: {receivedFms.type}"); })); // 继续监听下一个数据包 serverSocket.BeginReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref remoteEndPoint, ReceiveCallback, buffer); } catch (Exception ex) { MessageBox.Show($"接收错误: {ex.Message}"); } } } }
注意事项
- 结构体的字段顺序必须严格一致,Marshal序列化依赖字段在内存中的布局顺序
- 所有
[MarshalAs]属性的参数(如SizeConst)必须和发送端完全匹配 - 枚举类型的底层类型默认是
int,保持和发送端定义一致即可,Marshal会自动处理其数值转换 - 可通过
[StructLayout(LayoutKind.Sequential, Pack = 1)]强制指定内存布局和对齐方式,减少跨平台差异
内容的提问来源于stack exchange,提问作者Erencan Tekin
相关产品推荐
相关产品推荐

