Изучим протокол передачи гипертекста 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 подключенный к роутеру прямым кабелем.
Структура протокола.
Клиент и сервер обмениваются сообщениями – блоками данных.
В общем виде обмен информацией происходит так:
- клиент устанавливает с сервером TCP-соединение;
- посылает на сервер сообщение-запрос;
- в ответ получает сообщение-ответ;
- дальше TCP-соединение может быть разорвано или ожидать новых блоков данных.
Каждое сообщение состоит из 3 частей:
- Стартовая строка – задает тип сообщения.
- Заголовки – дают серверу дополнительную информацию о запросе, о клиенте, параметры обмена и прочие технические сведения.
- Тело сообщения – собственно данные, информация. Тело заголовка должно обязательно отделяться от предыдущих частей пустой строкой.
Тело сообщения может отсутствовать, особенно, в запросе клиента. А стартовая строка и заголовки должны присутствовать в любом сообщении.
Структура запроса.
Запрос это сообщение от клиента серверу.
Давайте напишем простую программу, позволяющую увидеть запросы клиента. Немного изменим первую программу из урока 64.
// 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-запрос на сервер. Мы увидим его в мониторе последовательного порта.
Стартовая строка.
Стартовая строка запроса имеет следующий формат:
МЕТОД 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-запроса это окончание запроса);
- послать ответ клиенту;
- разорвать соединение;
- все операции запротоколировать в мониторе последовательного порта.
Вот скетч такого сервера.
// 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.
Более сложный WEB-сервер.
Первый вариант сервера выводит одну и ту же статичную информацию. Мы его создали для демонстрации принципа организации HTTP-обмена. Вряд ли он будет востребован на практике.
Но ничего не мешает в сообщении-ответе сервера передавать клиенту динамические данные. Например, состояние кнопок, измеренное на аналоговых входах напряжение, температуру и т.п.
Чтобы не подключать к плате Ардуино дополнительные элементы, давайте передадим клиенту значение функции millis(), т.е. время с момента включения сервера.
Заодно немного приукрасим текстовую информацию, которая появится в окне браузера.
Информация для браузера в теле сообщения обычно передается HTML-кодом. Это совершенно другая, очень объемная тема. На нее есть прекрасные уроки. Для разработки WEB-серверов необходимо знать основы HTML-языка. Я покажу, как упростить процесс создания HTML-сообщений.
Я предлагаю использовать HTML-редактор. В окне такого редактора вы набираете текст, задаете шрифты, размеры фрагментов, раскрашиваете их, полностью оформляете текстовую информацию. На выходе получаете HTML-код, который можете передавать клиенту.
Существует огромное количество простых и сложных, платных и бесплатных редакторов. Можете выбрать по своему вкусу. Чтобы не заниматься загрузкой и установкой, я использовал онлайн HTML-редактор Vulk. Он мне первым попался под руку.
Я создавал сервер в такой последовательности.
- Запустил HTML-редактор http://vulk.ru/.
- В меню выбрал Визуальный редактор.
- Написал и раскрасил текстовую информацию. Оформить, конечно, можно было и красивее.
Число 34,56 – показатель времени я пока задал статическим.
- Нажал закладку 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() через указатель.
Загрузим скетч.
При трансляции получим сообщение о том, что оперативная память почти заполнена.
Пока проигнорируем и запустим браузер. Запросим сервер.
Мы получили статическое сообщение, оформленное нами в 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>"));
Вот скетч полностью.
Теперь трансляция проходит без пугающих предупреждений. И оперативной памяти используется намного меньше, 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>"));
А весь скетч будет таким.
В браузере увидим время.
Можно добавить в заголовки ответа строку
client.println(F("Refresh: 2")); // обновить страницу автоматически
Тогда браузер будет сам обновлять страницу каждые 2 секунды.
В течение часа мой сервер отработал в таком режиме. Никаких сбоев, зависаний.
Работает сервер и при обращении с телефона.
В следующем уроке продолжим тему. Научимся передавать данные от клиента серверу с помощью GET и POST запросов. Разработаем простой WEB-клиент.
Приветствую. Я тоже занимался созданием интернет страницы и измерением температуры и управлением выводами Arduino. Куда можно выложить мои наработки ?
Здравствуйте!
На форуме сайта.
«Поэтому кавычки внутри 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() {}
Приятного вечера Эдуард, пример кода который я продемонстрировал в комментарии, отображается искаженным!
Чтобы, не вводить в заблуждение других людей,
Пожалуйста удалите Мой комментарий — или отредактируйте.
Благодарю вас, за ваш труд.
«Поэтому кавычки внутри HTML-кода надо экранировать символом ”\”.»
в (C++ 11) Можно обойтись и без — «обратный слеш \» — (через Префикс R)
так проще, читать — писать — тестировать через Serial Monitor команды.
Префикс R (от англ. raw — “сырой”) позволяет ввести строку без экранирования символов:
все символы между R“ разделитель ( и ) разделитель ” включаются в литерал.
Serial.println(R»(«»»»»»)»);
Serial.println(F(R»(«»»»»»)»));
Ооо! Известные люди подтянулись в комментариях))?
Спасибо за :
client.println(«Content-Type: text/html; charset=utf-8»); // тело передается в коде HTML, кодировка UTF-8
Я новичок, искал неумеючи способы борьбы с «иероглифами» , намаялся, намучился, а тут такая простая красота!
Эдуард, здравствуйте, подскажите, а как сделать не статическую надпись, а динамическую
Здравствуйте!
Я не силен в веб программировании. Насколько я знаю, такого HTML-объекта нет. Надо использовать скрипты, выполняемые на стороне клиента.