Как я могу отправить несколько типов объектов через Protobuf? - PullRequest
7 голосов
/ 16 июня 2010

Я внедряю клиент-серверное приложение и изучаю различные способы сериализации и передачи данных. Я начал работать с сериализаторами 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 в некоторый тип оболочки, который определяет тип объекта. (Эта оболочка также будет содержать такую ​​информацию, как идентификатор пакета и место назначения.) Глупо использовать 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);
}

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] бросает KeyNotFoundException при поиске 63671269.

Если я заменю ToInt32 на ToInt16 в хэш-функции, значение поля будет установлено на 29723, и оно будет работать. Это также работает, если я явно определяю поле System.String для 1. Явное определение поля для 600542181 имеет тот же эффект, что и использование хеш-функции для его определения. Значение сериализуемой строки не меняет результат.

Ответы [ 2 ]

9 голосов
/ 17 июня 2010

Эта функциональность действительно встроена, хотя и не очевидно.

В этом случае ожидается, что вы назначите уникальный номер для каждого типа сообщения. Используемая вами перегрузка передает их все как «поле 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. Если это сработает, я передам весь вышеописанный сценарий комплекту тестов, чтобы убедиться, что работает в версии 2.

Edit:

да, он отлично работает на "v1", с выводом:

123
Person: Fred
abc
3 голосов
/ 16 июня 2010

Я придумала другое решение, но я решила поставить его в качестве ответа, а не в вопросе, потому что это имеет больше смысла для меня. Это довольно уродливо, на мой взгляд, и меня предупреждали против использования рефлексии, поэтому, пожалуйста, прокомментируйте это или предоставьте лучшие ответы, если они у вас есть. Спасибо!


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; }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...