18 мая 2011 в 14:29

Использование Protocol Buffers на платформе .Net (Часть 1)

.NET*
Предлагаю вашему вниманию введение в использование Protocol Buffers на платформе .Net в формате дискуссии. Я расскажу и покажу что это такое и зачем оно нужно .Net разработчику. Топик требует от читателя начального владения языком C# и системой контроля версий SVN. Так как объем материала превышает среднестатистический объем топиков на хабре, которые не вгоняют хаброюзеров в тоску и не заставляют их скроллить до комментариев, было принято решение разбить его на две части. В первой части мы познакомимся с основами и даже напишем (не)много кода!


Здравствуйте, а что такое Protocol Buffers?


Согласно определению на официальной странице, Protocol Buffers (protobuf) – это способ кодирования структурированных данных в эффективном и расширяемом формате, применяемый корпорацией Google почти во всех своих продуктах. Для большинства платформ, включая .Net, такой процесс называется сериализацией.

Я пользуюсь сервисами Google, но как protobuf поможет мне в разработке .Net приложений?


Да вы правы, Google не занимается написанием специализированных библиотек для .Net разработчиков. Однако существует проект protobuf-net (одна из нескольких реализаций protobuf для платформы), который позволяет использовать protobuf в .Net. Им руководит Marc Gravell — завсегдатай stackoverflow.com и участник множества других отличных проектов. Так что вы всегда можете задать ему вопрос, и он с радостью ответит на него (чем и злоупотреблял автор топика).

Почему мне стоит использовать эту библиотеку вместо встроенных средств?


Когда речь заходит о сериализации в .Net, все обычно вспоминают о существовании бинарного форматера и xml сериализатора. Следующее, что отмечают разработчики, то, что первый быстрый и имеет высокую степень сжатия, но работает только в пределах .Net платформы; а второй представляет данные в человеко-читабельном формате и служит основой для SOAP, который в свою очередь обеспечивает кросс-платформенность. Фактически утверждение, что вам всегда нужно делать выбор между скоростью и переносимостью, принимается за аксиому! Но protobuf позволяет решить обе проблемы сразу.

Так вы утверждаете, что protobuf не уступает бинарной сериализации и к тому же переносим?


Да именно это. Давайте рассмотрим небольшой пример, а заодно узнаем как использовать protobuf-net. Предположим, что у нас есть следующие сущности:
using System;
using ProtoBuf;

namespace Proto.Sample
{
    public enum TaskPriority
    {        
        Low,
        Medium,
        High
    }

    [Serializable] // <-- Только для BinaryFormatter
    [ProtoContract]
    public class Task
    {
        [ProtoMember(1)]
        public int Id { get; set; }

        [ProtoMember(2)]
        public DateTime CreatedAt { get; set; }

        [ProtoMember(3)]
        public string CreatedBy { get; set; }

        [ProtoMember(4)]
        public TaskPriority Priority { get; set; }

        [ProtoMember(5)]
        public string Content { get; set; }
    }
}


* This source code was highlighted with Source Code Highlighter.

Protobuf-net требует использования специальных атрибутов, что напрямую следует из главной особенности формата – зависимости от порядка следования полей. Напишем тест производительности и степени сжатия:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using ProtoBuf;

namespace Proto.Sample
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            var tasks = new List<Task>
                            {
                                new Task
                                    {
                                        Id = 1,
                                        CreatedBy = "Steve Jobs",
                                        CreatedAt = DateTime.Now,
                                        Priority = TaskPriority.High,
                                        Content = "Invent new iPhone"
                                    },
                                new Task
                                    {
                                        Id = 2,
                                        CreatedBy = "Steve Ballmer",
                                        CreatedAt = DateTime.Now.AddDays(-7),
                                        Priority = TaskPriority.Low,
                                        Content = "Install own Skype"
                                    }
                            };

            Console.WriteLine("The test of binary formatter:");

            const string file1 = "tasks1.bin";

            TestBinaryFormatter(tasks, file1, 1000);
            TestBinaryFormatter(tasks, file1, 2000);
            TestBinaryFormatter(tasks, file1, 3000);
            TestBinaryFormatter(tasks, file1, 4000);
            TestBinaryFormatter(tasks, file1, 5000);

            Console.WriteLine("\nThe test of protobuf-net:");

            const string file2 = "tasks2.bin";

            TestProtoBuf(tasks, file2, 1000);
            TestProtoBuf(tasks, file2, 2000);
            TestProtoBuf(tasks, file2, 3000);
            TestProtoBuf(tasks, file2, 4000);
            TestProtoBuf(tasks, file2, 5000);

            Console.WriteLine("\nThe comparision of file size:");

            Console.WriteLine("The size of {0} is {1} bytes", file1, (new FileInfo(file1)).Length);
            Console.WriteLine("The size of {0} is {1} bytes", file2, (new FileInfo(file2)).Length);

            Console.ReadKey();
        }

        private static void TestBinaryFormatter(IList<Task> tasks, string fileName, int iterationCount)
        {
            var stopwatch = new Stopwatch();
            var formatter = new BinaryFormatter();
            using (var file = File.Create(fileName))
            {
                stopwatch.Restart();

                for (var i = 0; i < iterationCount; i++)
                {
                    file.Position = 0;
                    formatter.Serialize(file, tasks);
                    file.Position = 0;
                    var restoredTasks = (List<Task>)formatter.Deserialize(file);
                }

                stopwatch.Stop();

                Console.WriteLine("{0} iterations in {1} ms", iterationCount, stopwatch.ElapsedMilliseconds);
            }
        }

        private static void TestProtoBuf(IList<Task> tasks, string fileName, int iterationCount)
        {
            var stopwatch = new Stopwatch();
            using (var file = File.Create(fileName))
            {
                stopwatch.Restart();
                
                for (var i = 0; i < iterationCount; i++)
                {
                    file.Position = 0;
                    Serializer.Serialize(file, tasks);
                    file.Position = 0;
                    var restoredTasks = Serializer.Deserialize<List<Task>>(file);
                }

                stopwatch.Stop();

                Console.WriteLine("{0} iterations in {1} ms", iterationCount, stopwatch.ElapsedMilliseconds);
            }
        }
    }
}


* This source code was highlighted with Source Code Highlighter.

Результаты:
The test of binary formatter:
1000 iterations in 423 ms
2000 iterations in 381 ms
3000 iterations in 532 ms
4000 iterations in 660 ms
5000 iterations in 814 ms

The test of protobuf-net:
1000 iterations in 1056 ms
2000 iterations in 76 ms
3000 iterations in 129 ms
4000 iterations in 152 ms
5000 iterations in 202 ms

The comparision of file size:
The size of tasks1.bin is 710 bytes
The size of tasks2.bin is 101 bytes


* This source code was highlighted with Source Code Highlighter.

Как вы видите, мы превзошли бинарную сериализацию не только по скорости, но и также по степени сжатия. Единственный недостаток, что protobuf-net потребовалось больше времени на «холодный старт». Но вы можете решить эту проблему используя следующий вспомогательный код:
var model = TypeModel.Create();
model.Add(typeof(Task), true);
var compiledModel = model.Compile(path);
compiledModel.Serialize(file, tasks);


* This source code was highlighted with Source Code Highlighter.

Остальные тесты и результаты можно посмотреть здесь.

Ок. Относительно скорости и сжатия вы меня убедили, но как решается проблема переносимости?


Понимаете, если существуют реализация для нужной вам платформы, вопрос переносимости в большинстве случаев снимается. А реализациии protobuf существует для более чем 20 языков. Полный список можно увидеть здесь. Отмечу только, что для некоторых языков существует более одной реализации. Так что у вас всегда есть выбор.

UPD: Часть 2
@osmirnov
карма
39,5
рейтинг 0,0
Самое читаемое Разработка

Комментарии (33)

  • +4
    Позволю себе продолжить ваши изыскания:
    json.codeplex.com/releases/view/64935
    The test of binary formatter:
    1000 iterations in 122 ms
    2000 iterations in 186 ms
    3000 iterations in 286 ms
    4000 iterations in 358 ms
    5000 iterations in 450 ms

    The test of protobuf-net:
    1000 iterations in 137 ms
    2000 iterations in 47 ms
    3000 iterations in 72 ms
    4000 iterations in 93 ms
    5000 iterations in 118 ms

    The test of json-net:
    1000 iterations in 232 ms
    2000 iterations in 200 ms
    3000 iterations in 313 ms
    4000 iterations in 406 ms
    5000 iterations in 513 ms

    The comparision of file size:
    The size of tasks1.bin is 725 bytes
    The size of tasks2.bin is 101 bytes
    The size of tasks3.bin is 244 bytes

    private static void TestJson(IList tasks, string fileName, int iterationCount)
    {
    var stopwatch = new Stopwatch();

    using (var file = File.Create(fileName))
    {
    stopwatch.Restart();
    for (var i = 0; i < iterationCount; i++)
    {
    var str = JsonConvert.SerializeObject(tasks);

    var bytes = Encoding.UTF8.GetBytes(str);
    file.Write(bytes, 0, bytes.Length);
    file.Read(bytes, 0, bytes.Length);
    file.Position = 0;
    var restoredTasks = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(bytes));
    }

    stopwatch.Stop();

    Console.WriteLine("{0} iterations in {1} ms", iterationCount, stopwatch.ElapsedMilliseconds);
    }
    }

    • 0
      Выводы?
      • +1
        Выводы:
        1. Можно юзать читаемый, переносимый и компактный Json.net вместо BinaryFormatter
        2. Если хочется скорости — можно посмотреть в сторону других библиотек
        • 0
          Спасибо, согласен с читаемостью и переносимостью. Однако скорость по сравнению с protobuf-net ниже ~4х, а степень сжатия — в ~2х. Так что на выбор влияет не только скоростью, но и объём.
    • 0
      хочется добавить, что в упомянутом Json.net в последних версиях (для .Net 4.0) реализовали нативную поддержку dynamic
      <offtop>мы используем его совместно с Ext.Direct.Mvc для передачи данных на клиент и сохранения изменений на сервере, более чем удобно</offtop>
      • 0
        Ну, мы делаем то же самое, только в связке с Ext.Net
  • 0
    Почему же так мало? Это даже не введение в protobuf, а статья типа «смотрите чего я нашел, смотрите — оно в быстрое!»

    П.С. Недели две назад перевел проект на protobuf-net — пока полет нормальный, все работает как часы.
    • 0
      Мне казалось я в введение ответил на ваш вопрос.
      • +1
        На самом деле просто не надо было постить весь код, который тестирует производительность. Его ведь можно было выложить на каком-нибуть pastebin или аналогичном сервисе. Сейчас в статье очень мало смысла.
        • +1
          Спасибо за замечание. Постараюсь учесть на будущее. Что касается смысла, стояла цель познакомить читателя с основами. Я понимаю, что вам как зубру разработки кажется написанное очевидным, особенно, после того как внедрили protobuf-net у себя; но, повторюсь, топик рассчитан на новичков.
  • 0
    Что планируется в продолжении?
    • 0
      Продолжение разговора про переносимость и использование protobuf-net в контексте wcf.
      • –3
        чегоб не написать одну статью, потом ее разбить на две и обе запостить сразу? что мешает? жадность?
        • +1
          Жадность чего? Вторую часть как раз заканчиваю, завтра с утра со свежей головой еще раз прочитаю и выложу.
          • 0
            Сравнения protobuf библиотек для .net не планируется?
            • 0
              К сожалению, нет, а есть смысл? Protobuf-net самый активный порт.
  • +1
    Недавно прикрутил эту библиотеку как сериалайзер между wcf-сервисом и silverlight-приложением. Скорость обработки сообщений выросло где-то в два раза при значительном сокращении размера сообщения.
    Пока работает отлично, так что рекомендую :-)
  • 0
    У protobuf было два недостатка в тот момент когда мы рассматривали его как кандидат для использования в нашем проекте:
    — не хранится «размер сообщения», то есть если использовать этот сериализатор для пересылки сообщений, надо либо дважды «заворачивать» данные, либо делать протокол передачи,
    — отсутствует стандартный RPC (строго говоря, нам нужен был именно RPC+сериализация) от «отца-основателя». В результате этих RPC (которые используют внутри себя protobuf) существует несколько, друг с другом не совместимых.

    «Независимая от языка сериализация» — она зачастую когда нужна? Когда делается клиент-серверная архитектура, в которой клиент (или клиенты) написаны на другом языке (языках), чем серверная часть. Описали протокол сериализации — сделайте и следующий шаг: опишите формат сообщений/ответов. Ценность библиотеки выросла бы многократно.
    • 0
      Ну protobuf — это же просто сериализатор. А где, как и для чего передавать данные уже решаете Вы, так же, как и куда слать размер данных.
      А эта реализация для .Net очень просто прикручивается к WCF с помощью атрибутов.
    • НЛО прилетело и опубликовало эту надпись здесь
  • 0
    У меня вопрос! А имеет ли смысл использовать protobuf для передачи структур между С++ и .Net проектами? Просто у меня есть проект, который сейчас mixed-mode и использует P/Invoke методы с большим кол-вом параметров.
    • 0
      Думаю смысл есть, Дмитрий. Однако существуют некоторые ограничения, я упомяну о них в следующей части.
      • 0
        А где можно найти примерчик такого взаимодействия? Понимаю что можно искать инфу по частям, но мне reference project Был бы полезней
        • 0
          На стеке полно ссылок с вопросами на эту тему, но найти готовый пример для вас, увы, найти не получилось (сам на C++ не писал уже лет 5, поэтому писать его самостоятельно не рискну). Если найдёте время для такого примера, буду рад, если поделитесь результатами здесь или у себя в блоге.
        • 0
          Сегодня совершенно случайно нашёл пример такого взаимодействия в блоге Alexey Korotaev.
    • 0
      Protobuf как раз и создавался для кросс-платформенной, кросс-языковой передачи сообщений. Так что теоретически Protobuf/Apache Thrift это то что нужно.
  • 0
    У меня была проблема огромного трафика WCF сервиса. Я тоже смотрел в сторону protocol buffers, но в итоге передумал и до сих пор гоняю XML между клиентом и сервером полученный DataContractSerializer. Вместо огромного по объёму переписывания кода (все DTO разметить, ого-го), я просто перешёл на HTTP и поставил перед сервисом gzip'ующий nginx. Трафик уменьшился в 8.5 раз, а для расжатия на клиентской стороне вообще ничего делать не пришлось. Ну и работы часа на два от силы, на protobuf-net явно больше надо. Тот же финт с сжатием может делать и Apache.
    • 0
      Идея супер! Сам хотел поковырять IIS для этого, но пока руки не дошли. Об nginx как-то особо не думал для этой цели.

      Относительно переписывания DTO, там не так много работы. Фактически, в protobuf-net для этого такая поддержка, что по контракту данных и не скажешь, что используется protobuf-net. Об этом в следующей части)
    • 0
      А у Вас WCF сервис хостится на IIS? Если да, то там есть поддержка gzip'а из коробки и не нужно ставить перед ним ngix.
      • 0
        Нет, не в IIS и он не может хостится в IIS по техническим причинам.
  • +1
    В статье «Когда речь заходит о сериализации в .Net, все обычно вспоминают о существовании бинарного форматера и xml сериализатора. Следующее, что отмечают разработчики, то, что первый быстрый и имеет высокую степень сжатия, но работает только в пределах .Net платформы; а второй представляет данные в человеко-читабельном формате»
    Я считаю правильнее было бы: "… первый имеет невысокую степень избыточности..."
    • 0
      Спасибо, вы, конечно, правы, но думаю «сжатие» больше понятно большинству читателей, хотя и не совсем точно.
  • 0
    реализациии protobuf существует для более чем 20 языков

    Почему-то для Delphi как всегда нет. Тенденция, однако

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.