C#通过套接字发送结构对象

6
我已经阅读了关于C#客户端/服务器编程的一些内容。我对此过程有足够的了解,现在我想问以下问题:
如何通过TCP/IP传输结构体对象而不仅仅是字符串?
我的应用程序是一个带有聊天功能的网络游戏。因此,我想使用一个数据结构或类结构来传输两个字段:i.数据包类型 ii.数据包类型的数据。
在应用程序执行期间需要时,我会传输这些数据,并在接收端解码数据对象并将其放置在相应位置。
我不是在寻找代码,只是想要一些想法和搜索语句,以便我能更好地理解。
我已经阅读了有关序列化/反序列化的文章,那是正确的方法吗?
谢谢。
我已经查看了相关主题的帖子,但仍然需要进一步的指导。

我刚刚读到以下内容:二进制格式化程序:二进制格式化程序提供二进制编码,用于紧凑的序列化,无论是用于存储还是用于基于套接字的网络流。当数据需要通过防火墙传递时,BinaryFormatter类通常不合适。
XML序列化是否适合通过防火墙?
- iTEgg
6个回答

7
最终是的:你在谈论序列化。这可以采用许多形式,特别是在.NET中,但最终你需要在以下选项之间进行选择:
- 文本 vs 二进制;直接二进制通常比文本更小,因为它通常涉及较少的解析等操作;文本(xml、json等)通常以UTF8格式表示在流中(尽管任何编码都可能)。它们基本上是人类可读的,尽管更冗长,但通常可以很好地压缩。 - 契约 vs 元数据;基于契约的序列化程序专注于表示数据——假定管道的另一端了解结构,但不假定它们共享实现。这有局限性,因为您不能突然引入一些完全意外的子类,但它使其独立于平台。相比之下,基于元数据的序列化程序在流上发送类型信息(即,“这是一个My.Namespace.FooBar实例”)。这使得它真正容易使用,但很少在不同平台之间起作用(通常也不在不同版本之间起作用),并且所有这些类型信息可能非常冗长。 - 手动 vs 自动;事实证明:手动序列化程序通常在带宽方面效率最高,因为您可以手动自定义流 - 但需要大量的工作量,并且您需要了解序列化程序。自动序列化程序更适合通用用途(实际上:大多数情况下)。除非别无选择,否则避免手动序列化程序。自动序列化程序处理有关不同类型数据等复杂性。
手动序列化程序方法包括(仅提到“序列化程序”关键字):TextWriterXmlWriterIXmlSerializableBinaryWriterISerializable。你不想这样做...
更多关注自动序列化程序:
               | Contract               | Metadata
===============+========================+===========================
  Text         | XmlSerializer          | SoapFormatter
               | DataContractSerializer | NetDataContractSerializer
               | Json.NET               |
---------------+------------------------+---------------------------
  Binary       | protobuf-net           | BinaryFormatter

如果你在谈论原始流,我更喜欢基于二进制协议的序列化器,但是,我写了protobuf-net,所以我可能有偏见 ;-p

与常见的RPC堆栈进行比较:

  • "remoting"使用BinaryFormatter
  • "asmx" web服务(包括WSE*)使用XmlSerializer
  • WCF可以使用许多种,最常见的是DataContractSerializerNetDataContractSerializer,有时也会使用XmlSerializer(它还可以配置为使用例如protobuf-net)

我可以很高兴地编写一个使用protobuf-net在流上表示不同类型消息的示例,但是一个简单的使用protobuf-net处理套接字的示例在一个示例项目中(实际上在这里


6
如果您不需要序列化的丰富性 - 如果您只想将结构写入字节数组,请考虑使用Marshal类。
例如,考虑在C#中使用tar应用程序。 tar格式基于512字节块,并且系列中的第一个块具有常规结构。 理想情况下,应用程序希望直接从磁盘文件中使用blitt数据,直接进入结构Marshal.PtrToStructure方法可以实现这一点。 这是结构体。
    [StructLayout(LayoutKind.Sequential, Size=512)]
    internal struct HeaderBlock
    {
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 100)]
        public byte[]   name;    // name of file. 

        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
        public byte[]   mode;    // file mode

        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
        public byte[]   uid;     // owner user ID

        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
        public byte[]   gid;     // owner group ID

        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 12)]
        public byte[]   size;    // length of file in bytes

        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 12)]
        public byte[]   mtime;   // modify time of file

        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
        public byte[]   chksum;  // checksum for header

        // ... more like that... up to 512 bytes. 

那么这里有一个通用的类,用于执行blitting操作。
internal class RawSerializer<T>
{
    public T RawDeserialize( byte[] rawData )
    {
        return RawDeserialize( rawData , 0 );
    }    

    public T RawDeserialize( byte[] rawData , int position )
    {
        int rawsize = Marshal.SizeOf( typeof(T) );
        if( rawsize > rawData.Length )
            return default(T);

        IntPtr buffer = Marshal.AllocHGlobal( rawsize );
        Marshal.Copy( rawData, position, buffer, rawsize );
        T obj = (T) Marshal.PtrToStructure( buffer, typeof(T) );
        Marshal.FreeHGlobal( buffer );
        return obj;
    }

    public byte[] RawSerialize( T item )
    {
        int rawSize = Marshal.SizeOf( typeof(T) );
        IntPtr buffer = Marshal.AllocHGlobal( rawSize );
        Marshal.StructureToPtr( item, buffer, false );
        byte[] rawData = new byte[ rawSize ];
        Marshal.Copy( buffer, rawData, 0, rawSize );
        Marshal.FreeHGlobal( buffer );
        return rawData;
    }
}

您可以将该类与 任何 结构一起使用。 您必须使用 LayoutKind.Sequential 并限制自己使用 blittable 类型(基本上是原始类型和相同的数组)来使用此方法。 它在代码、性能和内存方面都快速高效,但在使用方式上有些受限。
一旦您拥有字节数组,就可以通过 NetworkStream 等传输它,然后在另一端使用相同的类进行反序列化。

谢谢!非常适合ICD实现。在方法上具有通用参数而不是类会更好一些,我个人认为,因为编译器可以在大多数情况下推断T的序列化。 - Ohad Schneider

5

您可以基于Socket创建NetworkStream,并使用任何流机制来传输数据。这将转化为如何从/向流中读取/写入结构的问题。

您可以使用Serialization,也可以使用BinaryWriter/BinaryReader。对于小型结构(如您所描述),我会编写一些自定义方法:

var netStream = new NetworkStream(clientSocket, true);
var writer = new BinaryWriter(netStream);

writer.Write(data.Value1);
writer.Write(data.Value2);

对于更大的结构体,我会考虑使用Cheeso的编排选项。

编组只是将结构转换为字节数组。我假设他想要通过网络流传输字节,就像你展示的那样。 - Cheeso
是的,我同意你的答案。但为2个值分配未受管控的缓冲区是一个很大的开销。 - H H

5

序列化是最简单的方法,因为系统直接支持它。然而,对于大型和复杂对象存在一些性能问题。在您的情况下,似乎序列化是最好的选择。如果您想要更低级别的操作,可以尝试使用BinaryWriter/BinaryReader,这允许您自己完成工作。


3
也许您希望详细介绍二进制序列化与其他序列化的区别,以解释为什么二进制序列化会更高效。 - BlueTrin
3
归根结底,所有序列化都是“二进制”的——不同之处在于如何实现。 - Marc Gravell

1

如果通信机制的两端都是C#并且可以加载包含消息类型的相同程序集,则.NET的二进制序列化可能是最快的开箱即用选项。如果您的结构非常简单,那么自己编写序列化也可能是可以的。只需在类中定义数据结构以及将其转换为字符串和从字符串转换回来的方法。


1

使用对象序列化是正确的方法。

有一件事情我认为还没有被提到,那就是二进制序列化器通常会创建较少的字节以发送到套接字,但是如果您使用 XML 或 JSON 序列化器,然后在将其发送到网络流之前使用 CompressionStream(GZipStream?)压缩结果,则根据对象中数据类型的不同,您可能会获得更小的大小(当您有大量字符串时,这种方法效果最佳)。

这将需要更多的 CPU 时间来发送和读取消息,因此如果您需要降低带宽要求,则需要进行权衡。


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