В процессе разработки различных проектов на C/C++ часто возникает необходимость общаться с внешними системами или отдавать данные клиентам по HTTP. Примером может служить любой веб-сервис, а также любое устройство с веб-интерфейсом типа роутера, системы видеонаблюдения, и т.д.
Что в таком случае обычно делают? Правильно, идут протоптанной дорожкой — Apache/nginx + PHP. А дальше начинается ад, потому что:
1. Все это нужно устанавливать и настраивать.
2. Все это жрет приличное количество ресурсов.
3. Из PHP как-то надо получать данные от разрабатываемой системы. Повезет если для этого достаточно просто залезть в СУБД.
Поэтому у меня, как думаю и многих других разработчиков, есть непреодолимое желание впихнуть все эти функции непосредственно в разрабатываемую систему. Это даст неоспоримые преимущества:
1. Меньше внешних зависимостей, а значит проще установка и настройка.
2. Теоретически меньшее потребление ресурсов.
3. Можно отдавать данные прямо из вашего продукта, без посредников.
Но при этом мы не желаем заморачиваться всякими тонкостями обработки HTTP-соединений, парсинга и т.п.
Такие решения есть. И в этой статье я хотел бы поверхностно познакомить вас с одним из них – встраиваемый сервер Mongoose (не путать с MongoDB).
Mongoose изначально позиционировался как встраиваемый веб-сервер. Это означает, что если у вас проект на C/C++ — вам достаточно включить в свой проект два компактных файла mongoose.c и mongoose.h, написать буквально несколько десятков строк кода – и вуаля, вы можете обрабатывать HTTP-запросы!
Однако в последние годы Mongoose серьезно подрос и теперь это не просто встраиваемый веб-сервер, а целая встраиваемая “сетевая библиотека”. То есть, помимо сервера HTTP, с ее помощью вы можете реализовать также: сокеты TCP и UDP, клиент HTTP, WebSocket, MQTT, DNS-клиент и DNS-сервер, и т.д.
Также огромный плюс данной библиотеки – то что она работает асинхронно, т.е. вы просто пишете функцию-обработчик событий, которая вызывается при любом событии (установке соединения, разрыве, приеме данных, передаче, поступлении запроса, и т.д.), а в основном цикле вашей программы вставляете функцию, которая вызывает ваш обработчик по каждому произошедшему событию.
Таким образом, ваша программа может быть однопоточной и без блокировок, что положительно сказывается на экономии ресурсов и производительности.
Абстрактный пример для наглядности:
Обратите внимание, что соединение остается открытым, пока его не закроет клиент, либо пока мы его не закроем явно (с помощью conn->flags). Это означает, что мы можем обрабатывать запрос и после выхода из функции-обработчика.
Таким образом, для асинхронной обработки запросов нам остается только реализовать очередь запросов и контроль соединений. А далее можно делать асинхронные запросы к БД и внешним источникам/потребителям данных.
В теории должно получиться очень красивое решение!
Оно идеально подходит для создания веб-интерфейсов (на AJAX) управления компактными устройствами, а также например для создания различных API с использованием протокола HTTP.
Несмотря на простоту, мне видится, что это еще и масштабируемое решение (если это применимо в целом к архитектуре вашего приложения, конечно), т.к. впереди можно поставить nginx proxy:
Ну а дальше можно еще подключить и балансировочку на несколько инстансов…
Судя по страничке GitHub проекта, он до сих пор активно развивается.
Огромной ложкой дегтя остается лицензия – GPLv2, а ценник на коммерческую лицензию для небольших проектов кусается.
Если кто-то из читателей пользуется данной библиотекой, особенно в production – пожалуйста, оставляйте комментарии!
Что в таком случае обычно делают? Правильно, идут протоптанной дорожкой — Apache/nginx + PHP. А дальше начинается ад, потому что:
1. Все это нужно устанавливать и настраивать.
2. Все это жрет приличное количество ресурсов.
3. Из PHP как-то надо получать данные от разрабатываемой системы. Повезет если для этого достаточно просто залезть в СУБД.
Поэтому у меня, как думаю и многих других разработчиков, есть непреодолимое желание впихнуть все эти функции непосредственно в разрабатываемую систему. Это даст неоспоримые преимущества:
1. Меньше внешних зависимостей, а значит проще установка и настройка.
2. Теоретически меньшее потребление ресурсов.
3. Можно отдавать данные прямо из вашего продукта, без посредников.
Но при этом мы не желаем заморачиваться всякими тонкостями обработки HTTP-соединений, парсинга и т.п.
Такие решения есть. И в этой статье я хотел бы поверхностно познакомить вас с одним из них – встраиваемый сервер Mongoose (не путать с MongoDB).
Основные возможности
Mongoose изначально позиционировался как встраиваемый веб-сервер. Это означает, что если у вас проект на C/C++ — вам достаточно включить в свой проект два компактных файла mongoose.c и mongoose.h, написать буквально несколько десятков строк кода – и вуаля, вы можете обрабатывать HTTP-запросы!
Однако в последние годы Mongoose серьезно подрос и теперь это не просто встраиваемый веб-сервер, а целая встраиваемая “сетевая библиотека”. То есть, помимо сервера HTTP, с ее помощью вы можете реализовать также: сокеты TCP и UDP, клиент HTTP, WebSocket, MQTT, DNS-клиент и DNS-сервер, и т.д.
Также огромный плюс данной библиотеки – то что она работает асинхронно, т.е. вы просто пишете функцию-обработчик событий, которая вызывается при любом событии (установке соединения, разрыве, приеме данных, передаче, поступлении запроса, и т.д.), а в основном цикле вашей программы вставляете функцию, которая вызывает ваш обработчик по каждому произошедшему событию.
Таким образом, ваша программа может быть однопоточной и без блокировок, что положительно сказывается на экономии ресурсов и производительности.
Пример использования
Абстрактный пример для наглядности:
#include "mongoose.h"
// общая структура менеджера соединений
struct mg_mgr mg_manager;
// структура http-сервера
struct mg_connection *http_mg_conn;
// параметры http-сервера
struct mg_serve_http_opts s_http_server_opts;
const char *example_data_buf = "{ \"some_response_data\": \"Hello world!\" }";
const char *html_error_template = "<html>\n"
"<head><title>%d %s</title></head>\n"
"<body bgcolor=\"white\">\n"
"<center><h1>%d %s</h1></center>\n"
"</body>\n"
"</html>\n";
//-----------------------------------------------------------------------------
// Это наш обработчик событий
void http_request_handler(struct mg_connection *conn, int ev, void *ev_data)
{
switch (ev)
{
case MG_EV_ACCEPT:
{
// новое соединение - можем получить его дескриптор из conn->sock
break;
}
case MG_EV_HTTP_REQUEST:
{
struct http_message *http_msg = (struct http_message *)ev_data;
// новый HTTP-запрос
// http_msg->uri - URI запроса
// http_msg->body - тело запроса
// пример обработки запроса
if (mg_vcmp(&http_msg->uri, "/api/v1.0/queue/get") == 0)
{
mg_printf(conn, "HTTP/1.1 200 OK\r\n"
"Server: MyWebServer\r\n"
"Content-Type: application/json\r\n"
"Content-Length: %d\r\n"
"Connection: close\r\n"
"\r\n", (int)strlen(example_data_buf));
mg_send(conn, example_data_buf, strlen(example_data_buf));
// можно управлять соединением с помощью conn->flags
// например, указываем что нужно отправить данные и закрыть соединение:
conn->flags |= MG_F_SEND_AND_CLOSE;
}
// пример выдачи ошибки 404
else if (strncmp(http_msg->uri.p, "/api", 4) == 0)
{
char buf_404[2048];
sprintf(buf_404, html_error_template, 404, "Not Found", 404, "Not Found");
mg_printf(conn, "HTTP/1.1 404 Not Found\r\n"
"Server: MyWebServer\r\n"
"Content-Type: text/html\r\n"
"Content-Length: %d\r\n"
"Connection: close\r\n"
"\r\n", (int)strlen(buf_404));
mg_send(conn, buf_404, strlen(buf_404));
conn->flags |= MG_F_SEND_AND_CLOSE;
}
// для остальных URI - выдаем статику
else
mg_serve_http(conn, http_msg, s_http_server_opts);
break;
}
case MG_EV_RECV:
{
// принято *(int *)ev_data байт
break;
}
case MG_EV_SEND:
{
// отправлено *(int *)ev_data байт
break;
}
case MG_EV_CLOSE:
{
// соединение закрыто
break;
}
default:
{
break;
}
}
}
bool flag_kill = false;
//-----------------------------------------------------------------------------
void termination_handler(int)
{
flag_kill = true;
}
//---------------------------------------------------------------------------
int main(int, char *[])
{
signal(SIGTERM, termination_handler);
signal(SIGSTOP, termination_handler);
signal(SIGKILL, termination_handler);
signal(SIGINT, termination_handler);
signal(SIGQUIT, termination_handler);
// где брать статику
s_http_server_opts.document_root = "/var/www";
// не давать список файлов в директории
s_http_server_opts.enable_directory_listing = "no";
// инициализируем менеджера
mg_mgr_init(&mg_manager, NULL);
// запускаем сервер на localhost:8080 с обработчиком событий - функцией http_request_handler
http_mg_conn = mg_bind(&mg_manager, "127.0.0.1:8080", http_request_handler);
if (!http_mg_conn)
return -1;
// устанавливаем протокол http
mg_set_protocol_http_websocket(http_mg_conn);
while (!flag_kill)
{
// здесь может быть какое-то свое мультиплексирование
// причем можно через mg_connection->sock получить дескриптор
// каждого соединения (и сервера и клиентов) и слушать их в своем select/poll,
// чтобы избежать задержек и sleep-ов
// ...
//
int ms_wait = 1000;
// а здесь мы можем решить будем мы ждать новых событий ms_wait миллисекунд или
// обработаем только имеющиеся события
bool has_other_work_to_do = false;
// обрабатываем все соединения и события менеджера
mg_mgr_poll(&mg_manager, has_other_work_to_do ? 0 : ms_wait);
}
// освобождаем все ресурсы
mg_mgr_free(&mg_manager);
return 0;
}
Обратите внимание, что соединение остается открытым, пока его не закроет клиент, либо пока мы его не закроем явно (с помощью conn->flags). Это означает, что мы можем обрабатывать запрос и после выхода из функции-обработчика.
Таким образом, для асинхронной обработки запросов нам остается только реализовать очередь запросов и контроль соединений. А далее можно делать асинхронные запросы к БД и внешним источникам/потребителям данных.
В теории должно получиться очень красивое решение!
Оно идеально подходит для создания веб-интерфейсов (на AJAX) управления компактными устройствами, а также например для создания различных API с использованием протокола HTTP.
Несмотря на простоту, мне видится, что это еще и масштабируемое решение (если это применимо в целом к архитектуре вашего приложения, конечно), т.к. впереди можно поставить nginx proxy:
location /api {
proxy_pass http://127.0.0.1:8080;
}
Ну а дальше можно еще подключить и балансировочку на несколько инстансов…
Заключение
Судя по страничке GitHub проекта, он до сих пор активно развивается.
Огромной ложкой дегтя остается лицензия – GPLv2, а ценник на коммерческую лицензию для небольших проектов кусается.
Если кто-то из читателей пользуется данной библиотекой, особенно в production – пожалуйста, оставляйте комментарии!