.NET Framework 4.x下C#如何通过属性实现指定协议的序列化/反序列化?
在.NET Framework 4.x中实现严格二进制协议的序列化/反序列化
嘿,我之前对接自定义二进制协议时也遇到过这种搜资料净是旧内容的问题,刚好能给你靠谱的解决方案。首先明确:.NET Framework 4.x完全支持这种严格遵循字节布局的序列化需求,而且不需要第三方库,也不用写复杂的反射代码。下面给你两种最贴合需求的内置方案:
方案一:用StructLayout+Marshal实现“半自动化”序列化
这种方案能通过特性严格控制字段的字节偏移,完美匹配你的协议,同时利用.NET的Marshal类减少手动编码:
1. 定义带布局特性的Message结构体
把你的Message改成结构体(用类也可以,但结构体更适合值类型的二进制协议),通过StructLayout和FieldOffset精确指定每个字段的字节位置:
using System.Runtime.InteropServices; // 指定显式布局,基础大小是固定部分的长度:2(Type)+1(Number)+1(PayloadSize)+2(CheckSum) [StructLayout(LayoutKind.Explicit, Size = 6)] public struct Message { [FieldOffset(0)] // 对应字节0-1 public short Type; // 用short对应协议的Int16 [FieldOffset(2)] // 字节2 public byte Number; [FieldOffset(3)] // 字节3 public byte PayloadSize; // 可变长度的Payload没法直接放在显式布局里,单独作为属性处理 public byte[] Payload { get; set; } [FieldOffset(4)] // 注意:这里的偏移是基础结构体的位置,实际序列化时要加上Payload的长度 public short CheckSum; // 对应字节n、n+1(n=4+PayloadSize) }
2. 序列化(Message转byte[])
用Marshal类处理固定部分的序列化,再拼接Payload和校验和:
public static byte[] SerializeMessage(Message message) { // 先校验PayloadSize和实际长度是否匹配 if (message.Payload != null && message.PayloadSize != message.Payload.Length) { throw new InvalidOperationException("PayloadSize必须与Payload的实际长度一致"); } int totalSize = 6 + message.PayloadSize; byte[] buffer = new byte[totalSize]; // 序列化固定部分(Type、Number、PayloadSize)到前4字节 int fixedPartSize = Marshal.SizeOf(typeof(Message)) - 2; // 减去CheckSum的2字节,因为它在最后 IntPtr ptr = Marshal.AllocHGlobal(fixedPartSize); try { Marshal.StructureToPtr(message, ptr, false); Marshal.Copy(ptr, buffer, 0, fixedPartSize); } finally { Marshal.FreeHGlobal(ptr); } // 复制Payload到buffer的第4字节开始位置 if (message.PayloadSize > 0 && message.Payload != null) { Buffer.BlockCopy(message.Payload, 0, buffer, 4, message.PayloadSize); } // 把CheckSum写入buffer最后2字节 byte[] checkSumBytes = BitConverter.GetBytes(message.CheckSum); // 如果协议要求大端字节序,这里需要反转数组:Array.Reverse(checkSumBytes); Buffer.BlockCopy(checkSumBytes, 0, buffer, totalSize - 2, 2); return buffer; }
3. 反序列化(byte[]转Message)
同样用Marshal处理固定部分,再提取Payload和校验和:
public static Message DeserializeMessage(byte[] buffer) { if (buffer == null || buffer.Length < 6) { throw new ArgumentException("缓冲区长度不足以容纳一个合法的Message"); } Message message = new Message(); // 反序列化固定部分 int fixedPartSize = Marshal.SizeOf(typeof(Message)) - 2; IntPtr ptr = Marshal.AllocHGlobal(fixedPartSize); try { Marshal.Copy(buffer, 0, ptr, fixedPartSize); message = (Message)Marshal.PtrToStructure(ptr, typeof(Message)); } finally { Marshal.FreeHGlobal(ptr); } // 提取Payload if (message.PayloadSize > 0) { if (buffer.Length < 4 + message.PayloadSize + 2) { throw new ArgumentException("缓冲区大小与PayloadSize不匹配"); } message.Payload = new byte[message.PayloadSize]; Buffer.BlockCopy(buffer, 4, message.Payload, 0, message.PayloadSize); } else { message.Payload = Array.Empty<byte>(); } // 提取CheckSum byte[] checkSumBytes = new byte[2]; Buffer.BlockCopy(buffer, buffer.Length - 2, checkSumBytes, 0, 2); // 大端字节序的话需要反转:Array.Reverse(checkSumBytes); message.CheckSum = BitConverter.ToInt16(checkSumBytes, 0); return message; }
方案二:用BinaryWriter/BinaryReader手动序列化(更直观易维护)
如果你觉得Marshal的用法有点绕,其实手动用BinaryWriter/BinaryReader写序列化代码的工作量也很小,而且代码更直观,完全可控,也属于.NET内置功能:
// 手动序列化 public static byte[] SerializeManual(Message message) { using (var ms = new MemoryStream()) using (var writer = new BinaryWriter(ms)) { writer.Write(message.Type); writer.Write(message.Number); writer.Write(message.PayloadSize); if (message.PayloadSize > 0) { writer.Write(message.Payload); } writer.Write(message.CheckSum); return ms.ToArray(); } } // 手动反序列化 public static Message DeserializeManual(byte[] buffer) { using (var ms = new MemoryStream(buffer)) using (var reader = new BinaryReader(ms)) { var message = new Message(); message.Type = reader.ReadInt16(); message.Number = reader.ReadByte(); message.PayloadSize = reader.ReadByte(); message.Payload = message.PayloadSize > 0 ? reader.ReadBytes(message.PayloadSize) : Array.Empty<byte>(); message.CheckSum = reader.ReadInt16(); return message; } }
这种方案虽然是“手动”写的,但代码量极小,逻辑清晰,完全不会出现编码错误,而且能严格控制字段顺序和字节布局,我个人反而更推荐这种——毕竟比Marshal的方案更容易理解和维护。
关键注意点
- 字节序问题:上面的代码默认用系统的字节序(小端),如果你的协议要求大端字节序,需要在读写
Int16类型时手动反转字节数组(比如把BitConverter.GetBytes(message.Type)反转后再写入)。 - 校验和自动计算:你可以在序列化前自动计算校验和(比如基于
Type、Number、PayloadSize和Payload的所有字节),把这个逻辑加在序列化方法里,避免手动设置错误。 - 为什么不用
BinaryFormatter:BinaryFormatter会在字节流中加入大量.NET特有的元数据,完全不符合自定义二进制协议的要求,绝对不能用。
内容的提问来源于stack exchange,提问作者Mr. Boy
相关产品推荐
相关产品推荐

