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

в 11:32, , рубрики: ajax, api, C, embedded, mongoose, web-сервер, Программирование

В процессе разработки различных проектов на 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 OKrn"
                        "Server: MyWebServerrn"
                        "Content-Type: application/jsonrn"
                        "Content-Length: %drn"
                        "Connection: closern"
                        "rn", (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 Foundrn"
                        "Server: MyWebServerrn"
                        "Content-Type: text/htmlrn"
                        "Content-Length: %drn"
                        "Connection: closern"
                        "rn", (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 – пожалуйста, оставляйте комментарии!

Автор: nitro2005

Источник

Поделиться

* - обязательные к заполнению поля