Встраиваемый компактный веб-сервер 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 = "\n"
    "%d %s\n"
    "\n"
    "

%d %s

\n" " \n" "\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 — пожалуйста, оставляйте комментарии!

Комментарии (0)

© Habrahabr.ru