Вбудований компактний веб-сервер Mongoose

У процесі розробки різних проектів на 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, і т. д.

Також величезний плюс цієї бібліотеки – те що вона працює асинхронно, тобто ви просто пишете функцію-обробник подій, яка викликається при будь-якому подію (встановлення з'єднання, розірвання, прийомі даних, передачі, надходження запиту, і т. д.), а в основному циклі вашої програми вставляєте функцію, яка викликає ваш обробник по кожному події події.

Таким чином, ваша програма може бути однопотокового і без блокувань, що позитивно позначається на економії ресурсів і продуктивності.

Приклад

Абстрактний приклад для наочності:

#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 = "ні";

// ініціалізуємо менеджера
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 – ласка, залишайте коментарі!
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.