Продолжая свою
предыдущую статью, посвященную библиотеке 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)
.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")
.validator(new IntValidator(0, 100))
.binding("test.property"));
}
int main(const std::vector<std::string>& args)
{
cout << "Запуск бизнес-логики" << endl;
}
};
POCO_APP_MAIN(myApp)
Средства создания демонов UNIX и служб Windows.
Для создания сервера порой необходимо, чтобы её процесс был запущен от другого пользователя (например, от системы) и не занимал ресурсов у последнего. Также эта функция полезна для запуска приложения при старте ОС и не зависело от статуса пользователя. Реализация службы или демона в POCO сводится к наследованию от
Poco::Util::ServerApplication.
Реализуем класс некоторой задачи, которая будет являться логикой нашего сервера, например, каждую секунду будет писать в лог, сколько отработала наша программа:
class myServerTask: public Task
{
public:
myServerTask(): Task("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::initialize(self);
logger().setChannel(AutoPtr<FileChannel>(new FileChannel("C:\\log.log")));
logger().information("Инициализация");
}
void uninitialize()
{
logger().information("Выключение");
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>
</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. Соответственно здесь будет всегда идентификатор вида "категория.ключ".
Например
[Group]
ValueText = "hello world"
IntValue = 123
Грузим так:
std::string text = config().getString("Group.ValueText");
int value = config().getInt("Group.IntValue");
Файлы .property имеют имя самой переменной в файле
;Java property file
Value.Text = "hello world"
Int.Value = 123
Грузим так:
std::string text = config().getString("Value.Text");
int value = config().getInt("Int.Value");
Средства логирования
Средства логирования состоят из четырех основных частей:
- Логер
- Канал
- Объект хранения данных (файл, база данных)
- Форматер
Логер является в приведенной цепочке звеном, к которому обращается наше приложение для отправки данных в лог. Единицей процесса логирования является сообщение.
Сообщение представляет из себя объект, имеющий:
- Источник данных (заранее выбранное текстовое значение)
- Данные - строка, несущая в себе полезную информацию о событии
- Временную метку
- Приоритет сообщения
- Идентификаторы процесса (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);
Logger::get("Event").fatal("Сообщение в системный журнал");
Оформляем классы в отдельные модули
В POCO основная концепция - модульность любой ценой, а добиться такой модульности во время выполнения можно хорошим средством - загрузчиком классов (ClassLoader), позволяющим загрузку из динамических библиотек.
Реализуем абстрактный класс сортировки массива.
Для экспорта необходимо в базовом классе реализовать конструктор по умолчанию и виртуальный деструктор, а также создать чисто виртуальный метод
virtual string name() const = 0; и в классе-наследнике реализовать его.
class ABaseSort
{
protected:
vector<int> array;
public:
ABaseSort () {}
virtual ~ABaseSort() {}
virtual string name() const = 0;
virtual void sort() = 0;
void loadVector(vector<int>& lArray)
{
array.assign(lArray.begin(), lArray.end());
}
vector<int> getArray()
{
return array;
}
static void swap(int &A, int &B)
{
A ^= B ^= A ^= B;
}
};
Далее создадим 2 класса сортировки: методом пузырька и стандартным методом STL (stable_sort)
#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]);
}
};
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)
POCO_END_MANIFEST
Компилируем проект как динамическую библиотеку.
А теперь давайте воспользуемся нашими классами.
#include "sort.h"
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);
}
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), систему кэширования и модуль криптографии.