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



 

Урок 86

 

Часть 1

 

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

 

Сегодня мы продолжим нашу тему передачи данных с использованием протокола транспортного уровня TCP и попробуем передать по данному протоколу с нашего контроллера WEB-страницу. А чтобы передать или запросить такую страницу, но нам уже для этого потребуется протокол HTTP (HyperText Transfer Protocol — протокол передачи гипертекста), который уже является протоколом прикладного уровня, хотя в некоторых случаях он может выполнять роль протокола транспортного уровня.

Протокол HTTP является уже неструктурированным протоколом, то есть в его заголовке не используется строгий порядок полей, а также их размер. Вообщем, данный протокол является текстовым, но от этого он не становится проще, а можно сказать, наоборот — становится сложнее.

Порядок передачи по данному протоколу, как правило, следующий.

Клиент пытается запросить соединение у сервера, соединение устанавливается.

Затем клиент запрашивает файл у сервера (веб-страницу, картинку, архив или любой другой файл), сервер сначала подтверждает запрос, а затем передаёт клиенту запрашиваемый файл. Передача может происходить в несколько пакетов или сегментов.

Затем сервер дожидается подтверждения последнего отправленного пакета от клиента и инициирует завершение соединения.

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

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

Поэтому я не буду вас мучить теорией и перейдём к проекту.

Создадим проект ENC28J60_HTTPS, переработав его из проекта предыдущего занятия с именем ENC28J60_TCPS_DATA.

Запустим проект в генераторе Cube MX и, ничего там не трогая, сгенерируем проект для среды программирования Keil, откроем его, произведём настройки программатора на автоперезагрузку, а также подключим наши библиотеки в дерево проекта.

Давайте немного изменим функцию отправки пакетов Ethernet eth_send. Для этого откроем файл net.c и добавим в данную функцию ещё один входящий аргумент — тип протокола Ethernet

 

void eth_send(enc28j60_frame_ptr *frame, uint16_t type, uint16_t len)

 

Также в теле данной функции мы занесём тип протокола в соответствующее поле заголовка

 

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

frame->type=type;

 

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

 

uint16_t checksum(uint8_t *ptr, int16_t len, uint8_t type)

 

Также поправим в теле данной функции ошибку, которая приводит к неправильному расчёту контрольной суммы в случае нечётного количества байтов. Здесь будет не 0, а 1

 

while(len>1)

{

  sum += (uint16_t) (((uint32_t)*ptr<<8)|*(ptr+1));

 

Внесём те же изменения и в прототипы данных функций в заголовочном файле net.h.

 

Затем подправим код в местах вызова функции eth_send

1. В том же файле net.h в функции ip_send

eth_send(frame,ETH_IP,len);

 

2. В файле arp.c в функции arp_send

eth_send(frame,ETH_ARP,sizeof(arp_msg_ptr));

 

Скомпилируем наш проект, прошьём контроллер и проверим прежнюю работоспособность нашего стека (пропингуем наш модуль, а также пошлём строки в Putty на 80 порт.

Если всё работает, то теперь попробуем послать запрос на сервер с ПК.

Можно даже сделать это с помощью Putty, но мы пойдём более приземлённым путём и сделаем, это введя в адресную строку браузера IP-адрес нашего модуля. В качестве браузера мы будем использовать старый добрый Internet Explorer, так как он не пытается открывать несколько соединений, не пытается запросить иконку страницы, то есть в данном случае ведёт себя вполне адекватно.

Но, прежде чем мы эту страницу запросим, мы запустим анализатор сетевого трафика WireShark, отфильтровав там этот трафик, как обычно, по MAC-адресу нашего модуля, а также запустим и терминальную программу, соединившись там с нашим портом USART.

Вот теперь введём в адресную строку браузера IP-адрес модуля и нажмём клавишу «Enter» или стрелку справа от адресной строки. Никакую страницу наш браузер по понятным причинам не отобразит, поэтому мы не будем дожидаться страницы и нажмём на крестик, который также находится справа от адресной строки, тем самым заставив клиент разорвать соединение

 

image00

 

Перейдём в Wireshark и увидим, что соединение с сервером было установлено по инициативе клиента, затем клиент послал HTTP-запрос, который сервер подтвердил, так как у нас данная процедура уже так или иначе реализована. Затем клиент разорвал соединение (нажмите на картинку для увеличения изображения)

 

image01_0500

 

Поэтому дальнейшая наша задача — определить корректность подтверждения от сервера, а также вслед за этим подтверждением послать главную веб-страницу — index.html. Несмотря на то, что это звучит не очень заумно, выполнить данное задание будет не очень легко. Поэтому не будем спешить и проделаем кое-какие подготовительные шаги.

Для начала давайте немного для общей эррудиции исследуем тот пакет, который нам послал клиент в качестве запроса html-страницы. Так как пакет определился как пакет HTTP, то откроем в нижнем окне раздел HTTP — Hypertext Transfer Protocol

 

image02

 

Мы видим здесь следующие строки сообщения

1) GET / HTTP/1.1

С помощью данно сообщения клиент запрашивает (метод GET) главную страницу (косая черта с пробелом) и сообщает версию протокола — 1.1.

То есть сначала метод GET, затем путь к главному файлу, если нет имени, то запрашивается главная страница — index.html или index.php, а затем тип и версия протокола.

2) Accept: text/html, application/xhtml+xml, */*

Клиент с помощью метода Accept сообщает серверу о поддерживаемых типах данных (документов)

3) Accept-Language: ru-RU

С помощью метода Accept-Language клиент сообщает язык пользователя.

4) User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko

С помощью данного метода клиент сообщает тим п версию браузера

5) Accept-Encoding: gzip, deflate

Данный метод клиент использует для передачи серверу переченя поддерживаемых способов кодирования содержимого при передаче/

6) Host: 192.168.1.193

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

7) DNT: 1

Это HTTP-метод, который не дает веб-приложению отслеживать ваши действия. Если в качестве аргумента 1, то это значит, что пользователь против того, чтобы за ним следили.

8) Connection: Keep-Alive

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

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

Ну, вроде немного разобрались с запросом.

 

 

Перейдём к коду в файл tcp.c. Мы пока не будем создавать файловую пару отдельно для протокола HTTP, так как последний неразрывно связан с TCP.

Пока немного займёмся оптимизацией кода в данном модуле, так как функция отправки пакета TCP у нас постоянно растёт, а это непорядок и неизбежно приведёт к ошибкам.

Сразу после всех глобальных объявлений над данной функцией мы создадим функцию для подготовки заголовка TCP

 

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

//Подготовка заголовка TCP-пакета

void tcp_header_prepare(tcp_pkt_ptr *tcp_pkt, uint16_t port, uint8_t fl, uint16_t len)

{

}

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

 

В качестве входящих аргументов здесь используются указатель на пакет TCP, значение порта получателя, переменная с флагами и длина пакета.

Прежде чем заняться кодом тела данной функции, заглянем в заголовочный файл tcp.h и добавим структуру для хранения некоторых данных (свойств) TCP

 

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

typedef struct tcp_prop {

  volatile uint16_t port_dst;//порт получателя

  volatile uint32_t seq_num;//порядковый номер байта

  volatile uint32_t ack_num;//номер подтверждения

  volatile uint32_t data_stat;//статус передачи данных

  volatile uint32_t data_size;//размер данных для передачи

  volatile uint16_t last_data_part_size;//размер последней части данных для передачи

  volatile uint16_t cnt_data_part;//количество оставшихся частей данных для передачи

} tcp_prop_ptr;

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

 

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

Теперь вернёмся в файл tcp.c и создадим сначала глобальную переменную типа нашей структуры

 

extern uint8_t ipaddr[4];

tcp_prop_ptr tcpprop;

 

А затем заполним тело нашей новой функции

 

void tcp_header_prepare(tcp_pkt_ptr *tcp_pkt, uint16_t port, uint8_t fl, uint16_t len)

{

  tcp_pkt->port_dst = be16toword(port);

  tcp_pkt->port_src = be16toword(LOCAL_PORT_TCP);

  tcp_pkt->bt_num_seg = tcpprop.seq_num;

  tcp_pkt->num_ask = tcpprop.ack_num;

  tcp_pkt->fl = fl;

  tcp_pkt->size_wnd = be16toword(8192);

  tcp_pkt->urg_ptr = 0;

  tcp_pkt->len_hdr = len << 2;

  tcp_pkt->cs = 0;

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

}

 

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

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

 

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

//Подготовка заголовка IP-пакета

void ip_header_prepare(ip_pkt_ptr *ip_pkt, uint8_t *ip_addr, uint8_t prt, uint16_t len)

{

  ip_pkt->len=be16toword(len);

  ip_pkt->id = 0;

  ip_pkt->ts = 0;

  ip_pkt->verlen = 0x45;

  ip_pkt->fl_frg_of=0;

  ip_pkt->ttl=128;

  ip_pkt->cs = 0;

  ip_pkt->prt=prt;

    memcpy(ip_pkt->ipaddr_dst,ip_addr,4);

    memcpy(ip_pkt->ipaddr_src,ipaddr,4);

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

}

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

 

В качестве входящих аргументов здесь указатель на пакет IP, значение адреса IP, тип протокола и длина данных.

 

 

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

 

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

//Статусы TCP

#define TCP_CONNECTED 1

#define TCP_DISCONNECTED 2

#define TCP_DISCONNECTING 3 //закрываем соединение после подтверждения получателя

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

 

Вернёмся в файл tcp.c и добавим две глобальные переменные

 

tcp_prop_ptr tcpprop;

volatile uint16_t tcp_mss = 458;

volatile uint8_t tcp_stat = TCP_DISCONNECTED;

 

После функции подготовки заголовка IP-пакета добавим функцию, которая будет отправлять ответ на запрос соединения

 

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

/*Отправка ответа на запрос соединения*/

uint8_t tcp_send_synack(enc28j60_frame_ptr *frame, uint8_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);

  return res;

}

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

 

В теле данной функции мы пока добавим необходимые переменные, а также подключимся к пакетам.

Затем заполним поля с номерами байтов в потоке

 

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

tcpprop.seq_num = rand();

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

 

Затем заполним опции, в которых укажем максимальный размер сегмента

 

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

tcp_pkt->data[0]=2;//Maximum Segment Size (2)

tcp_pkt->data[1]=4;//Length

tcp_pkt->data[2]=(uint8_t) (tcp_mss>>8);//MSS = 458

tcp_pkt->data[3]=(uint8_t) tcp_mss;

 

Вычислим длину пакета TCP и вызовем функцию подготовки заголовка TCP

 

tcp_pkt->data[3]=(uint8_t) tcp_mss;

len = sizeof(tcp_pkt_ptr)+4;

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

 

То же самое проделаем и для заголовка IP

 

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

len+=sizeof(ip_pkt_ptr);

ip_header_prepare(ip_pkt, ip_addr, IP_TCP, len);

 

Затем подготовим и отправим наш пакет

 

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);

 

И по окончанию функции мы запишем статус установленного соединения в переменную

 

eth_send(frame,ETH_IP,len);

tcp_stat = TCP_CONNECTED;

return res;

 

Теперь в функции tcp_read мы сначала сохраним порт получателя в соответствующее поле структуры со свойствами после подключения к пакетам

 

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

tcpprop.port_dst = be16toword(tcp_pkt->port_src);

 

Там, где мы вызывали для установки соединения функцию tcp_send с определённой опцией, вызовем нашу новую функцию

 

if (tcp_pkt->fl == TCP_SYN)

{

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

}

 

Соберём код, прошьём контроллер и попытаемся установить и разорвать соединение, тем самым убедившись, что сервер наш на это адекватно отвечает, как и раньше. Можно с помощью утилиты Putty, не передавая никаких строк

 

image03

 

Отлично! Продолжаем дальше.

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

Для этого над функцией tcp_send добавим функцию, которая будет отправлять ответ на запрос разъединения и затем отправит такой же запрос при определённом условии. Код отправки пакета подтверждения заполним сразу, а остальной код чуть позже

 

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

/*Отправка ответа на запрос разъединения и затем отправка такого же запроса при условии*/

uint8_t tcp_send_finack(enc28j60_frame_ptr *frame, uint8_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 = be32todword(be32todword(tcp_pkt->bt_num_seg) + 1);

  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);

  return res;

}

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

 

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

 

eth_send(frame,ETH_IP,len);

if(tcp_stat == TCP_DISCONNECTED) return 0;

 

Затем, перезаполнив только некоторые поля наших заголовков, отправим запрос на разъединение и после отправки устанвим статус разъединения

 

if(tcp_stat == TCP_DISCONNECTED) return 0;

tcp_pkt->fl = TCP_FIN|TCP_ACK;

len = sizeof(tcp_pkt_ptr);

tcp_pkt->cs = 0;

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

len+=sizeof(ip_pkt_ptr);

eth_send(frame,ETH_IP,len);

tcp_stat = TCP_DISCONNECTED;

return res;

 

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

Также очень важное замечание — ни в коем случае не использовать код копирования MAC-адреса из источника в приёмник перед отправкой пакета, иначе пакет не дойдёт до получателя, так как мы их уже местами меняли выше в этой же функции.

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

 

else if (tcp_pkt->fl == (TCP_FIN|TCP_ACK))

{

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

}

 

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

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

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

 

tcp_send_data

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

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

uint8_t tcp_send_data(enc28j60_frame_ptr *frame, uint8_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!!!\r\n");

    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.

 

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

 

 

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

 

 

Отладочную плату STM32F103C8T6 можно приобрести здесь STM32F103C8T6

Программатор недорогой можно купить здесь ST-Link V2

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

Переходник USB to TTL можно приобрести здесь ftdi ft232rl

 

 

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

 

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

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

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

*