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