вторник, 27 ноября 2012 г.

Основы protobuf

Если вы еще используете JSON, то Google protobuf идет к вам!

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

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

Надо отметить, что, наверное единственный недостаток технологии заключается в необходимости тянуть в проект дополнительные связи с внешней библиотекой. Так, например, работая с JSON или XML, я часто использую свои парсеры и синтезаторы пакетов, чтобы не использовать сторонних библиотек, усложняющих сборку проекта на стороне. Здесь же вся соль технологии заключается в обработке метаописаний и, поэтому, без установки и использования специальных инструментов просто не обойтись.

Суть технологии составляет специальный метакомпилятор, утилита protoc, которая обрабатывает ваше метаописание того набора данных, который нужно сериализовать и составляет по нему систему файлов с исходными кодами неких программных элементов, с помощью которых можно будет легко выполнить сериализацию и десериализацию этих данных. Вам не придется писать ни строчки для синтеза и анализа необходимых пакетов сериализации.

На момент написания этой статьи, Google предоставлял технологию protobuf для языков C++, Java и Python. Кроме того, сторонними заинтересованными компаниями, группами и лицами предоставлена реализация этой технологии для пары десятков других известных языков и, даже, специально для сред разработки.

Чтобы начать использование технологии protobuf с помощью средств предоставляемых Google следует скачать необходимый установочный пакет (под вашу операционную систему) со страницы code.google.com/p/protobuf/downloads/list. Для своего Linux Ubuntu 11.10 я взял вот такой пакет с исходными кодами продукта - protobuf-2.4.1.tar.bz2. Это пакет собран в классическом GNU toolchain и для его сборки и установки надо в директории разархивированного пакета выполнить следующий набор команд в консоли.

$ ./configure
$ make
$ make check
$ sudo make install

Подробности сборки читайте в файле README.txt. Тем кто не знаком с такой системой сборки можно дать только один совет - как можно скорее познакомьтесь с ней. Обратите внимание на цель make check. По этой цели вызывается система автоматических тестов собранного пакета.

После установки пакета можно начинать эксперименты по его использованию. Здесь нам, прежде всего, помогут следующие ссылки на оригинальные ресурсы Goggle.

Получив начальное представление о технологии можно познакомиться с конкретной реализацией на простом примере.

Я начал с того, что доверился показательности примера метаописания адресной книги, приведенной в учебнике к C++, и, на его основе, написал простую программку на C++, которая исполняет сериализацию и десериализацию простейшей адресной книги.

package tutorial;

message Person {
    required string name = 1;
    required uint32 id = 2;
    optional string email = 3;

  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  message PhoneNumber {
    required string number = 1;
    optional PhoneType type = 2 [default = HOME];
  }

  repeated PhoneNumber phone = 4;
}

message AddressBook {
    repeated Person person = 1;
}

В общем, метаописание достаточно понятное. Подробности следует смотреть в руководстве к метаязыку описания данных. Сейчас дадим лишь несколько пояснений.

Объявление пакета (package) вводит в контексте C++ одноименное пространство имен, что предотвращает возможные коллизии данных.

Представленное метаописание содержит три объекта данных: адресная книга (AddressBook), единица записи в адресной книге (Person) и составная часть описания Person - объект телефонного номера (PhoneNumber). По каждому из этих объектов будет формироваться сообщение в пакете сериализации, поэтому для метаописания этих объектов используется ключевое слово message.

Каждый элемент данных сопровождается модификатором: required, optional и repeated.

  1. required - элемент обязательно присутствует в сообщении.
  2. optional - элемент опционален. Может не присутствовать в сообщении.
  3. repeated - элемент может повторяться в сообщении любое количество раз, включая ноль раз.

Таким образом, в нашем описании описана адресная книга (AddressBook), которая может состоять из любого количества элементов Person. Каждый элемент Person, состоит из четрех полей: name, id, email и phone. Поле email - опционально, а поле phone может включать любое число объектов сообщения типа PhoneNamber. Элемент PhoneNumber состоит из двух полей - номера телефона (number) и его типа (type), значение которого определяется специальным типом нумератором PhoneType. Тип номера - опционален.

В метаописании данных допускается использование нескольких примитивных типов, в том числе: bool, int32, uint32, float, double и string. Полный список примитивных типов можно найти в этом разделе руководства метаописания данных. В дополнение к этому, можно составлять типы нумераторы и организовывать иерархию сообщений используя их как типы.

Отдельно следует сказать о тегах, которые определяют уникальность элемента данных внутри записи. Номера тегов записываются в конце каждого элемента сообщения через знак "=". Номера этих тегов делятся на две группы. Группа 1-15 и группа >=16. Различие этих групп в особенностях двоичного кодирования данных, которое мне пока не совсем понятно.

Итак, у нас есть метаописание адресной книги. В нашем случае оно лежит в файле addressbook.proto. Создадим по нему систему классов в пространстве имен tutorial к языку C++. Для этого отдадим файл метаописания утилите protoc следующим образом.

$ protoc --cpp_out=. addressbook.proto

При успешном выполнении этой операции мы получим два файла: addressbook.pb.h и addressbook.pb.cc. Это заголовочный файл и файл реализации для классов составленных по метаописанию. Теперь надо включить эти классы в пространство нашего проекта и можно начинать их использовать в коде проекта.

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

#include "addressbook.pb.h"

int main(int /*argc*/, char */*argv*/[])
{
    // Создаем экземпляр класса адресной книги для сериализации
    tutorial::AddressBook src_book;
    {
        // Создаем и заполняем первую запись в адресной книге
        tutorial::Person * person = src_book.add_person();
        person->set_name("Alexey Knyazev");
        person->set_id(0);
        person->set_email("knzsoft@mail.ru");
        {
            tutorial::Person_PhoneNumber * pn = person->add_phone();
            pn->set_number("+7 927-220-35-67");
            pn->set_type(tutorial::Person_PhoneType_MOBILE);
        }
        {
            tutorial::Person_PhoneNumber * pn = person->add_phone();
            pn->set_number("+7 962-622-31-67");
            pn->set_type(tutorial::Person_PhoneType_MOBILE);
        }
    }
    {
        // Создаем и заполняем вторую запись в адресной книге
        tutorial::Person * person = src_book.add_person();
        person->set_name("Danilov Dmitry");
        person->set_id(1);
        {
            tutorial::Person_PhoneNumber * pn = person->add_phone();
            pn->set_number("8 (8452) 43-96-86");
            pn->set_type(tutorial::Person_PhoneType_HOME);

        }
    }

    std::string msg;
    src_book.SerializeToString(&msg);

    tutorial::AddressBook dst_book;
    dst_book.ParseFromString(msg);

    dst_book.PrintDebugString();

    return 0;
}

Большую часть представленного примера программы составляет заполнение адресной книги. Из кода видно, что API для технологии protobuf достаточно простой и интуитивно понятный.

После заполнения книги мы сериализуем данные созданной адресной книги в строку msg. После чего создаем объект другой адресной книги и заполняем его через десериализацию сообщения msg. В завершении, пользуясь отладочными API, выводим десериализованную адресную книгу в стандартное устройство вывода.

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

person {
  name: "Alexey Knyazev"
  id: 0
  email: "knzsoft@mail.ru"
  phone {
    number: "+7 927-220-35-67"
    type: MOBILE
  }
  phone {
    number: "+7 962-622-31-67"
    type: MOBILE
  }
}
person {
  name: "Danilov Dmitry"
  id: 1
  phone {
    number: "8 (8452) 43-96-86"
    type: HOME
  }
}

Напомню, что во всем этом есть небольшая ложка дегтя - необходимость линковки с библиотекой обеспечивающей фундамент технологии protobuf, с библиотекой libprotobuf. Я делал пример в QtCreator с использованием системы сборки QMake и мой проектный файл выглядит следующим образом.

TARGET = app-1
CONFIG += console
CONFIG -= app_bundle

LIBS += -lprotobuf

TEMPLATE = app

SOURCES += main.cpp \
           addressbook.pb.cc

HEADERS += addressbook.pb.h

OTHER_FILES += \
    addressbook.proto

В заключении следует заметить, что если надо внести изменения в структуру данных подлежащих сериализации, то понадобится перезапустить метакомпилятор protoc по новому описанию и убедиться, что в пространстве проекта лежат измененые файлы - результаты работы метакомпилятора. Соответственно понадобится изменить и проект, но только по части кода заполнения и извлечения данных из предоставленных бизнес-объектов.

Таким образом, основным достоинством технологии является простота использования. Сделал метаописание бизнес-объекта и получил готовый код бизнес-объект и его сериализации. Все что остается - использовать полученный код.

Наверное следует добавить, что предоставленные средства дают возможность не только выполнять сериализацию в строку, но и в поток, что позволяет сразу вывести данные в файл, сеть или куда-то еще, что поддерживает обычные C++ потоки типа std::ostream и std::istream.

воскресенье, 4 ноября 2012 г.

Кодирование по алгоритму MD5 при использовании OpenSSL

Пример использования OpenSSL для кодирования по алгоритму MD5

Занимаясь разработкой библиотеки для работы с протоколом SIP на языке C++ я столкнулся с проблемой кодирования в MD5. Собственно, проблему я создал сам, так как при получении нужного мне шифра я допустил хитрую ошибку в одном из преобразований, и при исполнении этого некорректного преобразования я получал правильное шифрование для одних эталонных данных и неправильное для других. Пытаясь найти ошибку я реализовал несколько вариантов разных решений, и, в конечном счете, понял досадный источник проблемы. Забавным было то, что ошибка была не в алгоритме шифрования, а в коде который превращал 128-битовую бинарную последовательность этого шифра в строку из 32-х символов. В общем, как обычно, все оказалось банальным, хотя и выглядело очень мистически.

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

Надо заметить, что в своих проектах я стараюсь избегать линковки на разные малоизвестные сторонние библиотки. Особенно это справедливо для случаев, если все, что из них берется не стоит тех проблем, с которыми могут столкнуться пользователи твоего кода собирая его под другими платформами. Именно поэтому, для этого проекта я подготовил свою реализацию MD5, но в процессе поиска проблемы я, как уже упоминалось, реализовал несколько совершенно разных решений.

Думаю, что для даного случая отлично подойдет пример с использованием библиотеки Open SSL. Библиотека, как мне показалось, кроссплатформенная, и те, кто работает под Windows, смогут найти соответствующую реализацию библиотеки. В академических целях я, как и прежде, рекомендую Linux. В Linux особенно удобно программировать, так как из репозитория любого дистрибутива, обычно, легко установить нужные языки и необходимые библиотеки. То, что нам понадобится в этом примере, скорее всего, уже есть у вас по умолчанию.

Сразу поясню, что формирование шифра для диалогов SIP требует шифрования досточно хитрых исходных последовательностей в некоторые из которых могут входить MD5-шифры других. Поэтому при поиске проблемы с которой я столкнулся и о которой упомянул выше, мне надо было понять лежит ли проблема в самом шифровании, в неправильной подготовке исходных последовательностей или в выборе вариантов кодирования предлагаемых разными RFC. Вообще, шифрование для SIP/HTTP требует отдельной статьи, и, возможно, я, как-нибудь, найду время на ее написание.

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

Итак, приведу пример кода, который я подготовил и проверил на Linux Ubuntu 11.10 (g++ v 4.6.1).

#include <iostream>

#include <openssl/md5.h>

//! \brief Преобразуем бинарный шифр в строку из hex-символов
std::string toHex(const char *pchData, int count)
{
    std::string s;
    for(int i=0; i<count; ++i) {
        unsigned char ch = pchData[i];
        unsigned char lo = ch%16;
        unsigned char hi = ch/16;

        s.push_back((hi<10)?(hi+0x30):(hi+87));
        s.push_back((lo<10)?(lo+0x30):(lo+87));
    }

    return s;
}

int main()
{
    // Объявим контекст шифрования
    MD5_CTX Md5Ctx;

    // Результат шифрования всегда составляет 128 бит (16 байт)
    const int hash_length = 16;
    char hash[hash_length];

    // Данные для исходной последовательности
    // Исходная последовательность должна содержать эти значения 
    // разделенные двоеточием
    std::string sUserName("alice");
    std::string sPassword("mielophone");

    // Вариант накопления последовательности в пространстве MD5
    std::string hash_string_v1;
    {
        // Передадим последовательность частями, а потом зашифруем
        MD5_Init(&Md5Ctx);
        MD5_Update(&Md5Ctx, (unsigned char *)sUserName.c_str(), sUserName.size());
        MD5_Update(&Md5Ctx, (unsigned char *)":", 1);
        MD5_Update(&Md5Ctx, (unsigned char *)sPassword.c_str(), sPassword.size());
        MD5_Final((unsigned char *)hash, &Md5Ctx);

        hash_string_v1 = toHex(hash, hash_length);
    }

    // Вариант внешнего накопления последовательности
    std::string input_string;
    std::string hash_string_v2;
    {
        // Накопим последовательность
        input_string.append(sUserName);
        input_string.append(":");
        input_string.append(sPassword);

        // Передадим ее в MD5 и зашифруем
        MD5_Init(&Md5Ctx);
        MD5_Update(&Md5Ctx, (unsigned char *)input_string.c_str(), input_string.size());
        MD5_Final((unsigned char *)hash, &Md5Ctx);

        hash_string_v2 = toHex(hash, hash_length);
    }

    // Выведем результаты шифрования одной и той же последовательности
    std::cout << "INPUT:   " << input_string << std::endl;
    std::cout << "HASH_v1: " << hash_string_v1 << std::endl;
    std::cout << "HASH_v2: " << hash_string_v2 << std::endl;

    return 0;
}

Для компиляции этого примера требуется подключение библиотек libcrypto.so и libexpat.so. Так как я готовил пример для системы сборки qmake, то для подключения этих библиотек мне понадобилось добавить следующую строку в файл проекта.

LIBS +=  -lcrypto -lexpat

После запуска получаем следующий вывод в стандартное устройство вывода.

INPUT:   alice:mielophone
HASH_v1: 98c322b45170287dcff3497ac2ab08ac
HASH_v2: 98c322b45170287dcff3497ac2ab08ac

Для проверки такого простого случая кодирования можно воспользоваться сервисом на странице http://www.pr-cy.ru/md5 или аналогичной (введите в строку поиска Google запрос типа "шифрование md5 онлайн"). Откройте указанную или найденную по запросу страницу, введите исходную последовательность alice:mielophone и запросите вычисление хеша.

В завершении хочу обратить внимание на одну, возможно, неочевидную деталь. Если в исходные последовательности шифрования должен входить MD5-хеш вычисленный по другим последовательностям, то будьте внимательны при переводе бинарного кода хеша в строку. Необходимо обговорить регистр используемых шестнадцатеричных литер.