Как реализовать VARIANT в Protobuf - PullRequest
4 голосов
/ 29 июня 2011

В рамках моего протокола protobuf мне требуется возможность отправки данных динамического типа, немного похожего на VARIANT .Грубо говоря, я требую, чтобы данные были целыми, строковыми, логическими или «другими», где «другие» (например, DateTime) сериализуются в виде строки.Мне нужно иметь возможность использовать их как одно поле и в списках в нескольких разных местах протокола.

Как это лучше всего реализовать, сохранив при этом минимальный размер сообщения и оптимальную производительность?

Я использую protobuf-net с C #.

РЕДАКТИРОВАТЬ:
Ниже я разместил предложенный ответ, который использует то, что я считаютребуется минимум памяти.

РЕДАКТИРОВАТЬ 2:
Создан проект github.com на http://github.com/pvginkel/ProtoVariant с полной реализацией.

Ответы [ 5 ]

4 голосов
/ 29 июня 2011

Несколько опций Джона покрывают простейшие настройки, особенно если вам нужна межплатформенная поддержка.На стороне .NET (чтобы гарантировать, что вы не сериализуете ненужные значения), просто верните null из любого свойства, которое не совпадает, например:

public object Value { get;set;}
[ProtoMember(1)]
public int? ValueInt32 {
    get { return (Value is int) ? (int)Value : (int?)null; }
    set { Value = value; }
}
[ProtoMember(2)]
public string ValueString {
    get { return (Value is string) ? (string)Value : null; }
    set { Value = value; }
}
// etc

Вы также можете сделать то же самоеиспользуйте шаблон bool ShouldSerialize*(), если вам не нравятся нули.

Оберните это в class, и вы можете использовать его либо на уровне поля, либо на уровне списка.Вы упоминаете оптимальную производительность;Единственная дополнительная вещь, которую я могу предложить, это, возможно, рассмотреть возможность рассматривать ее как «группу», а не как «суб-сообщение», так как это легче кодировать (и так же легко декодировать, если вы ожидаете данные).Чтобы сделать это, используйте формат данных Grouped через [ProtoMember], то есть

[ProtoMember(12, DataFormat = DataFormat.Group)]
public MyVariant Foo {get;set;}

Однако разница здесь может быть минимальной - но она позволяет избежать некоторого обратного отслеживания в выходном потоке для исправлениядлины.В любом случае, с точки зрения служебных данных , «submessage» займет как минимум 2 байта;«по крайней мере, один» для заголовка поля (возможно, принимая больше, если 12 на самом деле 1234567) - и «по крайней мере, один» для длины, которая становится больше для более длинных сообщений.Группа получает 2 x заголовка поля, поэтому, если вы используете младшие номера полей, это будет 2 байта независимо от длины инкапсулированных данных (это может быть 5 МБ двоичных данных).

Отдельный прием,полезен для более сложных сценариев, но не как совместимый, является универсальным наследованием, то есть абстрактным базовым классом, который имеет ConcreteType<int>, ConcreteType<string> и т. д., перечисленные как подтипы - это, однако, занимает дополнительные 2 байта (как правило), так что это не такбережливое.

Удаляя еще на шаг дальше от основной спецификации, если вы действительно не можете сказать , какие типы вам нужно поддерживать, и вам не нужна совместимость- есть некоторая поддержка для включения (оптимизированной) информации о типе в данные;см. параметр DynamicType на ProtoMember - это занимает больше места, чем два других параметра.

4 голосов
/ 29 июня 2011

Вы можете получить сообщение, подобное этому:

message Variant {
    optional string string_value = 1;
    optional int32 int32_value = 2;
    optional int64 int64_value = 3;
    optional string other_value = 4;
    // etc
}

Затем напишите вспомогательный класс - и, возможно, методы расширения - чтобы убедиться, что вы только когда-либо устанавливали одно поле в варианте.

Вы можете при желании включить отдельное значение перечисления, чтобы указать, какое поле установлено (чтобы сделать его более похожим на теговое объединение), но возможность проверки дополнительных полей просто означает, что данные уже есть.Это зависит от того, хотите ли вы найти правильное поле (в этом случае добавьте дискриминатор) или эффективность использования пространства только , включая сами данные (в этом случае не добавляйте дискриминатор).

Это общий подход буфера протокола.Конечно, может быть что-то более специфичное для protobuf-net.

3 голосов
/ 29 июня 2011

Задавать вопросы всегда помогает мне думать.Я нашел способ получить количество байтов, используемых для передачи, до минимума.

Что я здесь сделал, так это использовал дополнительные свойства.Скажем, я хочу отправить int32.Когда значение не равно нулю, я могу просто проверить свойство сообщения, чтобы узнать, имеет ли оно значение.В противном случае я устанавливаю тип INT32_ZERO.Таким образом, я могу правильно сохранить и восстановить значение.Пример ниже имеет эту реализацию для ряда типов.

Файл .proto:

message Variant {
    optional VariantType type = 1 [default = AUTO];
    optional int32 value_int32 = 2;
    optional int64 value_int64 = 3;
    optional float value_float = 4;
    optional double value_double = 5;
    optional string value_string = 6;
    optional bytes value_bytes = 7;
    optional string value_decimal = 8;
    optional string value_datetime = 9;
}

enum VariantType {
    AUTO = 0;
    BOOL_FALSE = 1;
    BOOL_TRUE = 2;
    INT32_ZERO = 3;
    INT64_ZERO = 4;
    FLOAT_ZERO = 5;
    DOUBLE_ZERO = 6;
    NULL = 7;
}

И сопровождающий его частичный файл .cs:

using System;
using System.Collections.Generic;
using System.Text;
using System.Globalization;

namespace ConsoleApplication6
{
    partial class Variant
    {
        public static Variant Create(object value)
        {
            var result = new Variant();

            if (value == null)
                result.Type = VariantType.NULL;
            else if (value is string)
                result.ValueString = (string)value;
            else if (value is byte[])
                result.ValueBytes = (byte[])value;
            else if (value is bool)
                result.Type = (bool)value ? VariantType.BOOLTRUE : VariantType.BOOLFALSE;
            else if (value is float)
            {
                if ((float)value == 0f)
                    result.Type = VariantType.FLOATZERO;
                else
                    result.ValueFloat = (float)value;
            }
            else if (value is double)
            {
                if ((double)value == 0d)
                    result.Type = VariantType.DOUBLEZERO;
                else
                    result.ValueDouble = (double)value;
            }
            else if (value is decimal)
                result.ValueDecimal = ((decimal)value).ToString("r", CultureInfo.InvariantCulture);
            else if (value is DateTime)
                result.ValueDatetime = ((DateTime)value).ToString("o", CultureInfo.InvariantCulture);
            else
                throw new ArgumentException(String.Format("Cannot store data type {0} in Variant", value.GetType().FullName), "value");

            return result;
        }

        public object Value
        {
            get
            {
                switch (Type)
                {
                    case VariantType.BOOLFALSE:
                        return false;

                    case VariantType.BOOLTRUE:
                        return true;

                    case VariantType.NULL:
                        return null;

                    case VariantType.DOUBLEZERO:
                        return 0d;

                    case VariantType.FLOATZERO:
                        return 0f;

                    case VariantType.INT32ZERO:
                        return 0;

                    case VariantType.INT64ZERO:
                        return (long)0;

                    default:
                        if (ValueInt32 != 0)
                            return ValueInt32;
                        if (ValueInt64 != 0)
                            return ValueInt64;
                        if (ValueFloat != 0f)
                            return ValueFloat;
                        if (ValueDouble != 0d)
                            return ValueDouble;
                        if (ValueString != null)
                            return ValueString;
                        if (ValueBytes != null)
                            return ValueBytes;
                        if (ValueDecimal != null)
                            return Decimal.Parse(ValueDecimal, CultureInfo.InvariantCulture);
                        if (ValueDatetime != null)
                            return DateTime.Parse(ValueDatetime, CultureInfo.InvariantCulture);
                        return null;
                }
            }
        }
    }
}

EDIT:
Дополнительные комментарии @Marc Gravell значительно улучшили реализацию.См. Репозиторий Git для полной реализации этой концепции.

0 голосов
/ 06 августа 2014

Я использую ProtoInclude с абстрактным базовым типом и подклассами, чтобы получить статически установленный тип и одно значение. Вот начало того, как это могло бы выглядеть для Варианта:

[ProtoContract]
[ProtoInclude(1, typeof(Integer))]
[ProtoInclude(2, typeof(String))]
public abstract class Variant
{
    [ProtoContract]
    public sealed class Integer
    {
        [ProtoMember(1)]
        public int Value;
    }

    [ProtoContract]
    public sealed class String
    {
        [ProtoMember(1)]
        public string Value;
    }
}

Использование:

var foo = new Variant.String { Value = "Bar" };
var baz = new Variant.Integer { Value = 10 };

Этот ответ дает немного больше места, так как он кодирует длину экземпляра класса ProtoInclude'd (например, 1 байт для int и строк <125 байтов). Я готов жить с этим в пользу статического контроля над типом. </p>

0 голосов
/ 29 июня 2011

На самом деле protobuf не поддерживает никаких типов VARIANT. Вы можете попробовать поиграть с помощью союзов, подробнее здесь Основная идея состоит в том, чтобы определить оболочку сообщения со всеми существующими типами сообщений в качестве необязательного поля, и с помощью union просто указать, какой это тип конкретного сообщения. Посмотрите пример, перейдя по ссылке выше.

...