В уроке покажу, как использовать POST-запросы для передачи данных на сервер. Разработаем WEB-сервер, обрабатывающий методы GET и POST. Узнаем, как формировать POST-запросы с помощью HTML-формы.
Предыдущий урок Список уроков Следующий урок
Еще раз повторю, что в протоколе HTTP для передачи данных WEB-серверу от клиента могут использоваться два способа, два типа запроса: с методом GET и методом POST.
- GET-запрос передает информацию, как часть идентификатора ресурса (URI). Т.е. через адресную строку браузера. Пользователь видит ее, и даже может сам создать.
- POST-запрос передает данные в теле сообщения запроса.
Главное достоинство GET-запросов – компактность. В одной строке адресная информация и передаваемые данные. А с помощью POST-запросов информация прячется где-то внутри запроса. Она недоступна простому пользователю.
Из этого вытекают все плюсы и минусы обоих методов. Достоинства и недостатки GET и POST-запросов зеркально-противоположные.
GET-запрос | POST-запрос | ||
+ - |
Информация передается в адресной строке. | + - |
Информация передается в теле сообщения. |
+ | Информация может быть сформирована и передана обычным пользователем, т.е. набрана в адресной строке браузера. Проще отлаживать обработку запросов. | - | Пользователь не имеет прямого доступа к передаваемой информации. |
- | Передаваемая информация видима в адресной строке браузера. Для секретных данных это недопустимо. | + | Передаваемая информация не видна для пользователя, а значит и для злоумышленника. |
- | Информация в адресных заголовках имеет уродливый вид – последовательность непонятных символов. | + | Красивые адресные строки, ничего лишнего, непонятного. |
+ | Данные для передачи можно сохранить стандартными средствами браузера, например, в закладках. | - | Данные невозможно сохранить стандартными средствами. |
- | Данные для передачи сохраняются стандартными средствами браузера, например, в истории. Могут быть прочитаны злоумышленником. | + | Данные не сохраняются в истории браузера. Злоумышленник их там не найдет. |
+ | Данные передаются быстрее по сравнению с POST-методом. | - | Данные передаются медленнее по сравнению с GET-методом. |
- | Объем информации для передачи ограничен. | + | Могут быть переданы неограниченные объемы информации. |
+ | Для обработки требуется меньше вычислительных ресурсов сервера. | - | Требует больших вычислительных ресурсов сервера. |
Каким способом передавать данные на сервер выбирать вам.
Но обратите внимание на последнюю строку таблицы. У WEB-сервера, реализованного на микроконтроллере, вычислительные ресурсы крайне ограничены.
И еще учтите, что GET-запросы должен обрабатывать любой WEB-сервер, а POST-запросы придется обрабатывать только для приема данных. Добавить обработку URI в GET-запросе намного проще, чем добавить на сервер еще один метод. Вы это прочувствуете ниже, при реализации сервера.
Формализованная постановка задачи.
Мы собираемся разработать WEB-сервер, работающий по следующему алгоритму:
- Пользователь набирает в адресной строке браузера адрес сервера.
- Браузер (клиент) формирует GET-запрос по этому адресу.
- Наш сервер по GET-запросу передает браузеру (клиенту) HTML-код.
- Браузер отображает на экране графические HTML-компоненты: кнопки, флажки, поля ввода…
- Пользователь заполняет поля и нажимает кнопку отправить.
- Браузер (клиент) посылает на наш сервер POST-запрос со значениями переменных формы.
- Сервер выводит полученные переменные в последовательный порт для контроля.
Для отладки программы по частям нам надо будет формировать POST-запросы с компьютера. Давайте напишем HTML-код, посылающий запросы на наш сервер. Сначала будем запускать его из браузера, а затем перенесем на сервер в сообщение ответа на GET-запрос.
Формирование POST-запросов средствами HTML. HTML-формы.
В HTML существует интерактивный элемент, позволяющий производить передачу данных на сервер. Это форма. Вы встречались с формой, когда регистрировались на сайте, писали комментарии и во многих других случаях.
HTML-форма – это поле, раздел документа, дающий пользователю возможность вводить информацию для передачи на сервер. Передача может происходить как GET, так и POST-запросами. Сейчас нас интересует второй метод.
Я приведу минимальный объем информации для работы с формами. Все остальное найдете в учебниках по HTML.
Форма – блочный элемент. Все элементы формы заключаются в теги:
<form>
. . . . . .
</form>
У формы несколько необязательных атрибутов и два обязательных:
- action - содержит URI обработчика формы, т.е. сервера;
- method – метод отправки данных post или get.
Можно добавить атрибут name – имя формы.
В нашем случае:
<form action="http://192.168.1.10" method="post" name="form">
Остается добавить нужные элементы формы.
Основной элемент - <input атрибуты>.
Позволяет создавать различные поля для ввода числовой и текстовой информации.
Нужных нам атрибута 3:
- name – имя элемента. Через него обработчик формы идентифицирует элемент. В тексте запроса на сервер именно этим атрибутом определяется принадлежность информации конкретному полю формы. На сервер отправляется ”имя=значение”. Например, при атрибуте name=”surname” и тексте в поле ”Ivanov” в запросе будет surname=Ivanov.
- type – определяет тип элемента, т.е. что рисовать, что вводить. Ниже подробно описан.
- value – значение элемента. В зависимости от атрибута type:
- для кнопок устанавливает на них текстовую надпись;
- для текстовых полей задает предварительно введенный текст;
- для переключателей и флажков определяет каждый элемент, чтобы сервер узнал, какой пункт был выбран.
Другие элементы формы.
Элемент, формат |
Описание | Вид |
<textarea> </textarea> | Многострочный текст, изменяется размер поля. | |
<select> </select> | Выпадающий список. |
С помощью любого текстового редактора, например Notepad, напишем простую HTML-форму регистрации.
<form action="http://192.168.1.10" method="post" name="form">
<br>
Имя: <input name="fname" type="text" value=""><br><br>
Фамилия: <input name="sname" type="text" value=""><br><br>
Возраст: <input name="age" type="number" value=""><br><br>
Пол:
М <input name="sex" type="radio" value="male">
Ж <input name="sex" type="radio" value="female"><br><br>
Любимый вид спорта:
<select name="sport">
<option value="Football"> Футбол
<option value="Volleyball"> Волейбол
<option value="Basketball"> Баскетбол
<option value="Swimming"> Плавание
<option value="Nothing"> Нет
</select><br><br>
О себе:<br>
<textarea cols="40" rows="8" name="about"></textarea><br><br>
<input type="submit" name="send" value="Отправить">
</form>
Сохраним в файле с именем TestForm.html.
В ней: текстовые поля, поля для чисел, переключатель, выпадающий список, многострочный текст и кнопка отправки данных.
Запустим файл из любого браузера.
Теперь мы можем отправлять POST-запросы из браузера.
Формат данных в POST-запросах.
Посмотрим, как данные передаются в запросе. Будем использовать программу вывода данных TCP-пакета в последовательный порт, написанную еще в уроке 70.
- Загрузим программу в плату.
- Откроем монитор последовательного порта.
- Запустим файл с HTML-кодом формы TestForm.html.
- Заполним форму и нажмем кнопку ”Отправить”.
Я заполнил форму на английском языке, для того чтобы было проще увидеть значения переменных. При использовании кириллицы символы передавались бы кодами в текстовом виде. Ниже я приведу пример.
В мониторе последовательного порта появится текст POST-запроса.
В отличие от GET-запроса, к стартовой строке и заголовкам добавилось тело сообщения.
fname=Ivan&sname=Ivanov&age=30&sex=male&sport=Swimming&about=I+love+to+travel.&send=%CE%F2%EF%F0%E0%E2%E8%F2%FC
В нем переданы значения переменных формы. Формат:
имя_элемента=значение&имя_элемента=значение& . . . имя_элемента=значение
- Имя_элемента это имя, заданное для элемента в HTML-форме.
- Значение, то, что будет набрано в форме или то, что соответствует выбранному значению для переключателей, выпадающих списков и т.п.
- Символ & разделяет переменные.
Для ввода имени мы создали объект: <input name="fname" type="text" value="">,
назвали его: fname,
набрали в форме: Ivan,
получили: fname=Ivan.
Для выбора пола создали переключатель: <input name="sex" type="radio" value="male">,
назвали: sex (это по-английски пол, а не то, что вы подумали),
выбрали в форме: мужской,
получили: sex=male.
Последняя переменная для кнопки ”Отправить”. Значение для нее было задано на русском языке. Поэтому кириллические символы переданы кодами в текстовом виде.
send=%CE%F2%EF%F0%E0%E2%E8%F2%FC
Если заполнить форму на русском языке полностью, то данные POST-запроса будут выглядеть так.
fname=%C8%E2%E0%ED&sname=%C8%E2%E0%ED%EE%E2&age=30&sex=male&sport=Swimming&about=%CB%FE%E1%EB%FE+%EF%F3%F2%E5%F8%E5%F1%F2%E2%EE%E2%E0%F2%FC.&send=%CE%F2%EF%F0%E0%E2%E8%F2%FC
Обработка POST-запроса. Разработка WEB-сервера.
Давайте для начала выделим данные POST-запроса в текстовую строку. Для контроля выведем ее в последовательный порт.
Сразу же появилась проблема – как определить конец тела сообщения.
В спецификации протокола HTTP указано, что тело сообщения (даже отсутствующее) отделено от блока заголовков пустой строкой. При обработке GET-запросов мы этим пользовались. Ждали появления пустой строки и считали это признаком окончания запроса.
В POST-запросах в общем случае определить конец тела сообщения можно только одним способом – с помощью заголовка Content-Length. Этот параметр указывает длину тела сообщения в байтах, т.е. в символах.
Спецификация протокола HTTP требует, что в запросе клиента при наличии тела сообщения должен быть заголовок Content-Length. Если он отсутствует, и сервер не может определить длину сообщения, то он должен ответить кодом 400 (неверный запрос).
Программы WEB-серверов из предыдущих уроков при приеме запроса подвешивали программу.
void loop() {
client = server.available(); // ожидаем объект клиент
if (client) {
while (client.connected()) {
if (client.available()) {
// здесь программа обрабатывает очередной символ
}
// здесь программа ожидает символ
// т.е. висит пока не будет обработан весь запрос
}
}
// при обработке запроса сюда программа не доходит
}
Давайте покажем класс и напишем программу, которая будет обрабатывать запрос параллельным процессом. Т.е. при приеме данных она не будет подвешивать контроллер. Цикл loop все время будет доходить до конца.
При получении запроса программа будет принимать данные, и формировать флаг requestRecieved (признак запрос принят). А в основном цикле мы будем анализировать этот признак и при его активном состоянии выводить в последовательный порт полученные данные.
void loop() {
// программный блок обработки запроса
. . . . . . . . . . . . . . . . . .
// проверка признака запроса
if( requestRecieved == true ) {
requestRecieved= false;
Serial.println();
Serial.println(F("POST-request recieved"));
Serial.println(dataPost); // вывод данных
}
}
Перед тем, как читать урок дальше лучше загрузите полный скетч первого варианта сервера.
Программа реализована по принципу пошагового автомата. Таким образом мы разрабатывали охранную сигнализацию в уроке 17.
void loop() {
//------------------- ожидание клиента
if( mode == 0 ) {
}
//------------------- определение типа запроса GET или POST
else if( mode == 1) {
}
//------------------- поиск Content-Length:
else if( mode == 2) {
}
//------------------- чтение параметра заголовка
else if( mode == 3) {
}
//------------------- прием POST-данных
else if( mode == 4) {
}
//------------------- обработка ошибочного запроса
else if( mode == 5) {
}
//------------------- разрыв соединения
else if( mode == 30) {
}
else mode=0;
// проверка признака запроса
}
Основной скелет программы представляет собой блоки, через которые проходит программа в зависимости от переменной mode. Например, если mode=1, то цикл loop будет проходить через блок “определение типа запроса GET или POST”. Для того, чтобы переключиться на другой блок достаточно занести соответствующее значение в переменную mode. Переменная mode, как бы запоминает, какую операцию в данный момент выполняет программа.
- В первом блоке (шаг 0) мы находимся в режиме ожидания. Как только подключается клиент - переходим на следующий шаг.
- Шаг 1 – это определение типа запроса. Мы сохраняем первый символ в переменной firstLetter. По нему мы сможем определить GET или POST-запрос пришел на сервер.
- Следующий блок (шаг 2) ищет нужный нам заголовок Content-Length и определяет окончание блока заголовков. В нем:
- Формируется признак пустой строки emptyLine. Если пришел любой символ, кроме перевода строки и возврата каретки, то строка уже считается не пустой.
- Сравнивает символы запроса со строкой ”Content-Length”.
- Если пришел символ ”:” и была строка ”Content-Length”, то надо считывать параметр заголовка, т.е. длину тела сообщения. Для этого автомат переходит на шаг 3.
- Если встретилась пустая строка, то это означает, что заголовки закончились. Тогда в зависимости от типа запроса программа переходит на шаги:
- обработки POST-запроса;
- GET-запроса;
- обработка ошибки.
- Блок шага 3 считывает размер тела сообщения и преобразовывает его в число.
- На шаге 4 мы принимаем данные POST-запроса в строку dataPost.
Программа пока не закончена. Проверим, как она работает. Я загрузил скетч в плату, открыл монитор порта. Запустил на компьютере код HTML-формы и заполнил форму теми же данными на мифического Иванова.
Все правильно. Программа определила, что был POST-запрос, длина тела сообщения 111 символов. Выделенная программой строка данных полностью соответствует строке из общего блока запроса.
GET-запросы наш сервер тоже определяет.
Выделить из полученной строки переменные уже не сложно.
До этого момента мы запускали на компьютере код HTML-формы, а потом формировали POST-запрос. Давайте сделаем так, чтобы форма появлялась в браузере из нашего сервера.
Для этого необходимо в обработку GET-запроса вставить отсылку клиенту кода формы TestForm.html. Не забыть экранировать кавычки.
client.print(F("<form action=\"http://192.168.1.10\" method=\"post\" name=\"form\"><br>Имя:<input name=\"fname\" type=\"text\" value=\"\"><br><br>Фамилия:<input name=\"sname\" type=\"text\" value=\"\"><br><br>Возраст:<input name=\"age\" type=\"number\" value=\"\"><br><br>Пол:М<input name=\"sex\""));
client.print(F(" type=\"radio\" value=\"male\">Ж<input name=\"sex\" type=\"radio\" value=\"female\"><br><br>Любимый вид спорта:<select name=\"sport\"><option value=\"Football\">Футбол<option value=\"Volleyball\">Волейбол"));
client.print(F("<option value=\"Basketball\">Баскетбол <option value=\"Swimming\">Плавание<option value=\"Nothing\">Нет</select><br><br>О себе:<br><textarea cols=\"40\" rows=\"8\" name=\"about\"></textarea><br><br><input type=\"submit\" name=\"send\" value=\"Отправить\"></form>"));
Полный скетч сервера:
Загружаем программу. В браузере набираем адрес сервера http://192.168.1.10.
В окне браузера появляется форма регистрации, полученная уже с нашего сервера.
Заполняем, отсылаем данные на сервер. У меня все работает.
Я установил такой ответ клиенту на POST-запрос:
// ответ клиенту
client.println(F("HTTP/1.1 200 OK")); // стартовая строка
client.println(); // пустая строка отделяет тело сообщения
mode=1;
Сервер не закрывает сессию на POST-запрос и продолжает принимать новые пакеты. Т.е. обратившись к серверу, мы получаем в браузере форму и можем, не прерывая сессию многократно отсылать данные. Получается управление сервером в реальном времени. При этом ничто не мешает нам опять обратиться к серверу с GET-запросом и получить от него форму заново.
Если нужен алгоритм, при котором после отсылки данных на сервер надо закрыть соединение, то ответ клиенту на POST-запросы должен выглядеть так:
// ответ клиенту
client.println(F("HTTP/1.1 200 OK")); // стартовая строка
client.println(F("Connection: close")); // закрыть сессию после ответа
client.println(); // пустая строка отделяет тело сообщения
mode= 30;
Последний штрих – ответ клиенту на ошибочный запрос кодом 400 (неверный запрос).
Еще, надо бы добавить при POST-запросе проверку переменной contentLength (длины сообщения). При ее нулевом значении также надо отвечать клиенту кодом 400, но у меня уже не хватило сил. Урок объемный получается.
Мы создали работоспособный WEB-сервер. Еще раз подчеркну, что сервер работает параллельным процессом. В конце цикла loop можно выполнять другие действия, конечно, которые надолго не подвешивают программу.
В следующем уроке будем разрабатывать WEB-клиент.
Эдуард, очень познавательно, огромное спасибо.
Вложил свою маленькую лепту, подписка на 1 месяц.
Спасибо.
Спасибо за познавательные уроки!
Ждем следующие!
Эдуард, спасибо за уроки. У меня огромная просьба помочь. Мы загружали скетчи с вашего сайта, но ни один скетч не хочет работать. Как только мы загружаем скетч плата перестает работать. Перезагружаем IDE загружаем другой код — все работает. Переходим на Ваш и все — ошибка загрузки. Перепробовали все варианты, но ничего не получается.
Здравствуйте!
Что у вас ни один скетч не работает? Даже самые простые?
Что значит ошибка загрузки? Что пишет компилятор?
Здравствуйте. Очень интересно и познавательно написаны уроки.
Но смотрю последний урок написан более полугода назад.
Не очень просто реализавать WEB-клиент или много другой работы?
Спасибо.
Здравствуйте!
Все вместе взятое: много работы (очень сложное ПО для нового фасовочного оборудования), лето… Спасибо, что беспокоитесь.
а в обратном направлении можно ? чтобы ардуино get запросами передавал показатели датчиков
Здравствуйте!
Можно, если Ардуино будет клиентом.
А где же урок про WEB-клиент на ардуино…?
Здравствуйте, Эдуард! Спасибо за урок! Я так понимаю, можно будет строку с данными (возраст) преобразовать в число?
Здравствуйте!
При обработке да, а при передаче лучше придерживаться единого формата представления данных — текстового.
Здравствуйте!
Подскажите пожалуйста, как теперь вытащить значения и присвоить переменным
Здравствуйте!
В блоке «выделение переменных из строки» ищете имя переменной, затем знак «=», затем считываете значение переменной до символа «&».
// выделение переменных из строки
Serial.println();
for(int i=0; i<200; i++ ) {
if( dataPost[i] == '=' ) Serial.print(" ");
else if( dataPost[i] == '&' ) Serial.println();
else if( dataPost[i] == '+' ) Serial.print(" ");
else if( dataPost[i] == 0 ) break;
else Serial.write(dataPost[i]);
}
С формой и как формируется POST запрос понял. Начал пробовать работу с платой и тут возник вопрос.
У меня плата WEMOS D1 mini на ESP8266
В сеть выхожу по WI-FI
В скетче видимо нужны другие библиотеки для HTTP запросов.
И подскажите, меняются ли команды в цикле loop, если будет другая библиотека.
Здравствуйте!
HTTP протокол использует TCP/IP соединение. Можно формировать HTTP запросы через TCP протокол. В уроках так и делается.