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

    Предлагаю вашему вниманию введение в использование 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
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 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 как всегда нет. Тенденция, однако

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