В процессе разработки различных проектов на 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 – пожалуйста, оставляйте комментарии!
