Pull to refresh

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

Reading time 11 min
Views 43K
Предлагаю вашему вниманию введение в использование 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
Tags:
Hubs:
+31
Comments 33
Comments Comments 33

Articles