如何通过 Protobuf 发送多种类型的对象?

发布于 2024-09-05 09:46:38 字数 2927 浏览 1 评论 0原文

我正在实现一个客户端-服务器应用程序,并正在研究序列化和传输数据的各种方法。我开始使用 Xml 序列化器,它工作得相当好,但生成数据很慢,并且生成大型对象,特别是当它们需要通过网络发送时。所以我开始研究 Protobuf 和 protobuf-net。

我的问题在于 protobuf 不发送类型信息。使用 Xml 序列化器,我能够构建一个包装器,它可以通过同一流发送和接收任何各种(可序列化)对象,因为序列化到 Xml 中的对象包含对象的类型名称。

ObjectSocket socket = new ObjectSocket();
socket.AddTypeHandler(typeof(string));  // Tells the socket the types
socket.AddTypeHandler(typeof(int));     // of objects we will want
socket.AddTypeHandler(typeof(bool));    // to send and receive.
socket.AddTypeHandler(typeof(Person));  // When it gets data, it looks for
socket.AddTypeHandler(typeof(Address)); // these types in the Xml, then uses
                                        // the appropriate serializer.

socket.Connect(_host, _port);
socket.Send(new Person() { ... });
socket.Send(new Address() { ... });
...
Object o = socket.Read();
Type oType = o.GetType();

if (oType == typeof(Person))
    HandlePerson(o as Person);
else if (oType == typeof(Address))
    HandleAddress(o as Address);
...

我已经考虑了一些解决方案,包括创建一个主“状态”类型类,这是通过我的套接字发送的唯一对象类型。不过,这偏离了我使用 Xml 序列化程序开发的功能,因此我想避免这个方向。

第二种选择是将 protobuf 对象包装在某种类型的包装器中,该包装器定义了对象的类型。 (这个包装器还包括数据包 ID 和目的地等信息。)使用 protobuf-net 序列化对象,然后将该流粘贴在 Xml 标记之间似乎很愚蠢,但我已经考虑过。有没有一种简单的方法可以从 protobuf 或 protobuf-net 中获取此功能?


我提出了第三种解决方案,并将其发布在下面,但是如果您有更好的解决方案,也请发布!


有关字段边界错误的信息(使用 System.String):

哈希:

protected static int ComputeTypeField(Type type) // System.String
{
    byte[] data = ASCIIEncoding.ASCII.GetBytes(type.FullName);
    MD5CryptoServiceProvider md5 = new MD5CryptoServiceProvider();
    return Math.Abs(BitConverter.ToInt32(md5.ComputeHash(data), 0));
}

序列化:

using (MemoryStream stream = new MemoryStream())
{
    Serializer.NonGeneric.SerializeWithLengthPrefix
        (stream, o, PrefixStyle.Base128, field);  // field = 600542181
    byte[] data = stream.ToArray();
    _pipe.Write(data, 0, data.Length);
}

反序列化:

using (MemoryStream stream = new MemoryStream(_buffer.Peek()))
{
    lock (_mapLock)
    {
        success = Serializer.NonGeneric.TryDeserializeWithLengthPrefix
            (stream, PrefixStyle.Base128, field => _mappings[field], out o);
    }
    if (success)
        _buffer.Clear((int)stream.Position);
    else
    {
        int len;
        if (Serializer.TryReadLengthPrefix(stream, PrefixStyle.Base128, out len))
            _buffer.Clear(len);
    }
}

字段 => _mappings[field] 在查找 63671269 时抛出 KeyNotFoundException

如果我在哈希函数中将 ToInt32 替换为 ToInt16,则字段值将设置为 29723 并且可以正常工作。如果我将 System.String 的字段显式定义为 1,它也可以工作。将字段显式定义为 600542181 与使用哈希函数定义它具有相同的效果。被序列化的字符串的值不会改变结果。

I'm implementing a client-server application, and am looking into various ways to serialize and transmit data. I began working with Xml Serializers, which worked rather well, but generate data slowly, and make large objects, especially when they need to be sent over the net. So I started looking into Protobuf, and protobuf-net.

My problem lies in the fact that protobuf doesn't sent type information with it. With Xml Serializers, I was able to build a wrapper which would send and receive any various (serializable) object over the same stream, since object serialized into Xml contain the type name of the object.

ObjectSocket socket = new ObjectSocket();
socket.AddTypeHandler(typeof(string));  // Tells the socket the types
socket.AddTypeHandler(typeof(int));     // of objects we will want
socket.AddTypeHandler(typeof(bool));    // to send and receive.
socket.AddTypeHandler(typeof(Person));  // When it gets data, it looks for
socket.AddTypeHandler(typeof(Address)); // these types in the Xml, then uses
                                        // the appropriate serializer.

socket.Connect(_host, _port);
socket.Send(new Person() { ... });
socket.Send(new Address() { ... });
...
Object o = socket.Read();
Type oType = o.GetType();

if (oType == typeof(Person))
    HandlePerson(o as Person);
else if (oType == typeof(Address))
    HandleAddress(o as Address);
...

I've considered a few solutions to this, including creating a master "state" type class, which is the only type of object sent over my socket. This moves away from the functionality I've worked out with Xml Serializers, though, so I'd like to avoid that direction.

The second option would be to wrap protobuf objects in some type of wrapper, which defines the type of object. (This wrapper would also include information such as packet ID, and destination.) It seems silly to use protobuf-net to serialize an object, then stick that stream between Xml tags, but I've considered it. Is there an easy way to get this functionality out of protobuf or protobuf-net?


I've come up with a third solution, and posted it below, but if you have a better one, please post it too!


Information on field bounds bug (using System.String):

Hashing:

protected static int ComputeTypeField(Type type) // System.String
{
    byte[] data = ASCIIEncoding.ASCII.GetBytes(type.FullName);
    MD5CryptoServiceProvider md5 = new MD5CryptoServiceProvider();
    return Math.Abs(BitConverter.ToInt32(md5.ComputeHash(data), 0));
}

Serialization:

using (MemoryStream stream = new MemoryStream())
{
    Serializer.NonGeneric.SerializeWithLengthPrefix
        (stream, o, PrefixStyle.Base128, field);  // field = 600542181
    byte[] data = stream.ToArray();
    _pipe.Write(data, 0, data.Length);
}

Deserializaion:

using (MemoryStream stream = new MemoryStream(_buffer.Peek()))
{
    lock (_mapLock)
    {
        success = Serializer.NonGeneric.TryDeserializeWithLengthPrefix
            (stream, PrefixStyle.Base128, field => _mappings[field], out o);
    }
    if (success)
        _buffer.Clear((int)stream.Position);
    else
    {
        int len;
        if (Serializer.TryReadLengthPrefix(stream, PrefixStyle.Base128, out len))
            _buffer.Clear(len);
    }
}

field => _mappings[field] throws a KeyNotFoundException while looking for 63671269.

If I replace ToInt32 with ToInt16 in the hash function, the field value is set to 29723 and it works. It also works if I explicitly define System.String's field to 1. Explicitly defining the field to 600542181 has the same effect as using the hash function to define it. The value of the string being serialized does not change the outcome.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

烟沫凡尘 2024-09-12 09:46:38

该功能实际上是内置的,尽管并不明显。

在这种情况下,预计您将为每种消息类型指定一个唯一的编号。您使用的重载将它们全部作为“字段 1”传递,但是有一个重载可以让您包含这些额外的标头信息(不过,决定如何将数字映射到类型仍然是调用代码的工作)。然后,您可以指定不同的类型,因为不同的字段是流(注意:这只适用于 base-128 前缀样式)。

我需要仔细检查,但目的是像下面这样的东西应该可以工作:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using ProtoBuf;
static class Program
{
    static void Main()
    {
        using (MemoryStream ms = new MemoryStream())
        {
            WriteNext(ms, 123);
            WriteNext(ms, new Person { Name = "Fred" });
            WriteNext(ms, "abc");

            ms.Position = 0;

            while (ReadNext(ms)) { }            
        }
    }
    // *** you need some mechanism to map types to fields
    static readonly IDictionary<int, Type> typeLookup = new Dictionary<int, Type>
    {
        {1, typeof(int)}, {2, typeof(Person)}, {3, typeof(string)}
    };
    static void WriteNext(Stream stream, object obj) {
        Type type = obj.GetType();
        int field = typeLookup.Single(pair => pair.Value == type).Key;
        Serializer.NonGeneric.SerializeWithLengthPrefix(stream, obj, PrefixStyle.Base128, field);
    }
    static bool ReadNext(Stream stream)
    {
        object obj;
        if (Serializer.NonGeneric.TryDeserializeWithLengthPrefix(stream, PrefixStyle.Base128, field => typeLookup[field], out obj))
        {
            Console.WriteLine(obj);
            return true;
        }
        return false;
    }
}
[ProtoContract] class Person {
    [ProtoMember(1)]public string Name { get; set; }
    public override string ToString() { return "Person: " + Name; }
}

请注意,这个当前在 v2 版本中工作(因为“WithLengthPrefix”代码不完整),但我会去 v1 上测试它。如果它有效,我会将上述所有场景添加到测试套件中,以确保它在 v2 中确实工作。

编辑:

是的,它在“v1”上运行良好,输出:

123
Person: Fred
abc

This functionality is actually built in, albeit not obviously.

In this scenario, it is anticipated that you would designate a unique number per message type. The overload you are using passes them all in as "field 1", but there is an overload that lets you include this extra header information (it is still the job of the calling code to decide how to map numbers to types, though). You can then specify different types as different fields is the stream (note: this only works with the base-128 prefix style).

I'll need to double check, but the intention is that something like the following should work:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using ProtoBuf;
static class Program
{
    static void Main()
    {
        using (MemoryStream ms = new MemoryStream())
        {
            WriteNext(ms, 123);
            WriteNext(ms, new Person { Name = "Fred" });
            WriteNext(ms, "abc");

            ms.Position = 0;

            while (ReadNext(ms)) { }            
        }
    }
    // *** you need some mechanism to map types to fields
    static readonly IDictionary<int, Type> typeLookup = new Dictionary<int, Type>
    {
        {1, typeof(int)}, {2, typeof(Person)}, {3, typeof(string)}
    };
    static void WriteNext(Stream stream, object obj) {
        Type type = obj.GetType();
        int field = typeLookup.Single(pair => pair.Value == type).Key;
        Serializer.NonGeneric.SerializeWithLengthPrefix(stream, obj, PrefixStyle.Base128, field);
    }
    static bool ReadNext(Stream stream)
    {
        object obj;
        if (Serializer.NonGeneric.TryDeserializeWithLengthPrefix(stream, PrefixStyle.Base128, field => typeLookup[field], out obj))
        {
            Console.WriteLine(obj);
            return true;
        }
        return false;
    }
}
[ProtoContract] class Person {
    [ProtoMember(1)]public string Name { get; set; }
    public override string ToString() { return "Person: " + Name; }
}

Note that this doesn't currently work in the v2 build (since the "WithLengthPrefix" code is incomplete), but I'll go and test it on v1. If it works, I'll all the above scenario to the test suite to ensure it does work in v2.

Edit:

yes, it does work fine on "v1", with output:

123
Person: Fred
abc
肥爪爪 2024-09-12 09:46:38

我提出了另一个解决方案,但我决定将其作为答案,而不是放在问题中,因为这对我来说更有意义。在我看来,它非常丑陋,并且我已被警告不要使用反射,因此请对其进行评论或提供更好的答案(如果有)。谢谢!


class Program
{
    static void Main(string[] args)
    {
        Person person = new Person
        {
            Id = 12345,
            Name = "Fred",
            Address = new Address
            {
                Line1 = "Flat 1",
                Line2 = "The Meadows"
            }
        };
        object value;
        using (Stream stream = new MemoryStream())
        {
            Send<Person>(stream, person);
            stream.Position = 0;
            value = Read(stream);
            person = value as Person;
        }
    }

    static void Send<T>(Stream stream, T value)
    {
        Header header = new Header()
        {
            Guid = Guid.NewGuid(),
            Type = typeof(T)
        };
        Serializer.SerializeWithLengthPrefix<Header>(stream, header, PrefixStyle.Base128);
        Serializer.SerializeWithLengthPrefix<T>(stream, value, PrefixStyle.Base128);
    }

    static object Read(Stream stream)
    {

        Header header;
        header = Serializer.DeserializeWithLengthPrefix<Header>
            (stream, PrefixStyle.Base128);
        MethodInfo m = typeof(Serializer).GetMethod("DeserializeWithLengthPrefix",
            new Type[] {typeof(Stream), typeof(PrefixStyle)}).MakeGenericMethod(header.Type);
        Object value = m.Invoke(null, new object[] {stream, PrefixStyle.Base128} );
        return value;
    }
}

[ProtoContract]
class Header
{
    public Header() { }

    [ProtoMember(1, IsRequired = true)]
    public Guid Guid { get; set; }

    [ProtoIgnore]
    public Type Type { get; set; }
    [ProtoMember(2, IsRequired = true)]
    public string TypeName
    {
        get { return this.Type.FullName; }
        set { this.Type = Type.GetType(value); }
    }
}

[ProtoContract]
class Person {
    [ProtoMember(1)]
    public int Id { get; set; }
    [ProtoMember(2)]
    public string Name { get; set; }
    [ProtoMember(3)]
    public Address Address { get; set; }
}

[ProtoContract]
class Address {
    [ProtoMember(1)]
    public string Line1 { get; set; }
    [ProtoMember(2)]
    public string Line2 { get; set; }
}

I've come up with another solution, but I decided to put it as an answer, instead of in the question, because that makes more sense to me. It's pretty ugly, in my opinion, and I've been warned against using reflection, so please comment on it or provide better answers if you have them. Thanks!


class Program
{
    static void Main(string[] args)
    {
        Person person = new Person
        {
            Id = 12345,
            Name = "Fred",
            Address = new Address
            {
                Line1 = "Flat 1",
                Line2 = "The Meadows"
            }
        };
        object value;
        using (Stream stream = new MemoryStream())
        {
            Send<Person>(stream, person);
            stream.Position = 0;
            value = Read(stream);
            person = value as Person;
        }
    }

    static void Send<T>(Stream stream, T value)
    {
        Header header = new Header()
        {
            Guid = Guid.NewGuid(),
            Type = typeof(T)
        };
        Serializer.SerializeWithLengthPrefix<Header>(stream, header, PrefixStyle.Base128);
        Serializer.SerializeWithLengthPrefix<T>(stream, value, PrefixStyle.Base128);
    }

    static object Read(Stream stream)
    {

        Header header;
        header = Serializer.DeserializeWithLengthPrefix<Header>
            (stream, PrefixStyle.Base128);
        MethodInfo m = typeof(Serializer).GetMethod("DeserializeWithLengthPrefix",
            new Type[] {typeof(Stream), typeof(PrefixStyle)}).MakeGenericMethod(header.Type);
        Object value = m.Invoke(null, new object[] {stream, PrefixStyle.Base128} );
        return value;
    }
}

[ProtoContract]
class Header
{
    public Header() { }

    [ProtoMember(1, IsRequired = true)]
    public Guid Guid { get; set; }

    [ProtoIgnore]
    public Type Type { get; set; }
    [ProtoMember(2, IsRequired = true)]
    public string TypeName
    {
        get { return this.Type.FullName; }
        set { this.Type = Type.GetType(value); }
    }
}

[ProtoContract]
class Person {
    [ProtoMember(1)]
    public int Id { get; set; }
    [ProtoMember(2)]
    public string Name { get; set; }
    [ProtoMember(3)]
    public Address Address { get; set; }
}

[ProtoContract]
class Address {
    [ProtoMember(1)]
    public string Line1 { get; set; }
    [ProtoMember(2)]
    public string Line2 { get; set; }
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文