Если бы вам нужно было подключить устройство, с которым вы работаете, например, бытовую технику, термостат или программное приложение, и вы искали решение с минимальными затратами, с чего бы вы начали?

Я был в такой ситуации в 2004 году, когда экспериментировал с различными веб-серверами для обслуживания веб-интерфейса для устройства сетевой безопасности, над которым я работал. Попробовав несколько серверов, я решил накатить свой. Я хотел сделать это:

  • простой, способный работать без настройки, с помощью простого двойного щелчка
  • кроссплатформенность, так как я использовал системы FreeBSD и OpenBSD на работе, а также системы FreeBSD, Linux и Windows дома и хотел запускать и тестировать свой код во всех средах.
  • и храните исходный код в одном файле, чтобы упростить сборку и интеграцию.

Я поддерживал проект в течение многих лет (сначала на Sourceforge, затем на Google Code и, наконец, на GitHub), и по мере того, как он постепенно набирал популярность, особенно в мире встраиваемых систем, это привело меня к тому, что в 2013 году я стал соучредителем Cesanta. многие проекты и предприятия включили Mongoose в свои решения (Карты Apple, Panasonic и т. д.) из-за его простоты и мобильности.

Со временем стало очевидно, что Mongoose активно используется для реализации функций IoT, и поэтому Cesanta добавила поддержку популярных протоколов IoT; делая Mongoose не просто веб-сервером, а полноценной сетевой библиотекой.

В следующих разделах я собираюсь показать сжатый обзор распространенных вариантов использования Mongoose с необходимым кодом, пояснениями и снимками экрана.

Вы можете скачать Mongoose и работать с примерами, пока читаете этот пост.

Подключение устройств

Сразу к делу. У меня есть встроенное устройство, на этом устройстве работает операционная система или операционная среда, которая предоставляет интерфейс сокетов BSD. Наличие API сокетов BSD является обязательным требованием для Mongoose. Примерами таких сред являются встроенные Linux, Windows CE, vxWorks, FreeRTOS, Ecos, QNX и многие другие. Наши цели:

1. Запустите веб-сервер на устройстве, чтобы предоставить панель мониторинга, которая показывает настройки устройства, состояние и т. д.
Обоснование: конечный пользователь может получить доступ к устройству с помощью любого браузера и увидеть состояние устройства. .

2. Сделайте так, чтобы устройство реализовало RESTful API.
Обоснование: некоторые внешние инструменты могут запрашивать информацию у устройства автоматически. Например, внешний инструмент мониторинга может запросить у устройства отчет об использовании ЦП/памяти и другую статистику.

3. Заставьте устройство реализовать полнодуплексное соединение WebSocket.
Обоснование: В некоторых случаях передача данных должна выполняться как можно быстрее. Данные могут быть в двоичном формате. Таким образом, использование WebSocket, которое поддерживает постоянное TCP-соединение с узлом и обеспечивает протокол кадрирования с низкими накладными расходами, является отличным вариантом для инкапсуляции связи.
Пример 1. Потоковая передача захваченных данных изображения на облако.
Пример 2. Устройство может иметь довольно медленный процессор. Инициация SSL для каждого подключения к серверу может занять до десятков секунд! Вот почему однократная инициализация SSL-соединения и поддержание его в рабочем состоянии (это делает WebSocket) является наиболее эффективным вариантом.
Пример 3. Устройство может реализовать панель мониторинга пользователя в реальном времени, которая хочет получать уведомления с устройства в любой случайный момент. В веб-разработке этот вариант использования иногда называют проталкиванием на сервер.

Итак, давайте рассмотрим все три случая один за другим, с кодом и пояснениями.

Создание базовой приборной панели/страницы настроек устройства

Чтобы сделать дашборд или страницу настроек, мне нужно:

  • Коллекция статических файлов: HTML, CSS, JavaScript, файлы изображений.
  • Некоторый динамический контент, отображающий информацию об устройстве.

Начнем с предоставления статического контента. Во-первых, создайте каталог web_root с index.shtml, немного css и изображением логотипа (см. здесь). Это будет наш веб-корень со статическим содержимым. Затем создайте файл server.c с основным кодом подключения, который запускает веб-сервер, устанавливает корневой веб-каталог в значение web_root и обслуживает оттуда файлы (здесь исходный код).

Самая важная часть кода, которая реализует обслуживание из web_root, — это функция обработчика событий, которая вызывается для каждого HTTP-запроса и которую я буду расширять в следующих примерах:

static void ev_handler(struct mg_connection *nc, int ev, void *p) {       
   if (ev == MG_EV_HTTP_REQUEST) { 
     mg_serve_http(nc, p, s_http_server_opts); // Serves static     
     content 
   } 
}

Посмотрим, что у нас получится в итоге. Скомпилируйте и запустите сервер: создайте && ./server и введите в браузере http://localhost:8000:

Двигаясь дальше, давайте добавим динамический контент в раздел «Настройки». Мы предполагаем, что в устройстве есть какие-то внутренние настройки. Я использую две переменные в программе для имитации этих настроек:

struct device_settings { 
   char setting1[100]; 
   char setting2[100]; 
}; 
static struct device_settings s_settings = { "value1", "value2" };

Затем я добавляю форму для раздела «Настройки», чтобы отображать значения этих переменных и позволять пользователям изменять их.

Важным моментом здесь является то, что теперь мне нужно показать некоторые внутренние состояния устройства в графическом интерфейсе. Как правило, есть два способа сделать это:

  1. Используйте такие технологии, как PHP или SSI, и вставляйте код прямо в HTML.
  2. Внедрите конечную точку RESTful, сделайте запрос AJAX с веб-страницы и обновите страницу с помощью JavaScript.

Для раздела Настройки я буду использовать подход номер 1. Mongoose поддерживает подмножество SSI, конкретный вызов SSI, который я собираюсь использовать, — это ‹!#call имя_параметра → директива. Я специально назвал индексный файл web_root index.shtml, чтобы директивы SSI обрабатывались Mongoose. Так как же работает директива call SSI? Для каждой директивы вызова Mongoose запускает событие MG_EV_SSI_CALL, передавая строку имя_параметра в качестве параметра события. Обратный вызов затем может напечатать некоторое содержимое, которое заменит содержимое директивы вызова (см. исходный код).

Я добавляю две части в обработчик событий, первая — для сохранения результата отправки формы, а вторая — для обработки событий SSI:

static void ev_handler(struct mg_connection *nc, int ev, void *ev_data) { 
   struct http_message *hm = (struct http_message *)ev_data; 
   switch (ev) { 
     case MG_EV_HTTP_REQUEST: 
       if (mg_vcmp(&hm->uri, "/save") == 0) { 
         handle_save(nc, hm); 
       } else { 
         mg_serve_http(nc, hm, s_http_server_opts); // Serve static content 
       } 
       break; 
     case MG_EV_SSI_CALL: 
       handle_ssi_call(nc, ev_data); 
       break; 
     default: 
       break; 
   } 
}

Когда вы соберете это, запустите сервер и введите http://localhost:8000 в браузере, вот как это выглядит:

Заставьте устройство реализовать RESTful API

Давайте реализуем конечную точку RESTful. В предыдущем разделе я реализовал отдельную конечную точку /save — это была конечная точка для сохранения отправки формы. Эта конечная точка сохраняет значения формы в переменных настроек устройства и перенаправляет на домашнюю страницу. Конечная точка RESTful делается почти так же, с той лишь разницей, что она не перенаправляет, а возвращает результат обратно вызывающей стороне — обычно в формате JSON.

В этом примере я добавляю конечную точку /get_cpu_usage, которая будет отображаться в разделе панели мониторинга. На стороне сервера эта конечная точка получает сведения об использовании процессора (для простоты я создаю поддельное значение) и возвращает его обратно вызывающей стороне в виде объекта JSON:

static void handle_get_cpu_usage(struct mg_connection *nc) { 
   // Generate random value, as an example of changing CPU usage 
   // Getting real CPU usage depends on the OS. 
   int cpu_usage = (double) rand() / RAND_MAX * 100.0; 
   // Use chunked encoding in order to avoid calculating Content-Length 
   mg_printf(nc, "%s", "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n"); 
   // Output JSON object which holds CPU usage data 
   mg_printf_http_chunk(nc, "{ \"result\": %d }", cpu_usage); 
   // Send empty chunk, the end of response 
   mg_send_http_chunk(nc, "", 0); 
}

Теперь, на стороне веб-интерфейса, мне нужно вызвать эту конечную точку RESTful. Я добавляю Jquery, чтобы сделать это с помощью ajax-вызова Jquery, и добавляю файл main.js. В main.js я каждую секунду выполняю RESTful-вызов /get_cpu_usage, отражая результат в разделе панели инструментов:

$(document).ready(function() { 
   // Start 1-second timer to call RESTful endpoint 
   setInterval(function() { 
       $.ajax({ 
         url: '/get_cpu_usage', 
         dataType: 'json', 
         success: function(json) { 
           $('#cpu_usage').text(json.result + '% '); 
         } 
       }); 
   }, 1000); 
});

Найти полный исходный код можно здесь. Когда вы создаете, запускаете сервер и набираете http://localhost:8000 в своем браузере, это выглядит так:

Реализация обмена данными в реальном времени через WebSocket

Переходим к реализации отправки данных сервера в режиме реального времени. Для этого мы создадим соединение WebSocket от клиента, и Mongoose будет периодически передавать ему данные. В качестве примера мы отправляем поддельное использование памяти на панель инструментов и визуализируем его на графике в реальном времени.

Для построения графиков я буду использовать клиентскую библиотеку Flot. Добавьте соответствующие файлы jquery.flot.min.js и jquery.flot.time.js в каталог web_root и свяжите их с index.shtml. В main.js я добавляю код для построения графиков, а также этот фрагмент для захвата запросов WebSocket от Mongoose:

var ws = new WebSocket('ws://' + location.host); 
ws.onmessage = function(ev) { 
   updateGraph(ev.data); 
};

На стороне Mongoose я установил таймер для отправки данных всем подключенным клиентам WebSocket:

for (;;) { 
   static time_t last_time; 
   time_t now = time(NULL); 
   mg_mgr_poll(&mgr, 1000); 
   if (now - last_time > 0) { 
     push_data_to_all_WebSocket_connections(&mgr); 
     last_time = now; 
   } 
}

А вот реализация реального push-уведомления через WebSocket:

static void push_data_to_all_WebSocket_connections(struct mg_mgr *m) { 
   struct mg_connection *c; 
   int memory_usage = (double) rand() / RAND_MAX * 100.0; 
   for (c = mg_next(m, NULL); c != NULL; c = mg_next(m, c)) { 
      if (c->flags & MG_F_IS_WebSocket) { 
         mg_printf_WebSocket_frame(c, WebSocket_OP_TEXT, "%d", memory_usage); 
       } 
   } 
}

Полный исходный код ищите здесь. И снова, когда вы создаете, запускаете сервер и набираете http://localhost:8000 в своем браузере, это выглядит так:

Попробуйте сами

Mongoose упрощает подключение устройств. Вы найдете его безопасным, надежным и чрезвычайно легким с ядром менее 40 КБ.

Я намерен сделать Mongoose еще лучше. Итак, я надеюсь, вам понравились примеры, протестируйте Mongoose сами и сообщите нам, как у вас дела!

Вы можете запустить приведенный выше пример и другие, загрузив Mongoose. Получите исходный код здесь.

Первоначально опубликовано на blog.cesanta.com.