Portable Components, вспомогательные средства разработки ПО

Источник: habrahabr
nephrael


Продолжая свою предыдущую статью, посвященную библиотеке POCO (Portable Components), хотелось бы рассказать об оснастке POCO Application и её таких производных, как ServerApplication и ConsoleApplication. 
Оснастка Application создана для упрощения разработки ПО и, как правило, экономии времени. Пользуясь данной оснасткой, мы cможем создать консольные приложения, службы Windows и демоны UNIX за считанные минуты. 


Описание


Производные от Application делятся 2 группы: консольные и серверные. 
Оснастка включает в себя такие вещи, необходимые приложению, как:
  • Работа с аргументами командной строки на высоком уровне. Также имеется система проверки параметров на основе регулярных выражений и проверки на целочисленное значение.
  • Средства создания демонов UNIX и служб Windows.
  • Работа с загрузкой конфигурации. Этот пункт немаловажен в современном программном обеспечении. Конфигурацией можно задать любое поведение программы, не перекомпилируя проект полностью. Возможна загрузка из файлов или из реестра Windows.
  • Инициализация и завершение работы программы. Жизнь программы в POCO Application подчинена циклу: Инициализация - Выполнение прикладной задачи - Завершение работы. Такой порядок позволяет нам оформить прикладную часть в Main, а все второстепенные вещи спрятать подальше.
  • Средства логирования. Ни для кого не секрет, что грамотные системы сбора логов позволяют нам экономить время, а порой и деньги. POCO предоставляет нам очень мощные средства логирования. Логи можно отправлять в консоль, в файл, в журнал событий Windows, на сервер SYSLOG (например, когда узким местом системы является жёсткий диск). Также возможно комбинировать данные методы, задавать произвольный формат записи для каждого канала. В общем, очень мощный инструмент, с которым я вас обязательно познакомлю.
  • Создание подсистем приложения, оформление их в модуль и упаковка в динамическую библиотеку. Очень удобное средство для создания модульной системы, в которой модули можно заменять, не перекомпилируя программу.

Практика


Для создания программы с помощью данной оснастки необходимо наследоваться от Poco::Util::Application и перегрузить следующие методы:
  • void initialize(Application& self) //Инициализации приложения
  • void uninitialize() //Завершение работы приложения
  • void reinitialize(Application& self) //Перезапуск приложения
  • void defineOptions() //Объявление опций
  • void handleOption()	//Для замены обработчика комманд
  • int main(const std::vector<std::string>& args) //Точка входа для логики приложения

Параметры запуска приложения

Параметры запуска приложения в POCO реализуются с помощью класса Option. 
Каждый параметр имеет следующие свойства: 
  • Полное имя
  • Короткое имя
  • Символьное имя (1 символ)
  • Описание

Параметры могут быть сгруппированы и могут быть опциональными. На каждый параметр можно прикрепить валидаторы значения. В POCO предопределены два типа валидаторов: IntValidator - проверяет численные значения, RegExpValidator - проверяет параметр на соответствие с регулярному выражению. В случае, если программа запущена с непрошедшими валидацию параметрами, программа вернет ошибку и покажет все возможные опции, которые в свою очередь формируются автоматически. На параметры можно "вешать" функции-обработчики (callback'и), которые будут вызваны в случае использования этих параметров при инициализации. 

class myApp : public Application
{
public:
    myApp(int argc, char** argv) 
        : Application(argc,argv) 
        {}
    
    void initialize(Application& self)
    {
        cout << "Инициализация" << endl;
        loadConfiguration(); // Конфигурация по умолчанию
        Application::initialize(self);
    }
    void reinitialize()
    {
        cout << "Реинициализация" << endl;
        Application::uninitialize();
    }
    void uninitialize(Application& self)
    {
        cout << "Деинициализация" << endl;
        Application::reinitialize(self);
    }
    
    void HelpHim(const std::string& name, const std::string& value)
    {
        cout << "Здесь я чем-то должен им помочь" << endl;
    }
    
    void Configure(const std::string& name, const std::string& value)
    {
        cout << "Здесь я выдергиваю информацию из конфигурации" << endl;
    }
    
    void defineOptions(OptionSet& options)
    {
        cout << "Конфигурирование опций" << endl;
        Application::defineOptions(options);
        options.addOption(
            Option("help", "h", "Вывод доп. информации")
                .required(false)	//Обязательный параметр
                .repeatable(false)	//Возможно повторение
                //myApp::handleOption - функция-обработчик параметра
                .callback(OptionCallback<myApp>(this, &myApp::handleOption))); 
                
        options.addOption(
            Option("config-file", "f", "Загрузка конфигурации из файла")
                .required(false)
                .repeatable(true)
                .argument("file")
                .callback(OptionCallback<myApp>(this, &myApp::Configure)));

        options.addOption(
            Option("bind", "b", "Связать пару ключ=значение")
                .required(false)
                //Этот параметр - текстовое значение
                .argument("value")
                //Создаем валидатор, который проверяет, что значение целочисленное и лежит в [0; 100]
                .validator(new IntValidator(0, 100)) 
                .binding("test.property"));	//В случае использования данного параметра
    }
    
    int main(const std::vector<std::string>& args)
    {
        cout << "Запуск бизнес-логики" << endl;
    }
};

// Макрос POCO_APP_MAIN разворачивается во что-то вроде этого:
//   int wmain(int argc, wchar_t** argv) 
//   {
//       myApp A(argc,argv);
//       return A.run();
//   }
POCO_APP_MAIN(myApp)


Средства создания демонов UNIX и служб Windows.

Для создания сервера порой необходимо, чтобы её процесс был запущен от другого пользователя (например, от системы) и не занимал ресурсов у последнего. Также эта функция полезна для запуска приложения при старте ОС и не зависело от статуса пользователя. Реализация службы или демона в POCO сводится к наследованию от Poco::Util::ServerApplication. 

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

class myServerTask: public Task
{
public:
    myServerTask(): Task("MyTask") //Регистрируем задачу под именем "MyTask"
    {
    }

    //Запуск задачи
    void runTask()
    {
        Application& app = Application::instance();
        while (!isCancelled())
        {
            //Ждем секунду
            sleep(1000);
            //Пишем в лог информацию
            Application::instance().logger().information
                                ("Приложение работает " + DateTimeFormatter::format(app.uptime()));
        }
    }
};

Далее реализуем непосредственно сервер:

class myServer: public ServerApplication
{
protected:
    void initialize(Application& self)
    {
        //Загружаем конфигурацию
        loadConfiguration();
        
        //Инициализируем ServerApplication
        ServerApplication::initialize(self);
        
        //Задаем логеру канал для вывода в файл
        logger().setChannel(AutoPtr<FileChannel>(new FileChannel("C:\\log.log")));
        
        //Выводим в лог строку
        logger().information("Инициализация");	
    }

    void uninitialize()
    {
        logger().information("Выключение");
        //Денициализируем ServerApplication
        ServerApplication::uninitialize();
    }

    int main(const std::vector<std::string>& args)
    {
        if (!config().getBool("application.runAsDaemon") && 
            !config().getBool("application.runAsService"))
        {
            //Выполняем действия для обработки запуска 
            //приложения как НЕ СЕРВИСА и НЕ ДЕМОНА
            cout << "Вы запустили приложения напрямую, запустите её как сервис или демон" << endl;
        }
        else
        {
            //А тут мы запустили как сервис или демон
            //можно работать
            
            //Создаем менеджер задач
            TaskManager tm;
            
            //Создаем и запускаем нашу задачу
            tm.start(new myServerTask);
            
            //Ждем сигнала о завершении работы
            waitForTerminationRequest();
            
            //Закругляем все задачи и потоки
            tm.cancelAll();
            tm.joinAll();
        }


        //Профит
        return Application::EXIT_OK;
    }
};

//Запускаем сервер
POCO_SERVER_MAIN(myServer)


Всё, сервис и демон написаны.
Теперь компилируем и регистрируем сервис Windows следующими ключами:
  • Для регистрации службы Windows: /registerService
  • Для выключения службы Windows: /unregisterService
  • Для смены имени службы Windows: /displayName "Name"


Запуск и завершение приложения осуществляется следующим образом:
  • Для запуска демона Unix: --daemon
  • Для запуска службы Windows выполняем в коммандной строке: net start <Приложение>
  • Для завершения демона killall <Приложение>
  • Для завершения сервиса net stop <Приложение>


Загрузка конфигурации

Конфигурация загружается методом:
void loadConfiguration(const std::string& path, int priority = PRIO_DEFAULT);

Тип файла определяется расширением:
  • .properties - Properties file (PropertyFileConfiguration)
  • .ini - Initialization file (IniFileConfiguration)
  • .xml - XML file (XMLConfiguration)

Как только данные загружены их можно использовать. В POCO модель данных представляет собой дерево, в котором доступ к каждому элементу задается строкой.
Например XML:
<?xml version="1.0" encoding="UTF-8"?>
<recipe name="хлеб" preptime="5" cooktime="180">
  <title>Простой хлеб</title>
  <composition>
    <ingredient amount="3" unit="стакан">Мука</ingredient>
    <ingredient amount="0.25" unit="грамм">Дрожжи</ingredient>
    <ingredient amount="1.5" unit="стакан">Тёплая вода</ingredient>
    <ingredient amount="1" unit="чайная ложка">Соль</ingredient>
  </composition>
  <instructions>
    <step>Смешать все ингредиенты и тщательно замесить.</step>
    <step>Закрыть тканью и оставить на один час в тёплом помещении.</step>
    <!-- <step>Почитать вчерашнюю газету.</step> - это сомнительный шаг... -->
    <step>Замесить ещё раз, положить на противень и поставить в духовку.</step>
  </instructions>
</recipe>

Грузим так:

void initialize(Application& self)
{
    ofstream file("out.txt");
    cout << "Инициализация" << endl;
    loadConfiguration("a:\\conf.xml");
    
    file << "Мы готовим: " << config().getString("title") << endl
         << "Для этого нам надо: "   << config().getString("composition.ingredient[0]") << " : "
                                    << config().getString("composition.ingredient[0][@amount]") << " " 
                                    << config().getString("composition.ingredient[0][@unit]") 
                                    << endl
                                    << config().getString("composition.ingredient[1]") << " : "
                                    << config().getString("composition.ingredient[1][@amount]") << " " 
                                    << config().getString("composition.ingredient[1][@unit]") 
                                    << endl
                                    << config().getString("composition.ingredient[2]") << " : "
                                    << config().getString("composition.ingredient[2][@amount]") << " " 
                                    << config().getString("composition.ingredient[2][@unit]") 
                                    << endl
                                    << config().getString("composition.ingredient[3]") << " : "
                                    << config().getString("composition.ingredient[3][@amount]") << " " 
                                    << config().getString("composition.ingredient[3][@unit]") 
                                    << endl
        << "Выполняем шаги: "       << endl
                                    << config().getString("instructions.step[0]") << endl
                                    << config().getString("instructions.step[1]") << endl
                                    << config().getString("instructions.step[2]") << endl;   

    int timeToCook = config().getInt("[@cooktime]");
    file << "Время на готовку: " << timeToCook << endl;
    
    file.close();
            
}

Результат такой:
Мы готовим: Простой хлеб
Для этого нам надо: Мука: 3 стакан
Дрожжи: 0.25 грамм
Тёплая вода: 1.5 стакан
Соль: 1 чайная ложка
Выполняем шаги: 
Смешать все ингредиенты и тщательно замесить.
Закрыть тканью и оставить на один час в тёплом помещении.
Замесить ещё раз, положить на противень и поставить в духовку.
Время на готовку: 180

Аналогичным образом можно парсить и INI. Соответственно здесь будет всегда идентификатор вида "категория.ключ".
Например
;INI-File
[Group]
ValueText = "hello world"
IntValue = 123

Грузим так:

std::string text = config().getString("Group.ValueText"); // text == "Hello world"
int value = config().getInt("Group.IntValue"); // value == 123

Файлы .property имеют имя самой переменной в файле
;Java property file
Value.Text = "hello world"
Int.Value = 123

Грузим так:

std::string text = config().getString("Value.Text"); // text == "Hello world"
int value = config().getInt("Int.Value"); // value == 123


Средства логирования

Средства логирования состоят из четырех основных частей:
  • Логер
  • Канал
  • Объект хранения данных (файл, база данных)
  • Форматер

Логер является в приведенной цепочке звеном, к которому обращается наше приложение для отправки данных в лог. Единицей процесса логирования является сообщение. 
Сообщение представляет из себя объект, имеющий: 
  • Источник данных (заранее выбранное текстовое значение)
  • Данные - строка, несущая в себе полезную информацию о событии
  • Временную метку
  • Приоритет сообщения
  • Идентификаторы процесса (PID) и потока (TID)
  • Некоторые опциональные параметры

Приоритеты выставлены в следующей последовательности (от низкого к высокому):
  • Трассировочная информация (Trace)
  • Отладочная информация (Debug)
  • Техническая информация (Information)
  • Напоминание (Notice)
  • Предупреждение (Warning)
  • Ошибка (Error)
  • Критическая ошибка (Critical)
  • Фатальная ошибка (Fatal)

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

Канал - связующее звено между логером и объектом хранения данных.
Существует несколько базовых каналов:
  • ConsoleChannel - как не сложно догадаться, это канал, который выводит данные в стандартный поток вывода STDOUT
  • WindowsConsoleChannel - специфичный для Windows консольный канал, который выводит данные в std::clog
  • NullChannel - отвергает все данные
  • SimpleFileChannel - простой канал для вывода в файл, причем каждое новое сообщение на новой строке. Имеет вшитый максимальный размер файла. Умеет использовать вторичный файл для хранения данных, когда первичный превышает максимальный размер.
  • FileChannel - полноприводный файловый канал. Поддерживает архивирование, часовые пояса, сжатие, максимальное время жизни лога.
  • EventLogChannel - специфичный для Windows канал данных, позволяющий выводить сообщения в системный журнал событий Windows.
  • SyslogChannel - канал, который отправляет сообщения на сервер демона syslog.
  • AsyncChannel - мост, позволяющий отправлять сообщения на любой канал асинхронно.
  • SplitterChannel - канал, позволяющий отправить одно сообщение на несколько каналов


Пример использования логера:

//Консольный канал
AutoPtr<ConsoleChannel> console(new ConsoleChannel);
//Задаем формат
AutoPtr<PatternFormatter> formater(new PatternFormatter);
formater->setProperty("pattern", "%Y-%m-%d %H:%M:%S %s: %t");
//Форматер канала
AutoPtr<FormattingChannel> formatingChannel(new FormattingChannel(formater, console));
//Создаем логер
Logger::root().setChannel(formatingChannel);

//Оправляем логеру сообщение
Logger::get("Console").information("Сообщение в консоль");

//Создаем форматированный канал записи в файл
AutoPtr<FormattingChannel> file(new FormattingChannel(formater, AutoPtr<FileChannel>(new FileChannel("A:\\123.txt"))));
//Создаем логер
Logger::create("File", file);
//Отправляем данные
Logger::get("File").fatal("I want to play a game. Это сообщение в файл");

//Создаем разветвляющий канал
AutoPtr<SplitterChannel> splitter(new SplitterChannel);
//Добавляем в него каналы консоли и файла
splitter->addChannel(file);
splitter->addChannel(console);
//Создаем для них логер
Logger::create("AllChannel", file);

//Пишем в логер сообщение
Logger::get("AllChannel").fatal("Сообщение в консоль и файл");

//Создаем канал системного журнала
AutoPtr<EventLogChannel> event(new EventLogChannel);
//Создаем логер
Logger::create("Event", event);
//Пишем сообщение в системный журнал (только для Windows)
Logger::get("Event").fatal("Сообщение в системный журнал");


Оформляем классы в отдельные модули

В POCO основная концепция - модульность любой ценой, а добиться такой модульности во время выполнения можно хорошим средством - загрузчиком классов (ClassLoader), позволяющим загрузку из динамических библиотек.
Реализуем абстрактный класс сортировки массива.
Для экспорта необходимо в базовом классе реализовать конструктор по умолчанию и виртуальный деструктор, а также создать чисто виртуальный метод virtual string name() const = 0; и в классе-наследнике реализовать его.

//Файл sort.h
class ABaseSort
{
protected:
    vector<int> array; //Массив для манипуляций
public:
    ABaseSort () {}	//конструктор по-умолчанию 
    virtual ~ABaseSort() {}	//деструктор
    virtual string name() const = 0; //специальный метод name , выводящий имя реализации
    
    //Собственно наш рабочий метод
    virtual void sort() = 0;

    //И методы ввода-вывода
    void loadVector(vector<int>& lArray)
    {
        array.assign(lArray.begin(), lArray.end());
    }

    vector<int> getArray()
    {
        return array;
    }

    //Xor-swap
    static void swap(int &A, int &B)
    {
        A ^= B ^= A ^= B;
    }
};

Далее создадим 2 класса сортировки: методом пузырька и стандартным методом STL (stable_sort)

//Класс сортировки методом пузырька
//Файл sort.cpp
#include "sort.h"
class bubbleSort : public ABaseSort
{
public:
    //Метод выводит имя
    string name() const
    {
        return "Bubble Sort";
    }

    //А здесь собственно логика сортировки
    void sort()
    {
        size_t size = array.size();
        for (int i=0; i<size-1; ++i)
            for (int j=i; j<size; ++j)
                if (array[i] > array[j])
                    swap(array[i],array[j]);
    }
};

//Класс сортировки методом STL (std::stable_sort)
class stableSort : public ABaseSort
{
public:
    //Метод выводит имя
    string name() const
    {
        return "Stable Sort";
    }

    //А здесь собственно логика сортировки
    void sort()
    {
        stable_sort(array.begin(), array.end());
    }
};

Осталось добавить параметры экспорта

POCO_BEGIN_MANIFEST(ABaseSort)	//Выгружаем базовый класс
    POCO_EXPORT_CLASS(bubbleSort) //Выгружаем класс сортировки методом пузырька
    POCO_EXPORT_CLASS(stableSort) //Выгружаем класс сортировки методом stable_sort
POCO_END_MANIFEST

Компилируем проект как динамическую библиотеку.
А теперь давайте воспользуемся нашими классами.

//Файл logic.cpp
#include "sort.h"
//Создаем загрузчик с базовым классом ABaseSort
Poco::ClassLoader<ABaseSort> loader;

loader.loadLibrary("myImportedFile.dll");	//Загружаем динамическую библиотеку
if (loader.isLibraryLoaded("myImportedFile.dll"))
{
    //Выведем все доступные классы
    cout << "Доступны следующие классы сортировки: " << endl;
    for (auto it = loader.begin(); it != loader.end(); ++it)
    {
        cout << "В библиотеке '" << it->first << "': " << endl;
        for (auto jt = it->second->begin(); jt != it->second->end(); ++jt)
        {
            cout << jt->name() << endl;
        }
    }
    
    //Тестовый массив
    int arr[13] = {32,41,23,20,52,67,52,34,2,5,23,52,3};
    vector<int> A (arr,arr+13);
    
    //Создаем класс сортировки
    if (ABaseSort *sort = loader.create("bubbleSort"))
    {
        //Загружаем в него вектор
        sort->loadVector(A);
        
        //Сортируем
        sort->sort();

        //Забираем результат
        auto vect = sort->getArray();

        //Наслаждаемся
        for (auto it = vect.begin(); it != vect.end(); ++it)
            cout << *it << " ";
        cout << endl;

        //Отмечаем объект на автоудаление
        loader.classFor("bubbleSort").autoDelete(sort);
    }
    
    //Далее повторяем тоже самое для stableSort
    if (ABaseSort *sort = loader.create("stableSort"))
    {
        sort->loadVector(A);
        sort->sort();

        auto vect = sort->getArray();

        for (auto it = vect.begin(); it != vect.end(); ++it)
            cout << *it << " ";
        cout << endl;

        loader.classFor("stableSort").autoDelete(sort);
    }
}

Таким образом, мы можем изменять логику работы программы, не перекомпилируя её полностью. Достаточно перекомпилировать отдельные её модули и "скармливать" их программе.

Заключение


Выше приведённые примеры показывают некоторые особенности разработки с использованием библиотеки POCO. Вы можете заметить, что создание приложения или службы на POCO не трудоемкая работа. В дальнейшем хотелось бы рассказать подробно о модулях XML, ZIP, Data, Net. Поподробней остановится на создании высокопроизводительных серверов на POCO. Разобрать систему оповещения и событий (Notifications & Events), систему кэширования и модуль криптографии.

Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=29842