Продолжая свою предыдущую статью, посвященную библиотеке 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), систему кэширования и модуль криптографии.
Спасибо за прочтение статьи. Приветствуется аргументированная критика и предложения
Автор: nephrael