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

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


  1. Sterpa
    08.02.2017 15:55

    За абстрактный пример — 5 (как мера абстрактности), за содержание — 2.
    Если вы сами работали с этой библиотекой, можете сделать другой абстрактный пример, например, как будет выглядеть код для Ардуины Уно и любым wifi модулем на ваш вкус, в режиме сервера, чтобы контроллер получил запрос GET, отпарсил его, нашел запрашиваемый ресурс на SD-карте и отдал клиенту.
    Дополнительно сразу хотелось бы уточнить, как будет реализовываться многопоточность (на выбраном wifi модуле), потому как фавиконку клиент запросит сразу же, до окончания передачи тела index.htm.


    1. nitro2005
      08.02.2017 16:09

      По поводу ардуины ничего не могу сказать, к сожалению — никакого опыта с ней нет :(


      1. Sterpa
        08.02.2017 16:15

        Ок, может кто из сообщества поделится опытом?
        Ниже дал ссылку на подобный Ардуино-форк Монгуза, но старый и на питоне…
        Кто-нибудь пытался реализовать подобное?
        ArduinoMegaServer не предлагать, там свои тараканы и никак не ребята не отвяжутся от конкретных wifi чипов.


    1. Sterpa
      08.02.2017 16:11

      Я поясню, на всякий случай, вопрос.
      Вот здесь представлен один дипломный проект, где разработчику удалось уместить всё в 2КБ оперативки Ардуины Уно. Но тут накручен питон и… и не раскрутить.
      https://github.com/hdlj/Mongoose

      Может с тех пор что-то поменялось в библиотеке, но пока Атмеги среди чипов на сайте разработчика нет.


  1. eao197
    08.02.2017 16:00
    +3

    Огромной ложкой дегтя остается лицензия – GPLv2, а ценник на коммерческую лицензию для небольших проектов кусается.


    Вроде как для тех, кого это не устраивает, есть civetweb, который является форком Mongoose от 2013-го года (когда Mongoose сменил лицензию).

    Но меня вот какой вопрос интересует: если при обработке события MG_EV_HTTP_REQUEST не выставить флаг MG_F_SEND_AND_CLOSE, а передать запрос на обработку куда-то еще, то как отдать результат обработки в соединение когда этот результат будет таки получен (например, спустя 10 секунд после вызова обработчика)?


    1. nitro2005
      08.02.2017 16:07

      Нужно сохранить conn, и туда выдать ответ, когда он будет готов.
      Я для этого делал очереди запросов. Также нужно мониторить соединения, чтобы если оно закрылось — отбраковать все запросы из очереди, которые связаны с данным соединением.


      1. eao197
        08.02.2017 16:08

        Это нужно делать на той же самой нити, где дергаются обработчики? Или можно с другой?


        1. nitro2005
          08.02.2017 16:12

          Я полагаю, что на той же.
          Работать с одним соединением в разных потоках — плохая идея.
          Тут весь плюс именно в асинхронной работе в один поток. Т.е. все что делает основной поток — это сетевой ввод-вывод, без диска, без сложной обработки.


          1. pavel_pimenov
            08.02.2017 16:19
            +1

            Так запрос к базе у вас ведь идет в другом потоке?
            поправьте плиз свой пример — чтобы было понятно когда нужно отдать ответ в сохраненный conn


            1. nitro2005
              08.02.2017 16:24

              Тогда нужно добавить очередь запросов и мониторинг соединений. Это значительно увеличит код.
              Хорошо, чуть позже добавлю.
              Запрос к базе может идти в том же потоке, если API базы позволяет. Если не позволяет — да, делаем отдельный поток и городим огород с синхронизацией. Но тогда это значительно нивелирует преимущества данного подхода.


              1. pavel_pimenov
                08.02.2017 16:28
                +1

                Это не пугает — синхронизация не проблема. у меня и так там много ниток для других целей.
                подскажите в какой момент вчитывается очередь ответов и в каком обработчике MG_EV_* нужно слать сообщение клиенту — это ведь нужно делать в нитке мангуста, а не в дочерней?


                1. nitro2005
                  08.02.2017 16:35

                  Я в комменте ниже описал.
                  А слать данные можно непосредственно когда их обнаружили в очереди, вне обработчика. Главное в том же потоке мангуста.


                  1. eao197
                    08.02.2017 16:38

                    Ну так как раз в этом и вопрос: как из другого рабочего потока сказать Mongoose, что у меня есть данные на отправку, чтобы на своем потоке Mongoose дернул нужный коллбэк с нужным событием?


                    1. nitro2005
                      08.02.2017 16:45

                      Тут без коллбэка. Вы просто в потоке мангуста помимо mg_mgr_poll() делаете еще поиск в очереди. Но тогда возникает проблема — mg_mgr_poll() придется делать с нулевым или очень маленьким таймаутом, т.е. чтобы не ждал новых событий. И тогда вы будете выжирать много CPU. Это и есть последствия того, что совмещаем два разных подхода — многопоточность и асинхронность. Возможно можно как-то обойти, но сейчас в голову пока ничего не идет.


                      1. eao197
                        08.02.2017 16:49

                        Очевидно, что делать эти трюки внутри цикла вызовов mg_mgr_poll() — это ерунда. Интересует есть ли возможность зашедулить Mongoose какое-то пользовательское событие из другой рабочей нити. Чтобы Mongoose сам внутри mg_mgr_poll дернул callback с нужным флагом.


                        1. nitro2005
                          08.02.2017 16:50

                          Ну первое что приходит на ум — слать ему какой-нибудь пакет TCP или UDP из потока с базой. Ну или HTTP-запрос, но это дороговато мне кажется.


  1. pavel_pimenov
    08.02.2017 16:06

    Пользуюсь с 2014 года — все хорошо.
    Сервер принимает запросы от десктопных клиентов в виде сжатого json по http
    после обращения к БД возвращает результат так-же в виде json.

    Под мангустом лежит sqlite база размером 17 гиг + levelDB 5 гиг

    Можно подробнее про асинхронный запрос к БД
    как вы его реализовали и в какой момент возвращаете результат?

    у меня к бд коннект один но зовется она синхронно и
    это оказывается узким местом в результате чего эта связка сносно работает только на VPS с SSD дисками.

    Нагрузка у меня очень мелкая 10-20 запросов в секунду.
    в каждом запросе пачки по 5-20 ключей размером 39 байт

    т.е. в секунду идет 50-400 запросов


    1. nitro2005
      08.02.2017 16:13

      Насчет sqlite не подскажу, для PostgreSQL libpq позволяет делать асинхронные запросы.
      Любое соединение с БД — это тоже сокет так или иначе. Так что если API СУБД позволяет асинхрон — вы в дамках :)
      Спасибо за отзыв, интересно!


      1. pavel_pimenov
        08.02.2017 16:25

        У sqlite нет сокета она так-же как и мангуст встраиваемая и состоит из одного файла на С
        но я могу шарить соединение и делать запросы к базе в дочерних нитках.
        как сделать очередь запросов представляю — пока не понял в какой момент отдавать назад результат клиенту.


        1. nitro2005
          08.02.2017 16:32

          Тогда самое простое — сделать два потока — один под мангуст, другой под работу с базой.
          Сделать очередь запросов расшареной (привет блокировки!).
          Поток мангуста:
          1. Поступил HTTP-запрос — записали в очередь
          2. Ищем в очереди запросы, для которых имеются готовые к выдаче ответы от БД и выдаем их клиентам
          поток базы:
          1. Ищем в очереди еще необработанные запросы
          2. Нашли — запрашиваем у базы, получаем от нее ответ, если нужно обрабатываем (обработку вообще можно отдельным потоком сделать), записываем ответ в тот же элемент очереди.
          Это если по-простому с блокировками очереди. Сюда можно присобачить семафоры/мьютексы чтобы не было напрасных sleep-ов.


  1. lpre
    08.02.2017 16:20
    +1

    идут протоптанной дорожкой — Apache/nginx +… А дальше начинается ад, потому что:...

    Все-таки, отдельный HTTPD (чаще, конечно, nginx) — это не ад, а скорее удобство, поскольку вам не нужно заморачиваться с обработкой статических ресурсов, security, рутингом между несколькими инстацами, load balancing, всякой тонкой настройкой соединения и т.д. и т.п.


    Так что если PHP в качестве backend не устраивает, то лучше (imho) сделать nginx + [ваш любимый C/C++ backend с HTTP (с тем же Mongoose) или *cgi].


    1. nitro2005
      08.02.2017 16:25

      Согласен, я даже про nginx написал пример конфига :)


  1. rustler2000
    08.02.2017 19:46

    Проще из ноды взять http_parser и самим сделать, чем от монгуза удовольствие получить. Попутно ускорение будет, нормальная асинхронность и более красивый код


  1. savostin
    09.02.2017 20:08

    Все супер конечно, но в 2016 вручную разбирать и собирать HTTP запросы как-то влом конечно добавляет гибкости, но и багов тоже.