序列化/封送C#中的简单对象以通过网络发送,供非托管C++应用程序读取

Serializing / Marshalling simple objects in C# to send over network, for an unmanaged C++ application to read

本文关键字:读取 应用程序 C++ 网络 封送 简单 单对象 序列化      更新时间:2023-10-16

我正在编写一个C#应用程序,该应用程序需要使用预定义的消息通过网络与非托管C++应用程序进行通信。每条消息都以消息id和长度开头。

Byte 0  MessageId
Byte 1  Length

之后,每条消息都会有所不同。如设置消息的时间,将被

Byte 0      MessageId
Byte 1      Length
Byte 2..5   Time

一开始,我想我只需要创建一个基类,所有其他消息都将从中继承,它将有一个方法来"序列化"它:

public class BaseMessage
{
    public virtual byte MessageId { get; }
    public virtual byte Length { get; }
    public byte[] GetAsMessage()
    {
        ...
    }
}
public class SetTimeMessage : BaseMessage
{
    public override byte MessageId => 1;
    public override byte Length {get; private set; }
    public byte[] Time {get; private set; }
    public SetTimeMessage(byte[] time)
    {
        ...
    }
}

在C++代码中,如果我创建了一个SetTimeMessage并想通过网络发送它,就会调用一个在基类上定义的类似GetAsMessage的方法,它只需将对象的内容复制到缓冲区中。这适用于BaseMessage的所有派生类型。

我如何在C#中做类似的事情?我尝试使用[Serializable]属性和BinaryFormatter,但它返回了一个巨大的字节数组,而不仅仅是值实际包含的6个字节。

我也研究了Marshalling,但它似乎只适用于structs?如果是这样的话,似乎每个消息类型都需要做很多工作,因为结构不支持继承(我需要实现很多消息)。

我还将收到相同格式的消息,这些消息需要反序列化回对象。有人知道实现我追求的目标的巧妙方法吗?

编辑

在Spo1ler的输入之后,我对结构和编组进行了一些实验。

这样做,很容易将任何消息序列化为字节数组

public interface IMsg
{
    byte MessageId { get; }
    byte Length { get; }
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct SetTime: IMsg
{
    public byte Id { get; }
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
    public byte[] Time;
    public StartLoggerWithSampleInterval(byte time)
    {
        Id = (byte) MessageType.SetTime;
        Time = time;
    }
}

然后我有一个静态类来序列化消息:

public static class MessageSerializer
{
    public static byte[] Serialize(IMsg msg)
    {
        int size = Marshal.SizeOf(msg);
        byte[] serialized = new byte[size];
        IntPtr ptr = Marshal.AllocHGlobal(size);
        Marshal.StructureToPtr(msg, ptr, true);
        Marshal.Copy(ptr, serialized, 0, size);
        Marshal.FreeHGlobal(ptr);
        return serialized;
    }
}

起初,这似乎像我想要的那样工作。这样做会有什么问题吗?

然而,消息的反序列化将是一件很难写的事情,尽管消息中的第一个字节(MessageId)应该告诉我字节数组转换为的结构类型

BinaryFormatter以.NET稍后可以再次读取的格式保存结构,它保存类型来自哪个程序集等等,所以当您有已知的消息字节结构时,它对您的情况没有用处。

要获得一个可以像Plain Old Data那样成功序列化的结构,您需要在结构上使用StructLayoutAttribute,如

[StructLayout(LayoutKind.Explicit)]
public class Message
{
    [FieldOffset(0)] public byte MessageId;
    [FieldOffset(1)] public byte Length;
    [FieldOffset(2)] public byte[] Data;
}

StructLayoutAttribute使用任何其他参数,然后LayoutKind.Auto告诉编译器不要更改内存中结构的布局,而是使用您提供的布局。在这种情况下,Explicit布局可以手动告知字段的偏移量。

此外,您可能应该考虑更改消息格式,因为拥有不是机器字大小的FieldOffset s在性能方面可能会非常昂贵,因为计算机可以更好地处理按机器字大小排列的数据,而不是任意数量的字节。

尝试以这种方式使用基类构建层次结构是一种糟糕的方法,如果你真的想在你的消息类型中建立层次结构,最好使用接口,但你仍然需要正确地布局你的数据。

然后,您可以使用封送将数据序列化到byte数组,类似于以下

byte[] GetBytes(Message message) {
    int size = Marshal.SizeOf(message);
    byte[] arr = new byte[size];
    IntPtr ptr = Marshal.AllocHGlobal(size);
    Marshal.StructureToPtr(message, ptr, true);
    Marshal.Copy(ptr, arr, 0, size);
    Marshal.FreeHGlobal(ptr);
    return arr;
}

如果你想反序列化,那么你需要一个像这样的方法

Message FromBytes(byte[] bytes) {
    Message message = new Message();
    int size = Marshal.SizeOf(message);
    IntPtr ptr = Marshal.AllocHGlobal(size);
    Marshal.Copy(arr, 0, ptr, size);
    message = (Message)Marshal.PtrToStructure(ptr, message.GetType());
    Marshal.FreeHGlobal(ptr);
    return message;
}

如果您需要在运行时根据收到的消息来决定使用哪个类,那么这些方法中的每一个都可能变得非常大,并且它非常特定于您的层次结构,但您当然可以通过查看第一个字节,然后相应地决定如何反序列化它们来实现这一点。

但从我的角度来看,尝试基于消息的一些常见部分进行分层是一种糟糕的方法。你宁愿做一些已知类型的消息序列来区分下一条消息或类似的消息,但这取决于你自己。我在这里提供的信息应该足以将任何整型序列化为字节数组,稍后可以通过网络发送。