【问题标题】:How can I send multiple types of objects across Protobuf?如何跨 Protobuf 发送多种类型的对象?
【发布时间】:2011-03-04 04:22:03
【问题描述】:

我正在实现一个客户端-服务器应用程序,并且正在研究各种序列化和传输数据的方法。我开始使用 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);
    }
}

field => _mappings[field] 在寻找 63671269 时抛出 KeyNotFoundException

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

【问题讨论】:

    标签: c# serialization protocol-buffers protobuf-net


    【解决方案1】:

    这个功能实际上是内置的,虽然不是很明显。

    在这种情况下,预计您将为每种消息类型指定一个唯一编号。您正在使用的重载将它们全部作为“字段 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
    

    【讨论】:

    • 我为低估 protobuf-net 的全面性而感到羞耻!如果我想避免使用神奇的预定义字典,使用 obj.GetType().GetHashCode() 生成 field 数字是个坏主意吗?
    • @Daniel - 只要你有一些方案来减少散列冲突的slim机会......
    • @Daniel - 一夜之间实现; 使用哈希码有充分的理由:通常,数字需要是&gt;= 1(这很容易修复),但更重要的是:哈希码不应该真正被外界信任给定的应用程序域。他们可以改变;例如,字符串哈希算法在 1.1 和 2.0 之间发生了变化——并且可以合法地再次发生变化。最好使用完整类型名称的 MD5 哈希之类的东西。或者:使您的第一条消息(带有字段 1 或类似的)字段编号和类型名称之间的映射集。
    • @Marc - 我不知道哈希算法已经改变;完整类型名称的 MD5 哈希听起来确实是一个更好的解决方案。与服务器端类型映射一样,在连接时发送给客户端。但是,该方法的唯一问题是,如果客户端想要发送服务器不熟悉的对象,它将不知道要给它什么数字。 (当然,我想不出什么时候会真正发生这种情况——服务器可能比客户端更新。)
    【解决方案2】:

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


    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; }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-07-18
      • 2019-11-07
      • 2019-01-02
      • 1970-01-01
      • 2012-10-11
      • 1970-01-01
      • 1970-01-01
      • 2016-01-20
      相关资源
      最近更新 更多