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

в 17:18, , рубрики: c++, Poco, Программирование, метки: ,

Portable Components, вспомогательные средства разработки ПО
Продолжая свою предыдущую статью, посвященную библиотеке 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

* - обязательные к заполнению поля