C# pack 1 StructLayout 网络

Posted

技术标签:

【中文标题】C# pack 1 StructLayout 网络【英文标题】:C# pack 1 StructLayout networking 【发布时间】:2013-11-28 00:52:49 【问题描述】:

我正在尝试将缓冲区从服务器发送到我自己制作的客户端。它适用于 TCP 上的套接字。

我有一个需要发送的结构:

[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct loginStruct


    public string userName;
    public string password;

    public loginStruct(string userName, string password)
    
        this.userName = userName;
        this.password = password;
    

我得到了这些函数来从字节数组转换为结构体,从结构体转换为字节数组:

    public static byte[] StructToByteArray(object obj)
    
        int len = Marshal.SizeOf(obj);
        byte[] arr = new byte[len];

        IntPtr ptr = Marshal.AllocHGlobal(len);
        Marshal.StructureToPtr(obj, ptr, false);
        Marshal.Copy(ptr, arr, 0, len);

        Marshal.FreeHGlobal(ptr);
        return arr;

    
    public static void ByteArrayToStruct(byte[] buffer, ref object obj)
    
        int len = Marshal.SizeOf(obj);

        IntPtr i = Marshal.AllocHGlobal(len);
        Marshal.Copy(buffer, 0, i, len);
        obj = Marshal.PtrToStructure(i, obj.GetType());

        Marshal.FreeHGlobal(i);
    

在客户端中,我收到缓冲区,但当客户端尝试使用 ByteArrayToStruct 函数时,出现运行时错误。

【问题讨论】:

如何发送数据?您可以发布用于通过套接字发送/接收的代码吗?看来您发布的内容应该可以,错误可能是由于传输错误造成的。 什么是运行时异常? 我不会在 sizeof 调用或 PtrToStructure 调用中使用“obj”,而是引用结构本身。 obj 引用可能为空,因为它的类型是“object”而不是“loginStruct”。此外,您是否有理由不使用任何序列化程序并“以艰难的方式”做到这一点?如果二进制格式是给定的,我仍然建议使用带有内存流的 BinaryWriter/BinaryReader,而不是编组(编码/安全)speedbump。 【参考方案1】:

好的,在尝试轻松解析来自专有服务器的响应时,我也有同样的想法。这是根据您的特定情况调整的简化示例。

首先,您需要一些扩展来让这一切变得更容易。请注意,要执行此操作,您需要使用 .NET 3.5 或更高版本或查看答案here。

现在,这是我为扩展类所做的工作:

public static class EndianExtensions 
    /// <summary>
    /// Convert the bytes to a structure in host-endian format (little-endian on PCs).
    /// To use with big-endian data, reverse all of the data bytes and create a struct that is in the reverse order of the data.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="buffer">The buffer.</param>
    /// <returns></returns>
    public static T ToStructureHostEndian<T>(this byte[] buffer) where T : struct 
        GCHandle handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
        T stuff = (T) Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(T));
        handle.Free();
        return stuff;
    

    /// <summary>
    /// Converts the struct to a byte array in the endianness of this machine.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="structure">The structure.</param>
    /// <returns></returns>
    public static byte[] ToBytesHostEndian<T>(this T structure) where T : struct 
        int size = Marshal.SizeOf(structure);
        var buffer = new byte[size];
        GCHandle handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
        Marshal.StructureToPtr(structure, handle.AddrOfPinnedObject(), true);
        handle.Free();
        return buffer;
    

    public static Dictionary<string, string> GetTypeNames<T>(this T structure) where T : struct 
        var properties = typeof(T).GetFields();

        var dict = new Dictionary<string, string>();

        foreach (var fieldInfo in properties) 
            string[] words = fieldInfo.Name.Split('_');
            string friendlyName = words.Aggregate(string.Empty, (current, word) => current + string.Format("0 ", word));
            friendlyName = friendlyName.TrimEnd(' ');
            dict.Add(fieldInfo.Name, friendlyName);
        
        return dict;
    

(请注意,以上部分来自 CodeProject 上的源代码,所有这些都在CPOL license 下)

另一个需要注意的重要事项是,如果您在需要空格的地方使用 CamelCaps 和下划线,则可以使用 GetTypeNames 扩展名来获取属性的友好名称。

完成这项工作的最后一个关键部分(至少对于我的特殊情况)是在 reverse 中声明你的结构。这是因为我的服务器使用了大字节序。您可能想尝试改变或不改变字节顺序 - 以适合您的方式。

所以要实际使用它,你可以这样做:

    声明您的结构。由于我需要在传输之前将其放入大端,所以我的都是反向的:

[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct Foo 
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
    public string User_Name;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
    public string Password;
;

[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct Bar 
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
    public string Password;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
    public string User_Name;
;

现在上面假设发送和接收数据缓冲区的实际内容定义不同,因此在您的情况下,您只需定义其中一个结构。请注意,它们以相反的顺序指定;同样,这是因为我需要以大端格式传输它。

现在要做的就是创建要发送的结构:

// buffer for storing our received bytes
var barBuf = new byte[64];

// struct that we're sending
var fuz = new Foo 
    User_Name = "username",
    Password = "password"
;

// get the byte equivalent of fuz
var fuzBytes = fuz.ToBytesHostEndian().Reverse().ToArray();

// simulates sock.send() and sock.receive()
// note that this does NOT simulate receiving big-endian data!!
fuzBytes.CopyTo(barBuf, 0);

// do the conversion from bytes to struct
barBuf = barBuf.Reverse().ToArray();
// change this to ToStructureHostEndian<Bar>() if receiving big endian
var baz = barBuf.ToStructureHostEndian<Foo>();
// get the property names, friendly and non-friendly
var bazDict = baz.GetTypeNames();

// change this to typeof(Bar) if receiving big endian
var bazProps = typeof(Foo).GetFields();

// loop through the properties array
foreach (var fieldInfo in bazProps) 
    var propName = fieldInfo.Name;
    // get the friendly name and value
    var fieldName = bazDict[propName];
    var value = fieldInfo.GetValue(baz);

    // do what you want with the values
    Console.WriteLine("0,-15:1,10", fieldName, value);

需要注意的是,通过使用CopyTo() 模拟sock.Send()sock.Receive() 命令,它不会导致barBuf 中的大端数组。我已经相应地修改了代码,但如果您确实使用它来接收大端数据,只需更改代码中指示的行。

我希望这会有所帮助。我花了很多时间才弄清楚自己,因为这些信息分散在多个来源中。

【讨论】:

以上是关于C# pack 1 StructLayout 网络的主要内容,如果未能解决你的问题,请参考以下文章

C#实现注销重启和关机代码

StructLayout 和 FieldOffset 的未定义行为

C# StructLayout.Explicit 问题

c# 中的联合与 StructLayout

unity c# byte array to struct 包含数据数组

Marshal.Sizeof() 返回意外值