如何在C#中将结构体转换为字节数组?

96

如何在C#中将一个结构体转换为字节数组?

我定义了一个如下的结构体:

public struct CIFSPacket
{
    public uint protocolIdentifier; //The value must be "0xFF+'SMB'".
    public byte command;

    public byte errorClass;
    public byte reserved;
    public ushort error;

    public byte flags;

    //Here there are 14 bytes of data which is used differently among different dialects.
    //I do want the flags2. However, so I'll try parsing them.
    public ushort flags2;

    public ushort treeId;
    public ushort processId;
    public ushort userId;
    public ushort multiplexId;

    //Trans request
    public byte wordCount;//Count of parameter words defining the data portion of the packet.
    //From here it might be undefined...

    public int parametersStartIndex;

    public ushort byteCount; //Buffer length
    public int bufferStartIndex;

    public string Buffer;
}
在我的主方法中,我创建了一个实例并为其赋值:
CIFSPacket packet = new CIFSPacket();
packet.protocolIdentifier = 0xff;
packet.command = (byte)CommandTypes.SMB_COM_NEGOTIATE;
packet.errorClass = 0xff;
packet.error = 0;
packet.flags = 0x00;
packet.flags2 = 0x0001;
packet.multiplexId = 22;
packet.wordCount = 0;
packet.byteCount = 119;

packet.Buffer = "NT LM 0.12";

现在我想通过socket发送这个数据包。为此,我需要将结构体转换为字节数组。我该怎么做?

我的完整代码如下。

static void Main(string[] args)
{

  Socket MyPing = new Socket(AddressFamily.InterNetwork,
  SocketType.Stream , ProtocolType.Unspecified ) ;


  MyPing.Connect("172.24.18.240", 139);

    //Fake an IP Address so I can send with SendTo
    IPAddress IP = new IPAddress(new byte[] { 172,24,18,240 });
    IPEndPoint IPEP = new IPEndPoint(IP, 139);

    //Local IP for Receiving
    IPEndPoint Local = new IPEndPoint(IPAddress.Any, 0);
    EndPoint EP = (EndPoint)Local;

    CIFSPacket packet = new CIFSPacket();
    packet.protocolIdentifier = 0xff;
    packet.command = (byte)CommandTypes.SMB_COM_NEGOTIATE;
    packet.errorClass = 0xff;
    packet.error = 0;
    packet.flags = 0x00;
    packet.flags2 = 0x0001;
    packet.multiplexId = 22;
    packet.wordCount = 0;
    packet.byteCount = 119;

    packet.Buffer = "NT LM 0.12";

    MyPing.SendTo(It takes byte array as parameter);
}

代码片段是什么意思?


最后一行有一个更正:MyPing.Send(它以字节数组作为参数);应该是Send而不是SendTo...... - Swapnil Gupta
嗨,Petar,我不明白你的意思... - Swapnil Gupta
3
接受一些你之前问题的答案可能是一个不错的选择。 - jnoss
1
我怀疑更具体地说明您期望的输出会有所帮助;有很多方法可以将其转换为byte[]... 我们可能可以对大部分内容进行一些假设,即您想要字段顺序、网络字节顺序和固定大小表示的字段 - 但字符串呢? - Marc Gravell
对于不包含字符串或其他非可平坦类型的情况,如果您愿意使用不安全代码,我已经在此线程上发布了一种技术,该技术涉及将结构体映射到字节数组中的字段上: https://dev59.com/c1rUa4cB1Zd3GeqPhSxR - RenniePet
显示剩余2条评论
15个回答

152

这相当容易,使用marshalling即可。

文件顶部

using System.Runtime.InteropServices

函数

byte[] getBytes(CIFSPacket str) {
    int size = Marshal.SizeOf(str);
    byte[] arr = new byte[size];

    IntPtr ptr = IntPtr.Zero;
    try
    {
        ptr = Marshal.AllocHGlobal(size);
        Marshal.StructureToPtr(str, ptr, true);
        Marshal.Copy(ptr, arr, 0, size);
    }
    finally
    {
        Marshal.FreeHGlobal(ptr);
    }
    return arr;
}

并将其转换回去:

CIFSPacket fromBytes(byte[] arr)
{
    CIFSPacket str = new CIFSPacket();

    int size = Marshal.SizeOf(str);
    IntPtr ptr = IntPtr.Zero;
    try
    {
        ptr = Marshal.AllocHGlobal(size);

        Marshal.Copy(arr, 0, ptr, size);

        str = (CIFSPacket)Marshal.PtrToStructure(ptr, str.GetType());
    }
    finally
    {
        Marshal.FreeHGlobal(ptr);
    }
    return str;
}
在你的结构中,你需要将这个放在一个字符串之前。
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 100)]
public string Buffer;

确保SizeConst足够大以容纳最大可能的字符串。

你应该阅读这篇文章: http://msdn.microsoft.com/en-us/library/4ca6d5z7.aspx


1
GetBytes将从您的结构转换为数组。FromBytes将从字节转换回您的结构。这可以从函数签名中看出。 - Vincent McNabb
1
@Swapnil 这是另一个问题,你应该单独提问。你应该考虑完成一些关于套接字的 CE 教程。只需在 Google 上搜索即可。 - Vincent McNabb
1
我已经完成了这个任务,但在运行时出现了异常,异常发生在 Marshal.StructureToPtr(str, ptr, true) 这一行。 - saeed
3
在您的fromBytes方法中,没有必要分配两次CIFSPacket。Marshal.SizeOf可以将Type作为参数轻松接受,而Marshal.PtrToStructure会分配一个新的托管对象。 - Jack Ukleja
2
请注意,在某些情况下,函数“StructureToPtr”会抛出异常。这可以通过将“false”传递给Marshal.StructureToPtr(str, ptr, false);来解决。但需要提到的是,我正在使用包装到通用函数中的这些函数... - Hi-Angel
显示剩余12条评论

36

如果你真的希望在Windows上让它运行得更快,可以使用unsafe code和CopyMemory。CopyMemory大约快5倍(例如,通过封送传输800MB的数据需要3秒钟,而通过CopyMemory只需要0.6秒钟)。这种方法限制了你只能使用实际存储在结构体blob中的数据,例如数字或固定长度的字节数组。

    [DllImport("kernel32.dll", EntryPoint = "CopyMemory", SetLastError = false)]
    private static unsafe extern void CopyMemory(void *dest, void *src, int count);

    private static unsafe byte[] Serialize(TestStruct[] index)
    {
        var buffer = new byte[Marshal.SizeOf(typeof(TestStruct)) * index.Length];
        fixed (void* d = &buffer[0])
        {
            fixed (void* s = &index[0])
            {
                CopyMemory(d, s, buffer.Length);
            }
        }

        return buffer;
    }

5
提醒那些阅读这篇回答的人注意,它并不支持跨平台(它仅使用Windows kernel32.dll)。但话说回来,它是在2014年编写的。 :) - Raid
2
加号需要结构是连续的。 - Tomer W
然而,如果在Windows上,这是否仍然更快? - Crog
是的,在Windows中,仍然是复制内存数据并获取其字节数组的最快方式。 - Cool guy

28
请注意以下这些方法:
byte [] StructureToByteArray(object obj)
{
    int len = Marshal.SizeOf(obj);

    byte [] arr = new byte[len];

    IntPtr ptr = Marshal.AllocHGlobal(len);

    Marshal.StructureToPtr(obj, ptr, true);

    Marshal.Copy(ptr, arr, 0, len);

    Marshal.FreeHGlobal(ptr);

    return arr;
}

void ByteArrayToStructure(byte [] bytearray, ref object obj)
{
    int len = Marshal.SizeOf(obj);

    IntPtr i = Marshal.AllocHGlobal(len);

    Marshal.Copy(bytearray,0, i,len);

    obj = Marshal.PtrToStructure(i, obj.GetType());

    Marshal.FreeHGlobal(i);
}

这是我通过谷歌搜索发现的另一个帖子的无耻复制!

更新:有关更多详细信息,请查看源代码


我已经使用Marshalling将结构体转换为字节数组,现在我该如何检查是否从套接字中获取到响应?如何进行检查? - Swapnil Gupta
@Alastair,我漏掉了!!谢谢你指出来.. 我已经更新了我的答案。 - Abdel Raoof Olakara
2
此选项取决于平台 - 请注意大端和小端以及32位/64位。 - x77
@Abdel,-1已经消失了 :) - Alastair Pitts
执行Alloc,将中间部分包装在try中,然后将Free放在finally中是否有意义?虽然似乎不太可能出现问题,但如果确实出现了,内存是否会被释放? - Casey

22

这是一个修改版的 Vicent 代码,减少了一次内存分配:

public static byte[] GetBytes<T>(T str)
{
    int size = Marshal.SizeOf(str);

    byte[] arr = new byte[size];

    GCHandle h = default(GCHandle);

    try
    {
        h = GCHandle.Alloc(arr, GCHandleType.Pinned);

        Marshal.StructureToPtr<T>(str, h.AddrOfPinnedObject(), false);
    }
    finally
    {
        if (h.IsAllocated)
        {
            h.Free();
        }
    }

    return arr;
}

public static T FromBytes<T>(byte[] arr) where T : struct
{
    T str = default(T);

    GCHandle h = default(GCHandle);

    try
    {
        h = GCHandle.Alloc(arr, GCHandleType.Pinned);

        str = Marshal.PtrToStructure<T>(h.AddrOfPinnedObject());

    }
    finally
    {
        if (h.IsAllocated)
        {
            h.Free();
        }
    }

    return str;
}

我使用GCHandle来“固定”内存,然后直接使用h.AddrOfPinnedObject()获取其地址。

应该移除 where T : struct,否则它会抱怨传递的 T 不是一个 非空类型 - codenamezero
如果结构体具有非平凡数据,例如数组,则“GCHandle.Alloc”将失败。 - joe
@joe 你说得对。这段代码是为给定的结构编写的,该结构仅包含可平坦化类型和 string - xanatos

7

我知道这已经很晚了,但是使用 C# 7.3,您可以为不受管理的结构体或任何其他未受管理的内容(例如 int、bool 等)执行此操作:

public static unsafe byte[] ConvertToBytes<T>(T value) where T : unmanaged {
        byte* pointer = (byte*)&value;

        byte[] bytes = new byte[sizeof(T)];
        for (int i = 0; i < sizeof(T); i++) {
            bytes[i] = pointer[i];
        }

        return bytes;
    }

然后像这样使用:

struct MyStruct {
        public int Value1;
        public int Value2;
        //.. blah blah blah
    }

    byte[] bytes = ConvertToBytes(new MyStruct());

5

由于主要答案使用了CIFSPacket类型,而这种类型在C#中不可用(或已不再可用),因此我编写了正确的方法:

    static byte[] getBytes(object str)
    {
        int size = Marshal.SizeOf(str);
        byte[] arr = new byte[size];
        IntPtr ptr = Marshal.AllocHGlobal(size);

        Marshal.StructureToPtr(str, ptr, true);
        Marshal.Copy(ptr, arr, 0, size);
        Marshal.FreeHGlobal(ptr);

        return arr;
    }

    static T fromBytes<T>(byte[] arr)
    {
        T str = default(T);

        int size = Marshal.SizeOf(str);
        IntPtr ptr = Marshal.AllocHGlobal(size);

        Marshal.Copy(arr, 0, ptr, size);

        str = (T)Marshal.PtrToStructure(ptr, str.GetType());
        Marshal.FreeHGlobal(ptr);

        return str;
    }

测试过,它们可以正常工作。


2
你可以使用Marshal(StructureToPtr,ptrToStructure)和Marshal.copy,但这取决于平台。
序列化包括自定义序列化函数。
public virtual void GetObjectData(SerializationInfo info, StreamingContext context)
Protected Sub New(ByVal info As SerializationInfo, ByVal context As StreamingContext) 

SerializationInfo包含序列化每个成员的函数。


BinaryWriter和BinaryReader还包含将数据保存/加载到字节数组(流)中的方法。

请注意,您可以从字节数组创建MemoryStream,也可以从MemoryStream创建字节数组。

您可以在结构体中创建一个Save方法和一个New方法:

   Save(Bw as BinaryWriter)
   New (Br as BinaryReader)

然后您可以选择会员进行保存/加载到流 -> 字节数组。

2
几乎所有的答案都使用Marshal.StructureToPtr,这对于P/Invoke可能是好的,但它非常慢,甚至不总是表示实际的原始内容。@Varscott128的答案更好,但它也包含了显式的字节复制,这是不必要的。
对于无托管引用的非托管结构体(structs without managed references),你只需要重新解释分配的结果数组,所以一个简单的赋值就可以解决问题(即使对于巨大的结构体也适用):
.NET (Core)解决方案:
如果您可以使用Unsafe类,则解决方案非常简单。unsafe修饰符仅由于sizeof(T)而需要。
public static unsafe byte[] SerializeValueType<T>(in T value) where T : unmanaged
{
    byte[] result = new byte[sizeof(T)];
    Unsafe.As<byte, T>(ref result[0]) = value;
    return result;
}

// Note: Validation is omitted for simplicity
public static T DeserializeValueType<T>(byte[] data) where T : unmanaged
    => return Unsafe.As<byte, T>(ref data[0]);

.NET框架/标准解决方案:

public static unsafe byte[] SerializeValueType<T>(in T value) where T : unmanaged
{
    byte[] result = new byte[sizeof(T)];
    fixed (byte* dst = result)
        *(T*)dst = value;
    return result;
}

// Note: Validation is omitted for simplicity
public static unsafe T DeserializeValueType<T>(byte[] data) where T : unmanaged
{
    fixed (byte* src = data)
        return *(T*)src;
}

可以在此处查看完整的带验证代码。

备注:

OP的示例包含一个字符串,它是一个引用类型,因此上述解决方案不能使用。如果由于某种原因无法使用泛型方法,则事情开始变得更加复杂,特别是对于.NET Framework(但非泛型大小计算在Core平台上也是痛苦)。如果性能不重要,则可以返回到其他答案中建议的Marshal.SizeOfStructureToPtr,或者随意使用我链接上述示例的BinarySerializer.SerializeValueType方法,它来自我的NuGet)。


1
这可以非常简单地完成。
使用[StructLayout(LayoutKind.Explicit)]明确定义你的结构体。
int size = list.GetLength(0);
IntPtr addr = Marshal.AllocHGlobal(size * sizeof(DataStruct));
DataStruct *ptrBuffer = (DataStruct*)addr;
foreach (DataStruct ds in list)
{
    *ptrBuffer = ds;
    ptrBuffer += 1;
}

这段代码只能在不安全的上下文中书写。当你使用完addr之后,必须释放它。

Marshal.FreeHGlobal(addr);

在对固定大小的集合进行显式有序操作时,应该使用数组和for循环。使用数组是因为它是固定大小的;使用for循环是因为foreach不能保证按照您期望的顺序执行,除非您知道列表类型和其枚举器的基本实现,并且它永远不会改变。例如,可以定义枚举器从末尾开始向后移动。 - user2026256

1
我想到了一种不同的方法,可以将任何struct转换为字节数组,而无需费心去修复长度,但生成的字节数组会有一些额外开销。
这里是一个示例struct:
[StructLayout(LayoutKind.Sequential)]
public class HelloWorld
{
    public MyEnum enumvalue;
    public string reqtimestamp;
    public string resptimestamp;
    public string message;
    public byte[] rawresp;
}

正如您所看到的,所有这些结构都需要添加固定长度属性。这可能会占用比实际需要更多的空间。请注意,LayoutKind.Sequential是必需的,因为我们希望反射始终在拉取FieldInfo时给出相同的顺序。我的灵感来自于TLV类型-长度-值。让我们来看看代码:

public static byte[] StructToByteArray<T>(T obj)
{
    using (MemoryStream ms = new MemoryStream())
    {
        FieldInfo[] infos = typeof(T).GetFields(BindingFlags.Public | BindingFlags.Instance);
        foreach (FieldInfo info in infos)
        {
            BinaryFormatter bf = new BinaryFormatter();
            using (MemoryStream inms = new MemoryStream()) {

                bf.Serialize(inms, info.GetValue(obj));
                byte[] ba = inms.ToArray();
                // for length
                ms.Write(BitConverter.GetBytes(ba.Length), 0, sizeof(int));

                // for value
                ms.Write(ba, 0, ba.Length);
            }
        }

        return ms.ToArray();
    }
}

上述函数简单地使用BinaryFormatter将未知大小的原始object进行序列化,并在输出的MemoryStream中跟踪并存储其大小。请保留HTML标签。
public static void ByteArrayToStruct<T>(byte[] data, out T output)
{
    output = (T) Activator.CreateInstance(typeof(T), null);
    using (MemoryStream ms = new MemoryStream(data))
    {
        byte[] ba = null;
        FieldInfo[] infos = typeof(T).GetFields(BindingFlags.Public | BindingFlags.Instance);
        foreach (FieldInfo info in infos)
        {
            // for length
            ba = new byte[sizeof(int)];
            ms.Read(ba, 0, sizeof(int));

            // for value
            int sz = BitConverter.ToInt32(ba, 0);
            ba = new byte[sz];
            ms.Read(ba, 0, sz);

            BinaryFormatter bf = new BinaryFormatter();
            using (MemoryStream inms = new MemoryStream(ba))
            {
                info.SetValue(output, bf.Deserialize(inms));
            }
        }
    }
}

当我们想要将其转换回原始的struct时,我们只需读取长度并直接将其转储回BinaryFormatter中,然后再将其转储回struct中。这两个函数是通用的,适用于任何struct,我已经在我的C#项目中测试了上述代码,其中我有一个服务器和一个客户端,通过NamedPipeStream连接和通信,并将我的struct作为字节数组从一个转发到另一个并将其转换回来。我相信我的方法可能更好,因为它不会在struct本身上固定长度,唯一的开销只是每个字段中的一个int。由BinaryFormatter生成的字节数组中还有一些微小的开销,但除此之外,没有太多开销。

6
通常情况下,当人们尝试处理这类问题时,也会关注序列化性能。理论上,任何结构体数组都可以被重新解释为字节数组,而无需涉及昂贵的序列化和复制操作。 - Tanveer Badar

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接