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 2.0

Ethernet LAN Сетевой Модуль можно купить здесь (модуль SD SPI в подарок) ENC28J60 Ethernet LAN Сетевой Модуль.

 

 

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

 

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

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

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

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

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

*