如何在Protobuf中发送多种类型的对象?

dlr*_*as2 8 c# serialization protocol-buffers protobuf-net

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

我的问题在于protobuf没有发送类型信息.使用Xml Serializers,我能够构建一个包装器,它将在同一个流上发送和接收任何各种(可序列化的)对象,因为序列化为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);
...
Run Code Online (Sandbox Code Playgroud)

我已经考虑了一些解决方案,包括创建一个主"状态"类型类,这是通过我的套接字发送的唯一类型的对象.然而,这远离了我用Xml Serializers开发的功能,所以我想避免这个方向.

第二种选择是将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));
}
Run Code Online (Sandbox Code Playgroud)

连载:

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);
}
Run Code Online (Sandbox Code Playgroud)

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

field => _mappings[field]扔了KeyNotFoundException一会儿寻找63671269.

如果我更换ToInt32ToInt16散列函数,该字段的值设置为29723和它的作品.如果我明确定义System.String's字段,它也有效1.明确定义字段与600542181使用散列函数定义字段具有相同的效果.被序列化的字符串的值不会改变结果.

Mar*_*ell 10

这个功能实际上是内置的,尽管不是很明显.

在这种情况下,预计您将为每种消息类型指定一个唯一的编号.您正在使用的重载将它们全部作为"字段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; }
}
Run Code Online (Sandbox Code Playgroud)

请注意,这目前在V2构建工作(因为"WithLengthPrefix"代码是不完整的),但我会去测试它V1.如果它有效,我将以上所有方案都放到测试套件中以确保它在v2 工作.

编辑:

是的,它在"v1"上运行正常,输出:

123
Person: Fred
abc
Run Code Online (Sandbox Code Playgroud)