AVR Урок 50. LAN. ENC28J60. TCP WEB Server. Передаём малую страницу. Часть 2



 

Урок 50

 

Часть 2

 

LAN. ENC28J60. HTTP Server. Передаём малую WEB-страницу

 

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

 

Теперь займёмся передачей данных с исиользованием транспортного протокола TCP.

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

 

tcp_send_data

//--------------------------------------------------

/*Подтверждение на пакет данных и ответ при условии*/

uint8_t tcp_send_data(enc28j60_frame_ptr *frame, uint32_t ip_addr, uint16_t port)

{

  uint8_t res=0;

  uint16_t len=0;

  uint16_t sz_data=0;

  tcp_stat = TCP_CONNECTED;

  ip_pkt_ptr *ip_pkt = (void*)(frame->data);

  tcp_pkt_ptr *tcp_pkt = (void*)(ip_pkt->data);

  sz_data = be16toword(ip_pkt->len)-20-(tcp_pkt->len_hdr>>2);

  tcpprop.seq_num = tcp_pkt->num_ask;

  tcpprop.ack_num = be32todword(be32todword(tcp_pkt->bt_num_seg) + sz_data);

  len = sizeof(tcp_pkt_ptr);

  //отправим подтверждение на пакет данных

  tcp_header_prepare(tcp_pkt, port, TCP_ACK, len);

  len+=sizeof(ip_pkt_ptr);

  ip_header_prepare(ip_pkt, ip_addr, IP_TCP, len);

  //Заполним заголовок Ethernet

  memcpy(frame->addr_dest,frame->addr_src,6);

  eth_send(frame,ETH_IP,len);

  //Если пришло "Hello!!!", то отправим ответ

  if (!strcmp((char*)tcp_pkt->data,"Hello!!!"))

  {

    strcpy((char*)tcp_pkt->data,"Hello to TCP Client!!!rn");

    tcp_pkt->fl = TCP_ACK|TCP_PSH;

    len = sizeof(tcp_pkt_ptr);

    len+=strlen((char*)tcp_pkt->data);

    tcp_pkt->cs = 0;

    tcp_pkt->cs=checksum((uint8_t*)tcp_pkt-8, len+8, 2);

    len+=sizeof(ip_pkt_ptr);

    ip_pkt->len=be16toword(len);

    ip_pkt->cs = 0;

    ip_pkt->cs = checksum((void*)ip_pkt,sizeof(ip_pkt_ptr),0);

    //Заполним заголовок Ethernet

    eth_send(frame,ETH_IP,len);

  }

return res;

}

//--------------------------------------------------

 

Вызовем данную функцию в нужном месте в функции tcp_read

 

if (tcp_pkt->fl&TCP_ACK)

{

  tcp_send_data(frame, ip_pkt->ipaddr_src, tcpprop.port_dst);

}

 

Соберём код, прошьём контроллер и проверим результат

 

image04

 

Если у вас всё точно так же и в утилите WireShark также всё зелёное, то можно функцию tcp_send удалить вместе со всем содержимым. Также удалить можно все операции TCP из заголовочного файла tcp.h.

Теперь наконец-то перейдём к работе с протоколом HTTP и, вернувшись в файл tcp.с подумаем, как нам добавить массив для заголовка ответного пакета HTTP, при этом не «съев» последние остатки оперативной памяти, которой и без него почти не осталось.

А воспользуемся мы для этого местом во флеш-памяти, в которой у нас пока есть некоторый запас.

Для этого подключим определённую библиотеку

 

#include "tcp.h"

#include "avr/pgmspace.h"

 

Теперь добавим глобальный массив с заголовком ответа HTTP

 

volatile uint8_t tcp_stat = TCP_DISCONNECTED;

//--------------------------------------------------

const PROGMEM char http_header[] = {"HTTP/1.1 200 OKrnServer: nginxrnContent-Type: text/htmlrnConnection: keep-alivernrn"};

 

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

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

У нас всего три строки, я думаю для того, чтобы клиентский браузер понял, что в заголовке, этого достаточно.

1)  HTTP/1.1 200 OK

Здесь сервер передаёт версию поддерживаемого ротокола, код сообщения и само сообщение. 200 означает успешный запрос, то есть документ, который клиент просит, на сервере присутствует и будет передан клиенту, OK значит «хорошо».

Коды всех сообщений можно найти в официальных документах описания протокола HTTP. Скажу только то, что коды начинающиеся с цифры 2 — это группа кодов, которые информируют о случаях успешного принятия и обработки запроса клиента. Если код начинается с 1, то это значит, что сервер информирует о процессе передачи, если с 3 — то это перенаправление и клиент должен будет сделать другой запрос, а если 4, то сервер таким образом говорит клиенту об ошибке. Мы часто при ошибочном наборе адреса видим сообщение 404 — страница не найдена. Это один из примеров ошибки. Как правило, на сервере хранится страница, в которой находится удобочитаемая информация об отсутствии страницы, и тогда, когда клиент запрашивает несуществующий документ на сервере, то сервер после передачи такого кода передаёт также эту страницу.

2) Server: nginx

Тип сервера. В нашем случае это не так важно и я передал самый часто используемый тип.

3) Connection: keep-alive

С данным сообщением мы уже знакомы по заголовку запроса клиента и здесь мы передаём своё согласие на то, что если мы не будем закрывать соединение после каждого запроса клиента.

 

 

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

 

if (tcp_pkt->fl&TCP_ACK)

{

  //Если строка "GET / ", то значит это запрос HTTP главной страницы, пока будем работать только с одной

  if (strncmp((char*)tcp_pkt->data,"GET / ", 6) == 0)

  {

  }

  //Иначе обычные данные

  else

  {

    tcp_send_data(frame, ip_pkt->ipaddr_src, tcpprop.port_dst);

  }

}

 

Пока условие оставим с пустым телом, так как далее в этом теле нужно написать код, который определит размер нашего пакета HTTP, который мы будем отправлять с целью, чтобы узнать, влезет ли у нас данный пакет в один пакет TCP или их будет несколько, а если несколько, то сколько именно и сколько именно байт в последнем пакете. А, как известно в пакет HTTP кроме заголовка также входят и данные, в качестве которых в нашем случае будет главная страница, которую мы пока не подготовили. Мы также разместим её содержимое в память FLASH, но только не в текстовом виде, так как в тексте может быть разные символы, в том числе и те, которые не очень понравятся компилятору, а в виде массива целых 8-битных чисел, который мы создадим с помощью утилиты makefsdata.exe, которую несложно найти в интернете и которая поставляется в виде одного исполняемого файла, также данная утилита требует установленных библиотек Microsoft Visual C++ Redistributable. Если пакет не установлен, то утилита запросит недостающую библиотеку. Ну это я думаю для нас не проблема.

Поместим файл makefsdata.exe в отдельную папку, также создадим в данной папке ещё одну папку с именем «fs» и поместим в созданную папку нужную страницу index.html, например с таким содержимым

 

<html><body><h1 style="text-align: center;">AtMega 328P<br><br>WEB Server</h1>
<p>Advanced RISC Architecture</p>
</body></html>

 

Запустим утилиту и у нас должен будет создаться файл с именем fsdata.c.

Прежде чем его открывать заготовим место, куда мы поместим данные созданного массива в нашем проекте сразу же после объявления массива заголовка HTTP

 

const PROGMEM uint8_t index_htm[] = {};

 

Теперь откроем файл fsdata.c, найдём там вот такоей место

 

/* raw file data (127 bytes) */

 

И скопируем в буфер обмена всё, что находится после этого комментария до закрытия фигурной скобки. Запятая последняя, в принципе нам также не нужна.

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

 

const PROGMEM uint8_t index_htm[] = {0x3c,0x68,0x74,0x6d,0x6c,0x3e,0x3c,0x62,0x6f,0x64,0x79,0x3e,0x3c,0x68,0x31,0x20,

0x73,0x74,0x79,0x6c,0x65,0x3d,0x22,0x74,0x65,0x78,0x74,0x2d,0x61,0x6c,0x69,0x67,

0x6e,0x3a,0x20,0x63,0x65,0x6e,0x74,0x65,0x72,0x3b,0x22,0x3e,0x41,0x74,0x4d,0x65,

0x67,0x61,0x20,0x33,0x32,0x38,0x50,0x3c,0x62,0x72,0x3e,0x3c,0x62,0x72,0x3e,0x57,

0x45,0x42,0x20,0x53,0x65,0x72,0x76,0x65,0x72,0x3c,0x2f,0x68,0x31,0x3e,0x0a,0x3c,

0x70,0x3e,0x41,0x64,0x76,0x61,0x6e,0x63,0x65,0x64,0x20,0x52,0x49,0x53,0x43,0x20,

0x41,0x72,0x63,0x68,0x69,0x74,0x65,0x63,0x74,0x75,0x72,0x65,0x3c,0x2f,0x70,0x3e,

0x0a,0x3c,0x2f,0x62,0x6f,0x64,0x79,0x3e,0x3c,0x2f,0x68,0x74,0x6d,0x6c,0x3e};

 

Вот это и есть текст нашей странице, преобразованный в массив.

Вернёмся теперь в функцию tcp_read в то место, где у нас осталось пустое тело условия наличия пакета HTTP в пакете TCP, измерим там отправляемый пакет и определим во сколько он сегментов влезет, а также длину данных в последнем сегменте и отобразим всё это, включая ещё и порт отправителя, в терминальной программе.

 

if (strncmp((char*)tcp_pkt->data,"GET / ", 6) == 0)

{

  tcpprop.data_size = strlen_P(http_header) + sizeof(index_htm);

  tcpprop.cnt_data_part = tcpprop.data_size / tcp_mss + 1;

  tcpprop.last_data_part_size = tcpprop.data_size % tcp_mss;

  sprintf(str1,"data size:%lu; cnt data part:%u; last_data_part_size:%urnport dst:%urn",

    tcpprop.data_size, tcpprop.cnt_data_part, tcpprop.last_data_part_size,tcpprop.port_dst);

  USART_TX((uint8_t*)str1,strlen(str1));

}

 

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

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

Мы видим в терминальной программе следующую информацию

 

image05

 

 

Теперь в файле tcp.h после статусов TCP добавим ещё некоторые статусы передачи данных

 

//--------------------------------------------------

//Статусы передачи данных

#define DATA_COMPLETED 0 //передача данных закончена

#define DATA_ONE 1 //передаём единственный пакет

#define DATA_FIRST 2 //передаём первый пакет

#define DATA_MIDDLE 3 //передаём средний пакет

#define DATA_LAST 4 //передаём последний пакет

#define DATA_END 5 //закрываем соединение после передачи данных

//--------------------------------------------------

 

Вернёмся в tcp.c туда же, откуда и ушли, и вставим там код для установки статуса передачи

 

  USART_TX((uint8_t*)str1,strlen(str1));

  if (tcpprop.cnt_data_part==1)

  {

    tcpprop.data_stat = DATA_ONE;

  }

  else if (tcpprop.cnt_data_part>1)

  {

    tcpprop.data_stat = DATA_FIRST;

  }

}

//Иначе обычные данные

 

Над функцией tcp_read добавим функцию отправки ответа на HTTP-запрос в случае, если он умещается в один пакет, наполнив её тело пока стандартным содержимым

 

//--------------------------------------------------

/*Отправка однопакетного ответа HTTP*/

uint8_t tcp_send_http_one(enc28j60_frame_ptr *frame, uint32_t ip_addr, uint16_t port)

{

  uint8_t res=0;

  uint16_t len=0;

  uint16_t sz_data=0;

  ip_pkt_ptr *ip_pkt = (void*)(frame->data);

  tcp_pkt_ptr *tcp_pkt = (void*)(ip_pkt->data);

  return res;

}

//--------------------------------------------------

 

Опять вернёмся туда, где мы определили размер и устанвили статусы в функции tcp_read, и вызовем там нашу функцию, если у нас данных именно на один пакет

 

    tcpprop.data_stat = DATA_FIRST;

  }

  if(tcpprop.data_stat==DATA_ONE)

  {

    tcp_send_http_one(frame, ip_pkt->ipaddr_src, tcpprop.port_dst);

  }

}

//Иначе обычные данные

 

Продолжим писать тело нашей функции tcp_send_http_one.

На всякий случай ещё раз имерим наши данные

 

tcp_pkt_ptr *tcp_pkt = (void*)(ip_pkt->data);

//Отправим сначала подтверждение на пакет запроса

sz_data = be16toword(ip_pkt->len)-20-(tcp_pkt->len_hdr>>2);

 

Заполним некоторые поля и вызовем функцию подготовки заголовка TCP

 

sz_data = be16toword(ip_pkt->len)-20-(tcp_pkt->len_hdr>>2);

tcpprop.seq_num = tcp_pkt->num_ask;

tcpprop.ack_num = be32todword(be32todword(tcp_pkt->bt_num_seg) + sz_data);

len = sizeof(tcp_pkt_ptr);

tcp_header_prepare(tcp_pkt, port, TCP_ACK, len);

 

Затем, подготовив заголовок IP, отправим наш пакет

 

tcp_header_prepare(tcp_pkt, port, TCP_ACK, len);

len+=sizeof(ip_pkt_ptr);

ip_header_prepare(ip_pkt, ip_addr, IP_TCP, len);

//Заполним заголовок Ethernet

memcpy(frame->addr_dest,frame->addr_src,6);

eth_send(frame,ETH_IP,len);

 

Можно считать, что подтверждение мы отправили. Теперь в этой же функции отправим и ответ клиенту в виде заголовка и самой страницы.

Для этого сформируем сначала поле данных в заголовке TCP. мы не будем создавать отдельную структуру для заголовка и данных HTTP ввиду неструктурированности последнего

 

eth_send(frame,ETH_IP,len);

//Отправляем страницу

strcpy_P((char*)tcp_pkt->data,http_header);

memcpy_P((void*)tcp_pkt->data+strlen_P(http_header),(void*)index_htm,sizeof(index_htm));

 

А вот тут остановимся поподробнее, так как в именах фунций копирования строк и других данных в памяти, к которым мы так привыкли, мы видим суффикс _P. А данный суффикс как раз и означает то, что данные операции мы проводим именно в памяти FLASH или программной. Сначала мы копируем заголовок, а затем по адресу, увеличенному на размер заголовка, саму страницу.

Установим нужные флаги, посчитаем длину пакета TCP, включая данные, но так как данные не входят в поле, предназначенное для них в заголовке TCP, то мы его не заполняем, так как оно уже заполнено. Затем пересчитаем контрольную сумму TCP, так как в её расчёте участвуют и данные

 

memcpy_P((void*)tcp_pkt->data+strlen_P(http_header),(void*)index_htm,sizeof(index_htm));

tcp_pkt->fl = TCP_PSH|TCP_ACK;

len = sizeof(tcp_pkt_ptr);

len+=tcpprop.data_size;

tcp_pkt->cs = 0;

tcp_pkt->cs=checksum((uint8_t*)tcp_pkt-8, len+8, 2);

 

Далее мы рассчитаем длину пакета IP, занесём её в соответствующее поле заголовка, пересчитаем контрольную сумму заголовка IP и отправим наш пакет, опять же не забывая о том, что адреса MAC мы местами не меняем, так как это сделано выше. И по окончанию мы устанавливаем нужный статус, который нам поможет отправить запрос на завершение соединения после приёма пакета с подтверждением от клиента

 

tcp_pkt->cs=checksum((uint8_t*)tcp_pkt-8, len+8, 2);

len+=sizeof(ip_pkt_ptr);

ip_pkt->len=be16toword(len);

ip_pkt->cs = 0;

ip_pkt->cs = checksum((void*)ip_pkt,sizeof(ip_pkt_ptr),0);

//Заполним заголовок Ethernet

eth_send(frame,ETH_IP,len);

tcpprop.data_stat=DATA_END;

 

Можно сейчас, в принципе собрать код и прошить контроллер. Хоть мы страницу в браузере не увидим, так как браузер её отобразит только по завершению соединения, но хотя бы посмотрим наши пакеты в Wireshark. Поэтому попробуем ещё раз запросить в браузере нашу страницу (нажмите на картинку для увеличения изображения)

 

image06_0500

 

Мы видим, что наше подтверждение и наш ответ HTTP клиент получил.

Верёмся в код и над функцией tcp_read добавим ещё одну функцию, которая будет посылать запрос на разъёдинение после получения подтверждения после отправки последнего пакета с данными ответа HTTP, в нашем случае пока единственного. Ближе к окончанию тела функции установим все необходимые статусы

 

//--------------------------------------------------

/*Разъединяемся после получения подтверждения на последний пакет данных*/

uint8_t tcp_send_http_dataend(enc28j60_frame_ptr *frame, uint32_t ip_addr, uint16_t port)

{

  uint8_t res=0;

  uint16_t len=0;

  ip_pkt_ptr *ip_pkt = (void*)(frame->data);

  tcp_pkt_ptr *tcp_pkt = (void*)(ip_pkt->data);

  tcpprop.seq_num = tcp_pkt->num_ask;

  tcpprop.ack_num = tcp_pkt->bt_num_seg;

  len = sizeof(tcp_pkt_ptr);

  tcp_header_prepare(tcp_pkt, port, TCP_FIN|TCP_ACK, len);

  len+=sizeof(ip_pkt_ptr);

  ip_header_prepare(ip_pkt, ip_addr, IP_TCP, len);

  //Заполним заголовок Ethernet

  memcpy(frame->addr_dest,frame->addr_src,6);

  eth_send(frame,ETH_IP,len);

  tcpprop.data_stat=DATA_COMPLETED;

  tcp_stat = TCP_DISCONNECTED;

  return res;

}

//--------------------------------------------------

 

Осталось нам теперь только эту функцию в необходимом месте вызвать. А это необходимое место находится в функции tcp_read в самом низу файла

 

else if (tcp_pkt->fl == TCP_ACK)

{

  if (tcpprop.data_stat==DATA_END)

  {

    tcp_send_http_dataend(frame, ip_pkt->ipaddr_src, tcpprop.port_dst);

  }

  USART_TX((uint8_t*)"ACKrn",5);

}

return res;

 

Соберём код, прошьём контроллер и посмотрим результат нашей нелёгкой работы в браузере

 

image07

 

Также мы видим что в WireShark у нас тоже всё нормально (нажмите на картинку для увеличения изображения)

 

image08_0500

 

Таким образом, мы создали очень простенький веб-сервер, который может отдавать клиенту пока маленькие странички в качестве ответа на HTTP-запрос.

 

 

Предыдущая часть Программирование МК AVR Следующий урок

 

Исходный код

 

 

Приобрести плату Arduino UNO R3 можно здесь.

Приобрести программатор USBASP USBISP с адаптером можно здесь USBASP USBISP 3.3 с адаптером

Ethernet LAN Сетевой Модуль можно купить здесь ENC28J60 Ethernet LAN Сетевой Модуль.

 

 

Смотреть ВИДЕОУРОК в RoTube (нажмите на картинку)

AVR LAN. ENC28J60. TCP WEB Server. Передаём малую страницу

 

Смотреть ВИДЕОУРОК в YouTube (нажмите на картинку)

AVR LAN. ENC28J60. TCP WEB Server. Передаём малую страницу

Один комментарий на “AVR Урок 50. LAN. ENC28J60. TCP WEB Server. Передаём малую страницу. Часть 2
  1. Roman:

    Здравствуйте! Отличные уроки! Всё заработало сразу, но имеются вопросы… время пинга малое, а вот страничка грузится долго!!! пробовал симулятором на разных компах и в железе…Понимаю, что это учебный материал и от этого код огромный, но нельзя ли очистить код от всего лишнего и оставить чистый web server? чтобы отправлялась простейшая HTML строка? наверняка этот урок пригодился бы многим и для понимания и для применения в своих поделках…спасибо,что дочитали:)и за ответ…

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

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

*