воскресенье, 15 мая 2011 г.

Google Protobuf - замена вашему велосипеду

Как часто приходится сталкивать с проблемой сериализации / десериализации данных? И если в .NET подобная проблема решена более-менее стандартно, то, как быть, если вы пишите код на C++? Или ещё хуже: вам необходимо обмениваться сообщениями между сервером, написанным на C++ и клиентом, написанным на C#. Некоторое время назад я открыл для себя такую вещь, как Google Protocol Buffers. В этой заметке я постараюсь рассказать, что это такое и зачем оно может быть полезно.

Велосипед не нужно изобретать – на нём нужно ездить!
Чтобы понять, чем же хорош Google Protobuf, нужно осознать, чем плохи реализации собственных протоколов и библиотек сериализации.

Главная проблема собственных реализаций в том, что их нужно реализовывать. Я предпочитаю тратить время на более интересные занятия, нежели в очередной раз писать ручной сериализатор / десериализатор.

Вторая проблема не так очевидна, но она напрямую следует из первой. В какой момент вы задумываетесь о возможности расширения вашего протокола? Хорошо, если вашей квалификации оказалось достаточно, чтобы подумать о проблеме расширения заранее, и снабдить ваш протокол необходимыми свойствами (как минимум вы добавили номер версии в заголовок). Отлично, если вы придумали, как сохранить обратную совместимость между версиями. Но это всё равно не спасёт, если какая-то из частей вашей системы, не будучи обновлённой (потому что обновление ей не требуется), начнём падать, при получении сообщений в новом формате. Просто кто-то из самых лучших побуждений встроил в неё жёсткую проверку версий.
В этом плане показателен пример XML`я. Расширять протоколы на базе XML`я легко – просто добавляем новый тэг, не так ли? Да, но до тех пор, пока вы не додумались вставить проверку по DTD! Как вы помните, данная проверка любезно сообщает вам не только об отсутствии необходимых элементов, но и о наличии неожидаемых и даже о неправильном порядке следования тэгов. Поэтому при расширении такого протокола, вы будете вынуждены обновлять все компоненты системы, даже если изменения имеют обратную совместимость, и новые элементы нужны только некоторым частям.

Ну и наконец, третья проблема: кросс-платформенность. Допустим, вы имеете стабильную систему, написанную на C++; реализация вашего протокола отлажена и не даёт сбоев; клиенты довольны. И вдруг у вас появляется новый компонент, написанный на Java! Вам придётся пройти весь путь по реализации и отладки протокола с самого начала. И в будущем решать проблему совместимости для 2-ух платформ, вместо одной.

Если вы успешно решите все эти проблемы, то вы окажетесь в одном шаге от собственной реализации того, что называется Google Protocol Buffers. Поэтому вы можете сильно сэкономить время и стоимость разработки, если сразу воспользуетесь его возможностями.

Приготовились…
Google Protobuf позволяет вам определить протокол передачи данных между различными частями системы на декларативном языке и сгенерировать по этому описанию набор классов для сериализации и десериализации. Официальная реализация от Google поддерживает 3 языка: С++, Java и Python. Кроме этого, энтузиастами создана и поддерживается версия Protobuf для .NET: Protobuf-net. Классы, сгенерированные для разных языков, совместимы на уровне формата передаваемых данных. Это означает, что информация сериализованная с помощью сгенерированного C++ класса, легко десериализуется в Java.

Итак, посмотрим, что же это такое. Для начала скачаем исходники со страницы загрузок проекта и соберём необходимые библиотеки и компилятор protoc, который используется для генерации классов реализации по описанию протокола. Для тестирования я использовал версию 2.3.0, как наиболее проверенную и отлаженную. Список файлов содержит 3 идентичных архива с исходными кодами, отличающимися только типом архиватора: protobuf-2.3.0.tar.bz2, protobuf-2.3.0.tar.gz и protobuf-2.3.0.zip. И один архив protoc-2.3.0-win32.zip с Windows сборкой компилятора protoc, смысла в котором нет никакого.
Распакованный архив исходных файлов в числе прочего содержит каталог vsprojects, содержащий проекты, для сборки Protobuf в Visual Studio. Файлы проектов в формате Visual Studio 2008, поэтому владельцы более поздних версий студии пройдут через автоматическую процедуру преобразования при первом открытии файла protobuf.sln, владельцы же Visual Studio 2005 должны будут преобразовать проекты вручную с помощью прилагаемого скрипта convert2008to2005.sh (для его запуска нужен CygWin). Открытый solution содержит 3 библиотеки protobuf`а: libprotobuf, libprotobuf-lite и libprotoc; компилятор protoc и несколько проектов с тестами. Поскольку последние отказались собираться в моей Visual Studio 2010 Express, я их отключил – на работоспособность библиотек это не повлияло.
Собираем весь solution (необходимо собрать обе конфигурации – Debug и Release, потому что Release версия библиотек окажется не совместимой с Debug версией вашего проекта и наоборот). Запускаем extract_includes.bat (находится в том же каталоге vsprojects) – он скопирует все необходимые заголовки в каталог include. Теперь собранные библиотеки, скопированные заголовки и компилятор protoc можно использовать в своём проекте. На самом деле библиотека нужна ровно одна – libprotobuf.lib. Библиотека libprotobuf-lite.lib представляет собой облегчённую версию с урезанным функционалом, а библиотека libprotoc.lib предназначена для компиляции компилятора.

Поехали!
Формат сообщений в Protobuf описывается на специальном декларативном языке, который в чём-то напоминает декларацию структур в языке C++. Эти декларации сохраняются в файл с расширением .proto и компилируются в исходные коды на выбранном языке с помощью компилятора protoc. Для тестирования я подготовил декларацию, описывающую некоторые свойства пользователя в Active Directory (файл user.proto):
package ad.sync;

import "phone_number.proto";

message User {
    required string id = 1;
    required string ldap = 2;

    message Login {
        optional string domain = 1;
        required string account = 2;
    }

    required Login login = 3;

    optional string display_name = 4;
    repeated PhoneNumber phoneNumber = 5;
    repeated string memberOf = 6;
}

Декларация телефонного номера вынесена в отдельный файл –
phone_number.proto:
package ad.sync;

message PhoneNumber {
    required string number = 1;

    enum Type {
        HOME = 0;
        WORK = 1;
        MOBILE = 2;
        IPPHONE = 3;
    }

    optional Type type = 2 [default = HOME];
}

Компилируем оба proto-файла в классы реализации:
protoc.exe --proto_path=ProtoBufTest\Proto
    --cpp_out=ProtoBufTest\Messages
    --error_format=msvs
    ProtoBufTest\Proto\phone_number.proto
protoc.exe --proto_path=ProtoBufTest\Proto
    --cpp_out=ProtoBufTest\Messages
    --error_format=msvs
    ProtoBufTest\Proto\user.proto

Подключаем к проекту все файлы, сгенерированные в каталог ProtoBufTest\Messages, и всё – теперь мы можем сериализовать и десериализовать данные, согласно нашей декларации. Давайте попробуем сохранить сообщение в файл из приложения на C++:
// Fill data
user.set_id("1234");
user.set_ldap("LDAP://CN=JDoe,CN=Users,DC=neverhood,DC=org");
user.mutable_login()->set_domain("neverhood");
user.mutable_login()->set_account("jdoe");
user.set_display_name("John Doe");
user.add_phonenumber()->set_number("+7 (123) 456-78-90");
ad::sync::PhoneNumber *workPhone = user.add_phonenumber();
workPhone->set_number("+7 (123) 444-55-66");
workPhone->set_type(ad::sync::PhoneNumber::WORK);

// And save to a file stream
std::ofstream fileStream("user.msg", std::ios_base::binary);
user.SerializeToOstream(&fileStream);

* This source code was highlighted with Source Code Highlighter.

А прочитаем сохранённое сообщение в приложении на C# (для этого воспользуемся библиотекой Protobuf-net, которую я упоминал выше):
FileStream fileStream = new FileStream("user.msg", FileMode.Open);

ad.sync.User user = ProtoBuf.Serializer.Deserialize<ad.sync.User>(fileStream);
Console.WriteLine("ID: " + user.id);
Console.WriteLine("Display name:" + user.display_name);
Console.WriteLine("LDAP:" + user.ldap);

* This source code was highlighted with Source Code Highlighter.

Результат выполнения приложения на C#:
ID: 1234
Display name:John Doe
LDAP:LDAP://CN=JDoe,CN=Users,DC=neverhood,DC=org

Потрясающе – оно работает! Чтобы написать такое с нуля понадобится гораздо больше времени.

1 комментарий:

Михаил Сухоруков комментирует...
Этот комментарий был удален автором.

Отправить комментарий