Как я могу отправить несколько типов объектов через Protobuf?

Я реализую клиент-серверное приложение и ищу различные способы сериализации и передачи данных. Я начал работать с сериализаторами 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);
}

Десериализация:

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 имеет тот же эффект, что и использование хеш-функции для его определения. Значение сериализуемой строки не меняет результата.


person dlras2    schedule 15.06.2010    source источник


Ответы (2)


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

В этом сценарии предполагается, что вы назначите уникальный номер для каждого типа сообщения. Перегрузка, которую вы используете, передает их все как «поле 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
person Marc Gravell    schedule 16.06.2010
comment
Как и было обещано, добавлено в набор тестов: code.google .com / p / protobuf-net / source / browse / trunk / Примеры / - person Marc Gravell; 17.06.2010
comment
Как мне не стыдно за недооценку полноты протобуфа-нета! Было бы использование obj.GetType().GetHashCode() плохой идеей для генерации field чисел, если бы я хотел избежать волшебного предопределенного словаря? - person dlras2; 17.06.2010
comment
@Daniel - до тех пор, пока у вас есть какая-то схема для смягчения небольшого шанса конфликтов хешей ... - person Marc Gravell; 17.06.2010
comment
@Daniel - понял в одночасье; есть веские причины не использовать хэш-код: банально, число должно быть >= 1 (что легко исправить), но более важно: хеш-кодам не следует доверять за пределами данный app-домен. Они могут измениться; например, алгоритм хеширования строки изменился между 1.1 и 2.0 - и может законно измениться снова. Было бы лучше использовать что-то вроде хеша MD5 полного имени типа. Или: сделайте свое первое сообщение (с полем 1 или аналогичным) набором сопоставлений между номерами полей и именами типов. - person Marc Gravell; 17.06.2010
comment
@Marc - я не знал, что алгоритм хеширования изменился; хеш MD5 полного имени типа действительно звучит как лучшее решение для этого. Как и карта типов на стороне сервера, отправляемая клиентам при подключении. Единственная проблема с этим методом заключается в том, что если клиент хочет отправить объект, с которым сервер не знаком, он не будет знать, какой номер ему дать. (Конечно, я не могу вспомнить, когда это действительно произойдет - сервер, вероятно, должен быть более современным, чем клиенты.) - person dlras2; 17.06.2010
comment
@Daniel - очевидно, что для перехода от MD5 к Int32 потребуется некоторое время, но важным моментом является известный постоянный алгоритм. - person Marc Gravell; 17.06.2010
comment
@Marc - Спасибо за все ваши советы! С другой стороны, не могли бы вы объяснить мне, какие поля может сериализовать protobuf-net? Кажется, что частные свойства и свойства со смешанными уровнями доступа прекрасно сериализуются, и мне любопытно, как это сделать. (Если это лучше подходит для отдельного вопроса, дайте мне знать.) - person dlras2; 17.06.2010
comment
@Daniel - это зависит от платформы. На полной версии .NET он может добраться до всего. Если это compact-framework / silverlight и т. Д. (Или если вы используете v2 для создания автономной предварительно скомпилированной dll), тогда он имеет доступ только к общедоступным типам и членам. В v1 требуется конструктор без параметров; в версии 2 вы можете (необязательно) обойти это (в стиле WCF). - person Marc Gravell; 17.06.2010
comment
@Marc - Похоже, что поле разрывается для очень больших индексов полей. 600542181 возвращается как 63671269. Но 29723 работает нормально. Какие границы на поле? Я дам вам знать, если у меня появятся другие проблемы или информация. - person dlras2; 07.07.2010
comment
@ Дэниел - это в протобуфе? Если да, то какой версии? - person Marc Gravell; 07.07.2010
comment
@Marc - Да, я использую protobuf-net v1.0.0.282 (время выполнения v2.0.50727). Я добавлю часть своего кода к своему вопросу, потому что он здесь не подходит. - person dlras2; 07.07.2010
comment
@MarcGravell - В чем заключается недостаток производительности при использовании такого подхода к сериализации (по сравнению с скомпилированной моделью). ?? - person ; 05.03.2014
comment
Производительность при холодном запуске @guillaume. Как только он построит модель во время выполнения, в любом случае это будет довольно быстро. - person Marc Gravell; 06.03.2014

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


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; }
}
person dlras2    schedule 15.06.2010