Урок 70. Протокол HTTP. Создание WEB-сервера на Ардуино. Использование HTML-кода.

HTTP

Изучим протокол передачи гипертекста HTTP. Научимся разрабатывать WEB серверы в системе Ардуино. Покажу простой способ оформлять информацию для браузера с помощью HTML-редактора.

Предыдущий урок     Список уроков     Следующий урок

Обычно уроки я строю по принципу – даю формальное описание функций, методов, протоколов…, а затем, используя эту информацию, разрабатываю учебные проекты.

Информация о протоколе HTTP объемная. Что-то нам не пригодится, поэтому в этом уроке я решил все перемешать. Последовательно рассказывать о протоколе и тут же демонстрировать его применение на практике.

 

Я не претендую на полноту освещения этой объемной темы. Рассмотрю варианты использования HTTP протокола наиболее приемлемые для систем на базе Ардуино.

 

Общая информация.

HTTP - это протокол уровня приложений (высокого уровня) для передачи гипертекстовых документов.

Текстовая информация разбита на фрагменты. Между ними существуют видимые ссылки, по которым можно переходить от фрагмента к фрагменту. Так вот протокол HTTP обеспечивает просмотр текста, организованного таким образом.

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

В настоящее время HTTP используется сервисом World Wide Web (Всемирная Сеть) для получения информации с WEB-сайтов. На ваш компьютер информация с сайтов интернета попадает через именно этот протокол. И то, что вы читаете сейчас доставлено посредством HTTP.

Надо твердо помнить, что:

  • HTTP работает по технологии клиент-сервер. Клиент посылает запрос и получает в ответ информацию с сервера. В нашем случае:
    •  клиентом является WEB-браузер на компьютере;
    • сервером – плата Ардуино с сетевым Ethernet-модулем ENC28j60.
  • Запрос будет формировать браузер, отвечать – удаленный сервер.
  • HTTP – протокол уровня приложений, т.е. приложение на компьютере (веб-браузер) обменивается данными с приложением на удаленном сервере (в нашем случае с резидентной программой Ардуино).
  • Обмен данными происходит через TCP-соединение.
  • Для создания WEB-сервера нет специальных функций библиотеки. Мы будем устанавливать TCP-соединение и через него передавать данные в формате HTTP.
  • Основной объект для HTTP протокола – это ресурс (URI – Uniform Resource Identifier ), который задается в запросе клиента. Обычно ресурсом являются файлы, расположенные на сервере, но может быть что угодно, даже нечто абстрактное. Для идентификации ресурсов используются глобальные идентификаторы URI, т.е. адреса Интернета.
  • Стандартным портом для HTTP считается порт 80.
  • Сама текстовая информация обычно передается на языке гипертекстовой разметки HTML.

Аппаратная часть, для которой мы будем разрабатывать программы в этом уроке, описывается в уроке 63 и была нами использована в уроках 64 и 69. Это плата Arduino UNO R3 и модуль ENC28J60 подключенный к роутеру прямым кабелем.

Arduino UNO и ENC28J60

 

Структура протокола.

Клиент и сервер обмениваются сообщениями – блоками данных.

В общем виде обмен информацией происходит так:

  • клиент устанавливает с сервером TCP-соединение;
  •  посылает на сервер сообщение-запрос;
  • в ответ получает сообщение-ответ;
  • дальше TCP-соединение может быть разорвано или ожидать новых блоков данных.

Каждое сообщение состоит из 3 частей:

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

Тело сообщения может отсутствовать, особенно, в запросе клиента. А стартовая строка и заголовки должны присутствовать в любом сообщении.

 

Структура запроса.

Запрос это сообщение от клиента серверу.

Давайте напишем простую программу, позволяющую увидеть запросы клиента. Немного изменим первую программу из урока 64.

 Зарегистрируйтесь и оплатите. Всего 25 руб. в месяц за доступ ко всем ресурсам сайта! 

// TCP сервер, выводит полученные данные
#include <SPI.h>
#include <UIPEthernet.h>

// определяем конфигурацию сети
byte mac[] = {0xAE, 0xB2, 0x26, 0xE4, 0x4A, 0x5C}; // MAC-адрес
byte ip[] = {192, 168, 1, 10}; // IP-адрес

EthernetServer server(80); // создаем сервер, порт 80
EthernetClient client; // объект клиент
boolean clientAlreadyConnected= false; // признак клиент уже подключен

void setup() {
  Ethernet.begin(mac, ip); // инициализация контроллера
  server.begin(); // включаем ожидание входящих соединений
  Serial.begin(9600);
  Serial.print("Server address:");
  Serial.println(Ethernet.localIP()); // выводим IP-адрес контроллера
}

void loop() {
  client = server.available(); // ожидаем объект клиент
  if (client) {
    // есть данные от клиента
    if (clientAlreadyConnected == false) {
      // сообщение о подключении
      Serial.println("Client connected");
      Serial.println("");
      clientAlreadyConnected= true;
    }

    while(client.available() > 0) {
      char chr = client.read(); // чтение символа
      Serial.write(chr);
    }
  }
}

Программа:

  • при запуске выводит IP-адрес сервера;
  • при установке TCP-соединения выдает сообщение;
  • выводит в монитор последовательного порта все, что поступает с клиента.

Запустим.

Окно монитора

В строке адреса браузера наберем IP-адрес сервера (у меня 192.168.1.10).

Запрос браузера

Браузер пошлет HTTP-запрос на сервер. Мы увидим его в мониторе последовательного порта.

HTTP-запрос

Стартовая строка.

Стартовая строка запроса имеет следующий формат:

МЕТОД  URI  HTTP/Версия

  • Метод – тип запроса.
  • URI – идентификатор ресурса. Определяет путь к запрашиваемому файлу.
  • Версия – разделенные точкой цифры.

В нашем случае:  GET / HTTP/1.1

  • GET – метод.
  • / - URI. Мы обратились к серверу без указания дополнительных идентификаторов ресурса, т.е. просто по адресу 192.168.1.10.
  • HTTP/1.1 – версия HTTP.

 

Методы.

Методы это команды серверу. Они сообщают серверу, что он должен делать.

Состоят из любых символов, кроме управляющих и разделителей.

Методы чувствительны к регистру.

Система может использовать любые методы, даже не документированные в протоколе. Если сервер не распознал указанный метод, он должен вернуть код состояния 501. Если метод известен, но не может быть применим к указанному ресурсу, то возвращается код 405.

Любой сервер обязан обрабатывать методы GET и HEAD.

На практике востребованными являются только методы GET и POST. Мы будем использовать только эти запросы.

Метод Описание
GET Применяется для запроса содержимого ресурса. GET-запрос может запустить какой-либо процесс на сервере, информации о ходе выполнения которого обычно передается в ответном сообщении.

Через GET-запросы клиент может передавать параметры. Параметры отделятся от URI символом “?”. Параметры разделяются символом “&”.

GET /path/resourse?parameter1=value1& parameter2=value2 HTTP/1.1

HEAD Действие аналогично методу GET, за исключением того, что в ответе отсутствует тело сообщения.
POST Используется для передачи данных ресурсу. Например, комментарии к записям сайта или заполнение регистрационных форм. Пользователь вводит текст в окне браузера, данные передаются на сервер и сохраняются там. Также, с помощью POST-метода на сервер загружаются файлы.

Передаваемые методом POST данные включаются в тело запроса.

В следующем уроки, при реализации методов  GET и POST, я расскажу о них подробнее.

 

Заголовки.

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

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

В нашем запросе блок заголовков выглядит так.

Host: 192.168.1.10
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7

Почти вся информация понятна без расшифровки.

Заголовки разделяются на 4 группы:

  • Основные – могут включаться в любое сообщение сервера и клиента.
  • Заголовки запроса – применяются только в запросах клиентов.
  • Заголовки ответа – используются только в ответах от серверов.
  • Заголовки сущности – используются для описания тела сообщения. Определяют вид представления информации конечному пользователю.

Если не хватает стандартных, то заголовки можно вводить свои.

 

Тело сообщения.

В запросе тело сообщения бывает только при использовании метода POST. Об этом в следующем уроке.

 

Структура ответа.

После получения и обработки запроса, сервер посылает клиенту ответ.

Стартовая строка ответа должна выглядеть так:

HTTP/ВЕРСИЯ  КодСостояния  Пояснения

  • Версия – разделенные точкой цифры.
  • Код состояния – число из 3 цифр. Говорит клиенту о результате выполнения запроса и определяет его дальнейшие действия. Набор кодов состояния описан в протоколе.
  • Пояснение – короткое текстовое пояснение кода состояния. Необязательное поле, не влияющее на действия клиента.

Стартовая строка ответа может выглядеть так:

HTTP/1.1 200 OK

 

Код состояния.

Представляет собой 3-значное число. Первая цифра определяет класс состояния.

Необязательный параметр стартовой строки ПОЯСНЕНИЕ разъясняет код состояния в  коротком текстовом сочетании.

200 OK
102 Processing
202 Accepted
304 Not Modified
407 Proxy Authentication Required
500 Internal Server Error

По коду ответа клиент узнает о результатах его запроса и решает, какие действия предпринять.

Существует 5 классов кодов.

Код состояния Класс Назначение
1XX Информационный Информирование о процессе передачи данных.
2XX Успех Информирование об успешной обработке запроса клиента.
3XX Перенаправление Сообщает клиенту о необходимости сделать запрос по другому URI.
4XX Ошибка клиента Информирует об ошибках со стороны клиента.
5XX Ошибка сервера Информирует о неудачных операциях по вине сервера.

 

Тело сообщения.

Тело сообщение это собственно информация ресурса, которую запросил клиент. Кодировка  и формат данных указывается в поле заголовков. Тело сообщений, предназначенных для браузеров, содержит HTML-код.

Конечно, это значительно сокращенная форма представления протокола HTTP. Но для наших целей вполне достаточно.

 

Создание простого WEB-сервера.

Для начала разработаем самый простой WEB-сервер, который по GET-запросу клиента будет передавать одну и ту же текстовую строку.

Надо:

  • установить TCP-соединение;
  • принять запрос клиента;
  • выделить в нем пустую строку, те окончание блока заголовков (для простого GET-запроса это окончание запроса);
  • послать ответ клиенту;
  • разорвать соединение;
  • все операции запротоколировать в мониторе последовательного порта.

Вот скетч такого сервера.

 Зарегистрируйтесь и оплатите. Всего 25 руб. в месяц за доступ ко всем ресурсам сайта! 

// WEB сервер, передает клиенту текстовую строку
#include <SPI.h>
#include <UIPEthernet.h>

// определяем конфигурацию сети
byte mac[] = {0xAE, 0xB2, 0x26, 0xE4, 0x4A, 0x5C}; // MAC-адрес
byte ip[] = {192, 168, 1, 10}; // IP-адрес

EthernetServer server(80); // создаем сервер, порт 80
EthernetClient client; // объект клиент

boolean flagEmptyLine = true; // признак строка пустая
char tempChar;

void setup() {
  Ethernet.begin(mac, ip); // инициализация контроллера
  server.begin(); // включаем ожидание входящих соединений
  Serial.begin(9600);
  Serial.print("Server address:");
  Serial.println(Ethernet.localIP()); // выводим IP-адрес контроллера
  Serial.print("");
}

void loop() {
  client = server.available(); // ожидаем объект клиент
  if (client) {
    flagEmptyLine = true;
    Serial.println("New request from client:");

    while (client.connected()) {
      if (client.available()) {
        tempChar = client.read();
        Serial.write(tempChar);

        if (tempChar == '\n' && flagEmptyLine) {
          // пустая строка, ответ клиенту
          client.println("HTTP/1.1 200 OK"); // стартовая строка
          client.println("Content-Type: text/html; charset=utf-8"); // тело передается в коде HTML, кодировка UTF-8
          client.println("Connection: close"); // закрыть сессию после ответа
          client.println(); // пустая строка отделяет тело сообщения
          client.println("<!DOCTYPE HTML>"); // тело сообщения
          client.println("<html>");
          client.println("<H1> Первый WEB-сервер</H1>"); // выбираем крупный шрифт
          client.println("</html>");
          break;
        }
        if (tempChar == '\n') {
          // новая строка
          flagEmptyLine = true;
        }
else if (tempChar != '\r') {

          // в строке хотя бы один символ
          flagEmptyLine = false;
        }
      }
    }
    delay(1);
    // разрываем соединение
    client.stop();
    Serial.println("Break");
  }
}

Загрузил скетч в плату.

Запустил браузер GoogleChrome.

Набрал адрес моего сервера 192.168.1.10. В браузере появилось сообщение.

Ответ в браузере

Сообщение состояло из иероглифов, пока я не задал правильную кодировку строкой:

client.println("Content-Type: text/html; charset=utf-8");  // тело передается в коде HTML, кодировка UTF-8

Ардуино для кириллических символов использует кодировку UTF-8. В большинстве браузеров по умолчанию другая кодировка.

Этот пример показывает, как заголовки влияют на отображение информации.

В мониторе последовательного порта видно, что появилось 2 запроса.

Запрос в окне монитора

Вторым запросом Google Chrome пытается получить с сервера иконку favicon, чтобы отобразить ее рядом с информацией ресурса.

Я перешел на браузер Internet Explorer.

Браузер Internet Explorer

Запрос в окне монитора

В нем только один запрос. Дальнейшие проверки я буду производить с веб-обозревателем Internet Explorer.

 

Более сложный WEB-сервер.

Первый вариант сервера выводит одну и ту же статичную информацию. Мы его создали для демонстрации принципа организации HTTP-обмена. Вряд ли он будет востребован на практике.

Но ничего не мешает в сообщении-ответе сервера передавать клиенту динамические данные. Например, состояние кнопок, измеренное на аналоговых входах напряжение, температуру и т.п.

Чтобы не подключать к плате Ардуино дополнительные элементы, давайте передадим клиенту значение функции millis(), т.е. время с момента включения сервера.

Заодно немного приукрасим текстовую информацию, которая появится в окне браузера.

Информация для браузера в теле сообщения обычно передается HTML-кодом. Это совершенно другая, очень объемная тема. На нее есть прекрасные уроки. Для разработки WEB-серверов необходимо знать основы HTML-языка. Я покажу, как упростить процесс создания HTML-сообщений.

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

Существует огромное количество простых и сложных, платных и бесплатных редакторов. Можете выбрать по своему вкусу. Чтобы не заниматься загрузкой и установкой, я использовал онлайн HTML-редактор Vulk. Он мне первым попался под руку.

Я создавал сервер в такой последовательности.

  • Запустил HTML-редактор  http://vulk.ru/.
  • В меню выбрал Визуальный редактор.

HTML-редактор

  • Написал и раскрасил текстовую информацию. Оформить, конечно, можно было и красивее.

HTML-редактор

Число 34,56 – показатель времени я временно задал статическим.

  •  Нажал закладку HTML в левом нижнем углу редактора.

HTML-редактор

Эти строчки надо передать клиенту.

<div style="text-align: center;"><font size="6" color="#0000ff">Время работы сервера:</font></div><div style="text-align: center;"><font color="#ff0000" size="6"><b>34,56</b></font><font size="4" color="#0000ff"> </font><font size="5">сек</font></div>

Т.е. вставить их в тело сообщения.

client.println("<!DOCTYPE HTML>"); // тело сообщения
client.println("<html>");
client.println("<div style=\"text-align: center;\"><font size=\"6\" color=\"#0000ff\">Время работы сервера:</font></div><div style=\"text-align: center;\"><font color=\"#ff0000\" size=\"6\"><b>34,56</b></font><font size=\"4\" color=\"#0000ff\"> </font><font size=\"5\">сек</font></div>");
client.println("</html>");

Символ “двойные кавычки” (“) используется в функции prrint() для задания строки. Поэтому кавычки внутри HTML-кода надо экранировать символом ”\”.

Другой вариант – подготовить текстовую строку и передать ее в функцию print() через указатель.

Загрузим скетч.

 Зарегистрируйтесь и оплатите. Всего 25 руб. в месяц за доступ ко всем ресурсам сайта! 

При трансляции получим сообщение о том, что оперативная память почти заполнена.

Отчет компиляции

Пока проигнорируем и запустим браузер. Запросим сервер.

Окно браузера

Мы получили статическое сообщение, оформленное нами в HTML-редакторе.

Беспокоит предупреждение при компиляции “Недостаточно памяти, программа может работать нестабильно.” Причина его понятна. Мы задали много текстовой информации в строках функции print() и почти заполнили всю оперативную память. А сообщения могут быть намного длиннее.

Выход – хранить текстовую информацию не в ОЗУ, а в FLASH памяти. У микроконтроллера ATmega 328 объем FLASH 32 кбайт, а ОЗУ всего 2 кбайт.

Это делается просто. Достаточно использовать макрос F() библиотеки pgmspace.h. Я писал о ней в уроке 27, но макроса F() не касался.

Так вот, если текстовую строку разместить в качестве аргумента макроса F(), то она будет сохранена в FLASH.

Serial.print(“test”);        // строка ”test” размещается в ОЗУ
Serial.print(F(“test”));   // строка ”test” размещается в FLASH

Перепишем заголовки и тело сообщения с учетом вышесказанного.

client.println(F("HTTP/1.1 200 OK")); // стартовая строка
client.println(F("Content-Type: text/html; charset=utf-8")); // тело передается в коде HTML, кодировка UTF-8
client.println(F("Connection: close")); // закрыть сессию после ответа
client.println(); // пустая строка отделяет тело сообщения

client.println(F("<!DOCTYPE HTML>")); // тело сообщения
client.println(F("<html>"));
client.println(F("<div style=\"text-align: center;\"><font size=\"6\" color=\"#0000ff\">Время работы сервера:</font></div><div style=\"text-align: center;\"><font color=\"#ff0000\" size=\"6\"><b>34,56</b></font><font size=\"4\" color=\"#0000ff\"> </font><font size=\"5\">сек</font></div>"));
client.println(F("</html>"));

Вот скетч полностью.

 Зарегистрируйтесь и оплатите. Всего 25 руб. в месяц за доступ ко всем ресурсам сайта! 

Теперь трансляция проходит без пугающих предупреждений.  И оперативной памяти используется намного меньше, 1318 байт . В предыдущем варианте программы было 1696.

Отчет компиляции

Здесь я вынужден сделать небольшое отступление. До последнего варианта программы все работало идеально. Как только я стал использовать в программе макрос F() сервер начал виснуть. На второе, третье обращение. Я убрал макрос и вставил несколько одинаковых строк с функцией println(). Сервер периодически зависал. Я заменил плату Arduino UNO R3, затем  модуль ENC28j60. Ничего не помогало. Система заработала стабильно только после того, как я подключил модуль к плате Arduino Nano. Мощности стабилизатора платы 3,3 В для питания ENC28j60 явно не хватает. Поэтому я использовал внешний стабилизатор LD1117.

 

После этого все заработало идеально. К серверу по последнему варианту программы я сделал более 2000 запросов.

Я не стал разбираться в причине происходящего. Могу предположить, что либо не хватает пиковой мощности стабилизатора 3,3 В платы Arduino UNO, либо библиотека UIPEthernet не работает с этой платой. Если кто-то разберется в этом вопросе – напишите. Я добавлю эту информацию в урок. Можно создать тему на форуме сайта.

Дальнейшие испытания я производил на связке Arduino Nano - модуль ENC28j60.

Последний вариант сервера дает в ответе только статичное сообщение. Давайте часть строки ”34,56” заменим на вывод значения функции millis().

Функция millis() возвращает значение времени в мс, а нам надо в секундах. Сделаем простейшее преобразование.

client.print( (float)millis() /1000. ); // вывод времени в секундах

Разорвем длинную строку HTML кода и вместо 34,56 вставим вывод времени.

В итоге тело сообщения будет выглядеть так.

client.println(F("<!DOCTYPE HTML>")); // тело сообщения
client.println(F("<html>"));
client.print(F("<div style=\"text-align: center;\"><font size=\"6\" color=\"#0000ff\">Время работы сервера:</font></div><div style=\"text-align: center;\"><font color=\"#ff0000\" size=\"6\"><b>"));
client.print( (float)millis() /1000. );
client.println(F("</b></font><font size=\"4\" color=\"#0000ff\"> </font><font size=\"5\">сек</font></div>"));
client.println(F("</html>"));

А весь скетч будет таким.

 Зарегистрируйтесь и оплатите. Всего 25 руб. в месяц за доступ ко всем ресурсам сайта! 

В браузере увидим время.

Окно браузера

Можно добавить в заголовки ответа строку

client.println(F("Refresh: 2"));  // обновить страницу автоматически

Тогда браузер будет сам обновлять страницу каждые 2 секунды.

 Зарегистрируйтесь и оплатите. Всего 25 руб. в месяц за доступ ко всем ресурсам сайта! 

В течение часа мой сервер отработал в таком режиме. Никаких сбоев,  зависаний.

Браузер телефона

Работает сервер и при обращении с телефона.

 

В следующем уроке продолжим тему. Научимся передавать данные от клиента серверу с помощью GET и POST запросов. Разработаем простой WEB-клиент.

Предыдущий урок     Список уроков     Следующий урок

0

Автор публикации

не в сети 3 дня

Эдуард

8
Комментарии: 1128Публикации: 124Регистрация: 13-12-2015

5 комментариев на «Урок 70. Протокол HTTP. Создание WEB-сервера на Ардуино. Использование HTML-кода.»

  1. Приветствую. Я тоже занимался созданием интернет страницы и измерением температуры и управлением выводами Arduino. Куда можно выложить мои наработки ?

    0
  2. «Поэтому кавычки внутри HTML-кода надо экранировать символом ”\”.»
    в (C++ 11) Можно обойтись и без — «обратный слеш \» — (через Префикс R)
    так проще, читать — писать — тестировать через Serial Monitor команды.
    Префикс R (от англ. raw — “сырой”) позволяет ввести строку без экранирования символов:
    все символы между R“ разделитель ( и ) разделитель ” включаются в литерал.

    void setup() {
    Serial.begin(9600);

    Serial.println(R»(«Время работы сервера:
    34,56
    сек»)»);

    Serial.println();

    Serial.println(F(R»(«Время работы сервера:
    34,56
    сек»)»));

    Serial.println();

    char* r_str = R»(«Время работы сервера:
    34,56
    сек»)»;

    Serial.println();

    Serial.println(r_str);
    }
    void loop() {}

    0
  3. Приятного вечера Эдуард, пример кода который я продемонстрировал в комментарии, отображается искаженным!
    Чтобы, не вводить в заблуждение других людей,
    Пожалуйста удалите Мой комментарий — или отредактируйте.
    Благодарю вас, за ваш труд.

    «Поэтому кавычки внутри HTML-кода надо экранировать символом ”\”.»
    в (C++ 11) Можно обойтись и без — «обратный слеш \» — (через Префикс R)
    так проще, читать — писать — тестировать через Serial Monitor команды.
    Префикс R (от англ. raw — “сырой”) позволяет ввести строку без экранирования символов:
    все символы между R“ разделитель ( и ) разделитель ” включаются в литерал.

    Serial.println(R»(«»»»»»)»);

    Serial.println(F(R»(«»»»»»)»));

    0

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *