Всем привет!
ВВЕДЕНИЕ
Я в последнее время делал несколько проектов на Python, суть которых сводилась к внешнему сервису, работающему как очередь оборудования и механизм контроля. Но Python - это хоть и очень просто, но не так быстро, поэтому время от времени я пытался найти, как сделать какой-нить сервис на С[++] (плюсы в скобках как бы намекают, что от плюсов там по большому счету только int перед main). В итоге нашел, что позволило мне все свои механизмы, написанные на Python, достаточно легко и непринужденно переработать в механизмы, работающие на С++.
БИБЛИОТЕКА RESTBED
Периодически набирая в гуглах что-то типа "HTTP-сервер на С++", я натыкался на разные решения с костылями и палками, но в последний раз наткнулся на отличную (на мой скромный взгляд) библиотеку, которая достаточно просто позволяет организовать HTTP-сервис, и при этом (и это было важно) существует и отлично работает даже на DEBIAN для Rispberry PI (ниже приведу сравнительный тест).
Сама по себе библиотека очень проста и в сути своей оперирует всего несколькими сущностями: настройкой соединения и ссылкой на сервисы. У библиотеки достаточно большой функционал (включая авторизацию, SSL/HTTPS, многопоточность и все то, что еще может нам потребоваться).
ПРИМЕР ПРОСТОГО СЕРВИСА
Давайте замутим простой сервис, добавляющий в некий массив значения и возвращающий нам, есть ли такое значение в этом массиве.
Для начала установим соответствующую библиотеку (для Linux, как это сделать в винде - я без понятия, если, конечно, что не WSL[2]).
sudo apt install librestbed-dev librestbed0
Тут у нас два пакета - сама библиотека и ее заголовочные файлы для разработчиков.
Дальше при сборке нам достаточно будет указать опцию "-lrestbed" и все.
Напишем простой код:
ЗАГОЛОВКИ
#include <stdlib.h>
#include <map>
#include <string>
#include <memory>
#include <cstdlib>
#include <restbed>
1. Стандартная библиотека - нужна нам для преобразования параметра командной строки в число функцией atoi (для указания номера порта, на котором весить сервис).
2. Map - "ассоциативный массив", который всегда упорядочен по ключу. Скорость доступа к элементуO( Log2N ).
3. Строки - куда без них...
4. Умные указатели - в нашем случае это shared_ptr, который... Сами где-нить прочитайте, а то это может быть надолго. В нашем случае понимать всю суть этого не нужно.
5. Честно говоря, сам с этим не разбирался. Но нам пока это тоже не нужно.
6. Ну и сама наша библиотека для организации HTTP-сервиса.
И еще немного кода...
using namespace std;
using namespace restbed;
map <string, string> srvArray;
Собственно, это у нас определение пространств имен (чтобы не писать перед каждой функцией и типом std::) и нашего "ассоциативного массива".
"ХЭНДЛЕРЫ"
void get_set_method_handler( const shared_ptr< Session > ss )
{
const auto req = ss->get_request();
auto data = req->get_path_parameter( "data" );
srvArray[ data ] = req->get_path_parameter( "value" );
ss->close( OK, "OK", { { "Content-Length", "2" } } );
}
void get_get_method_handler( const shared_ptr< Session > ss )
{
const auto req = ss->get_request();
auto data = req->get_path_parameter( "data" );
if (!srvArray[ data ].empty()) {
const string ret = srvArray[ data ];
//cout << ret << endl;
ss->close( OK, ret, { { "Content-Length", ::to_string( ret.size() ) } } );
} else {
//cout << "EMPTY" << endl;
ss->close( OK, "EMPTY", { { "Content-Length", "5" } } );
}
}
Здесь у нас два обработчика событий HTTP-сервиса, которые мы чуть ниже зарегистрируем.
Первый обработчик устанавливает ключ и значение по приехавшим параметрам. Второй обработчик возвращает установленный ранее параметр или строку "EMPTY", если такое значение мы еще не устанавливали.
Алгоритм тут достаточно прост:
1. Получаем из сессии (аргумент функции) текущий запрос (в принципе, все как в 1С).
2. Получаем из запроса именованный параметр(ы) (задается в шаблоне, увидите ниже).
3. Устанавливаем в ключ значение или извлекаем значение по ключу.
4. Возвращаем "ОК, значение или "ENPTY" (если значение с таким ключом еще не было установлено).
Ну и давайте перейдем к самому интересному...
ФУНКЦИЯ MAIN
int main ( const int carg, const char** arg)
{
int port = 8080;
if ( carg > 1 ) port = atoi( arg[1] );
auto resource_set = make_shared< Resource >();
resource_set->set_path( "/set/{data: .*}/{value: .*}" );
resource_set->set_method_handler( "GET", get_set_method_handler );
auto resource_get = make_shared< Resource >();
resource_get->set_path( "/get/{data: .*}" );
resource_get->set_method_handler( "GET", get_get_method_handler );
auto settings = make_shared< Settings >();
settings->set_port( port );
settings->set_default_header( "Connection", "close" );
Service service;
service.publish( resource_set );
service.publish( resource_get );
service.start( settings );
}
Вот и вся программа на "супер-пупер-сложном" языке.
1. Указываем порт по умолчанию "8080".
2. Проверяем, есть ли параметры в командной строке.
3. Если параметры есть, то устанавливаем порт из первого (в действительности - второго).
4. Определяем первый наш HTTP-ресурс (SET - установка значения), как умный указатель с соответствующим типом значения.
5. Устанавливаем шаблон "/set/{data: .*}/{value: .*}". Я несколько минут потратил, пока до меня дошло, а Вы?
6. Определяем второй ресурс (GET - получение установленного ранее значения или "EMPTY").
7. Создаем настройку, передаем в нее порт, устанавливаем заголовки по умолчанию.
8. Создаем сервис и регистрируем там наши ресурсы.
9. Стартуем сервис с нашими настройками.
Ну и осталось извлечь пользу.
ДЕРНЕМ СЕРВИС ИЗ 1С
Ну тут тоже все просто, но чуть усложним и проведем нагрузочный тест.
Процедура ОтправитьВСервисНаСервере()
СреднееВремя = 0;
МаксВремя = 0;
МинВремя = 100000000;
ВсегоВремя = 0;
Запрос = Новый Запрос(
"ВЫБРАТЬ
| Номенклатура.Наименование КАК Наименование,
| Номенклатура.Код КАК Код
|ИЗ
| Справочник.Номенклатура КАК Номенклатура");
С = Новый HTTPСоединение("172.23.38.82",8080,,,,10);
Т = Запрос.Выполнить().Выгрузить();
Для Каждого Ст ИЗ Т Цикл
З = Новый HTTPЗапрос(СтрШаблон("/set/%1/%2",ст.код, КодироватьСтроку(СтрЗаменить(ст.Наименование,"/"," "),СпособКодированияСтроки.URLВКодировкеURL)));
Время = ТекущаяУниверсальнаяДатаВМиллисекундах();
О = С.Получить(З);
ВремяТ = ТекущаяУниверсальнаяДатаВМиллисекундах() - Время;
ВсегоВремя = ВсегоВремя + ВремяТ;
МинВремя = Мин( МинВремя, ВремяТ);
МаксВремя = Макс( МаксВремя, ВремяТ);
Если НЕ О.КодСостояния = 200 Тогда
Сообщить( "Ошибка! " + О.ПолучитьТелоКакСтроку() );
КонецЕсли;
КонецЦикла;
Сообщить(
СтрШаблон( "Статистика SET:
|Всего запросов: %4,
|Минимальное время запроса: %1 мс,
|Максимальное время запроса: %2 мс,
|Среднее время запроса: %3 мс.", МинВремя, МаксВремя, Цел(ВсегоВремя / Т.Количество()), Т.Количество() )
);
Для Каждого Ст ИЗ Т Цикл
З = Новый HTTPЗапрос(СтрШаблон("/get/%1",ст.код));
Время = ТекущаяУниверсальнаяДатаВМиллисекундах();
О = С.Получить(З);
ВремяТ = ТекущаяУниверсальнаяДатаВМиллисекундах() - Время;
ВсегоВремя = ВсегоВремя + ВремяТ;
МинВремя = Мин( МинВремя, ВремяТ);
МаксВремя = Макс( МаксВремя, ВремяТ);
//Сообщить( Ст.Код + "/" + О.ПолучитьТелоКакСтроку() );
КонецЦикла;
Сообщить(
СтрШаблон( "Статистика GET:
|Всего запросов: %4,
|Минимальное время запроса: %1 мс,
|Максимальное время запроса: %2 мс,
|Среднее время запроса: %3 мс.", МинВремя, МаксВремя, Цел(ВсегоВремя / Т.Количество()), Т.Количество() )
);
КонецПроцедуры
Ну суть кода проста - создаем соединение, дергаем SET с кодом товара в качестве ключа и закодированном наименовании номенклатуры в качестве значения. Дальше дергаем GET с кодом товара. В результат выводим ошибки и измеренное время доступа к сервису.
Ошибка! Argument is not a valid URI: http://localhost/set/000000047/%D0%91%D1%83%D0%BC%D0%B0%D0%B3%D0%B0%20%D0%BE%D1%84%D0%B8%D1%81%D0%BD%D0%B0%D1%8F%20%D1%84-%D1%82%20%D0%903%20500%D0%BB%20%22Ballet%20Premier%22%20%D0%B1%D0%B5%D0%BB%D0%B8%D0%B7%D0%BD%D0%B0%20161%%20CIE,%20%D0%90%20%D0%BA%D0%BB%D0%B0%D1%81%D1%81%20(%D1%80091745,%20%D0%BA398664)
Ошибка! Argument is not a valid URI: http://localhost/set/000000048/%D0%91%D1%83%D0%BC%D0%B0%D0%B3%D0%B0%20%D0%BE%D1%84%D0%B8%D1%81%D0%BD%D0%B0%D1%8F%20%D1%84-%D1%82%20%D0%903%20500%D0%BB%20%22%D0%A1%D0%B2%D0%B5%D1%82%D0%BE%D0%BA%D0%BE%D0%BF%D0%B8%22%20%D0%B1%D0%B5%D0%BB%D0%B8%D0%B7%D0%BD%D0%B0%20146%%20CIE,%20%D0%A1%20%D0%9A%D0%BB%D0%B0%D1%81%D1%81%20CIE%20(%D0%BA28993,%20%D1%80007221)
Ошибка! Argument is not a valid URI: http://localhost/set/000000049/%D0%91%D1%83%D0%BC%D0%B0%D0%B3%D0%B0%20%D0%BE%D1%84%D0%B8%D1%81%D0%BD%D0%B0%D1%8F%20%D1%84-%D1%82%20%D0%904%20500%D0%BB%20%22Ballet%20Brilliant%22%20%D0%B1%D0%B5%D0%BB%D0%B8%D0%B7%D0%BD%D0%B0%20168%%20CIE,%20%D0%90+%20%D0%9A%D0%BB%D0%B0%D1%81%D1%81%20(%D1%80212721)
Ошибка! Argument is not a valid URI: http://localhost/set/000000050/%D0%91%D1%83%D0%BC%D0%B0%D0%B3%D0%B0%20%D0%BE%D1%84%D0%B8%D1%81%D0%BD%D0%B0%D1%8F%20%D1%84-%D1%82%20%D0%904%20500%D0%BB%20%22IQ%20AllRound%22%20%D0%B1%D0%B5%D0%BB%D0%B8%D0%B7%D0%BD%D0%B0%20162%%20CIE(%D0%BA306383,%20%D1%80075193)
Ошибка! Argument is not a valid URI: http://localhost/set/000000051/%D0%91%D1%83%D0%BC%D0%B0%D0%B3%D0%B0%20%D0%BE%D1%84%D0%B8%D1%81%D0%BD%D0%B0%D1%8F%20%D1%84-%D1%82%20%D0%904%20500%D0%BB%20%22%D0%A1%D0%B2%D0%B5%D1%82%D0%BE%D0%BA%D0%BE%D0%BF%D0%B8%22%20%D0%B1%D0%B5%D0%BB%D0%B8%D0%B7%D0%BD%D0%B0%20146%%20CIE,%20%D0%A1%20%D0%9A%D0%BB%D0%B0%D1%81%D1%81%20(%D1%80000877,%20%D0%BA398657)
Ошибка! Argument is not a valid URI: http://localhost/set/000000052/%D0%91%D1%83%D0%BC%D0%B0%D0%B3%D0%B0%20%D0%BE%D1%84%D0%B8%D1%81%D0%BD%D0%B0%D1%8F%20%D1%84-%D1%82%20%D0%904%20500%D0%BB%20%22%D0%A1%D0%BD%D0%B5%D0%B3%D1%83%D1%80%D0%BE%D1%87%D0%BA%D0%B0%22%20%D0%B1%D0%B5%D0%BB%D0%B8%D0%B7%D0%BD%D0%B0%20146%%20CIE%20(%D0%BA306647)
Ошибка! Argument is not a valid URI: http://localhost/set/000000053/%D0%91%D1%83%D0%BC%D0%B0%D0%B3%D0%B0%20%D0%BE%D1%84%D0%B8%D1%81%D0%BD%D0%B0%D1%8F%20%D1%84-%D1%82%20%D0%904%20500%D0%BB%20Ballet%20%22Classic%22%20%D0%B1%D0%B5%D0%BB%D0%B8%D0%B7%D0%BD%D0%B0%20153%%20CIE,%20%D0%92%20%D0%BA%D0%BB%D0%B0%D1%81%D1%81%20(398639,%20398661)
Ошибка! Argument is not a valid URI: http://localhost/set/000000054/%D0%91%D1%83%D0%BC%D0%B0%D0%B3%D0%B0%20%D0%BE%D1%84%D0%B8%D1%81%D0%BD%D0%B0%D1%8F%20%D1%84-%D1%82%20%D0%904%20500%D0%BB%20Ballet%20%22Premier%22%20%D0%B1%D0%B5%D0%BB%D0%B8%D0%B7%D0%BD%D0%B0%20161%%20CIE,%20%D0%90%20%D0%BA%D0%BB%D0%B0%D1%81%D1%81%20(%D1%80066047,%20%D0%BA398663)
Ошибка! Argument is not a valid URI: http://localhost/set/000000055/%D0%91%D1%83%D0%BC%D0%B0%D0%B3%D0%B0%20%D0%BE%D1%84%D0%B8%D1%81%D0%BD%D0%B0%D1%8F%20%D1%84-%D1%82%20%D0%905%20500%D0%BB%20%22OfficeSpace%22%20%D0%B1%D0%B5%D0%BB%D0%B8%D0%B7%D0%BD%D0%B0%20149%%20CIE%201%2010%20(264198)
Ошибка! Argument is not a valid URI: http://localhost/set/000000403/%D0%9F%D0%BE%D0%B4%D1%81%D1%82%D0%B0%D0%B2%D0%BA%D0%B0%20%D0%90%D0%BA%D1%86%D0%B8%D1%8F%2030%%20205*175%20%D0%BC%D0%BC.%20(%D0%BA416133)
Статистика SET:
Всего запросов: 2 000,
Минимальное время запроса: 0 мс,
Максимальное время запроса: 34 мс,
Среднее время запроса: 9 мс.
Статистика GET:
Всего запросов: 2 000,
Минимальное время запроса: 0 мс,
Максимальное время запроса: 44 мс,
Среднее время запроса: 19 мс.
Сначала у меня для ряда позиций система возвратила ошибку, т.к. не смогла определить в запросе соответствие шаблону. Таких строк в моем справочнике не так и много - всего 10 из 2000. Над причиной мне было лень думать, но, например, символ "/" уже приведет к подобной ошибке.
Ну и скорость обращений с виртуалки на винде к хост-системе на убунту достаточно высокая - в среднем запрос выполняется за 9 мс для записи и 19 мс для чтения (не знаю, почему так). Максимальное время не превышает 50 мс. Давайте протестируем это на Rispberry PI 3 B.
sergey@sergey-X570-AORUS-ELITE:~$ ssh pi@172.23.38.105
pi@raspberrypi:~ $ g++ exmpl.cpp -lrestbed
pi@raspberrypi:~ $ ./a.out
Зашли на R PI, скомпилировали программку, запустили...
Статистика SET:
Всего запросов: 2 000,
Минимальное время запроса: 0 мс,
Максимальное время запроса: 94 мс,
Среднее время запроса: 15 мс.
Статистика GET:
Всего запросов: 2 000,
Минимальное время запроса: 0 мс,
Максимальное время запроса: 125 мс,
Среднее время запроса: 31 мс.
С учетом того, что до Rispberry надо ехать через WIFI-роутер, то результат вполне себе приличный.
Ну все, всем спасибо за внимание. Надеюсь, куму-то данная статья поможет в решении каких-то своих интересных задач.
*** тестовая база 1С была развернута на бесплатной версии 1С для обучения программированию на виртаульной машине на базе VirtualBox 6.1.26, на которой была установлена винда 10 с опцией "у меня нет ключа". Подробнее тут: ТЫЛЫЩЬ!