STM Урок 84. LAN. ENC28J60. TCP Server. Устанавливаем и разрываем соединение. Часть 2



 

Урок 84

 

Часть 2

 

TCP Server. Устанавливаем и разрываем соединение

 

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

 

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

 

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

uint8_t tcp_send(uint8_t *ip_addr, uint16_t port, uint8_t op)

{

  uint8_t res=0;

  uint16_t len=0;

  return res;

}

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

 

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

В файле tcp.h создадим структуру для заголовка TCP

 

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

typedef struct tcp_pkt {

  uint16_t port_src;//порт отправителя

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

  uint32_t bt_num_seg;//порядковый номер байта в потоке данных (указатель на первый байт в сегменте данных)

  uint32_t num_ask;//номер подтверждения (первый байт в сегменте + количество байтов в сегменте + 1 или номер следующего ожидаемого байта)

  uint8_t len_hdr;//длина заголовка

  uint8_t fl;//флаги TCP

  uint16_t size_wnd;//размер окна

  uint16_t cs;//контрольная сумма заголовка

  uint16_t urg_ptr;//указатель на срочные данные

  uint8_t data[];//данные

} tcp_pkt_ptr;

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

 

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

Добавим макрос для номера нашего локального порта для обращения к нам извне по протоколу TCP

 

#include "net.h"

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

#define LOCAL_PORT_TCP 80

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

 

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

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

 

} tcp_pkt_ptr;

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

//флаги TCP

#define TCP_CWR 0x80

#define TCP_ECE 0x40

#define TCP_URG 0x20

#define TCP_ACK 0x10

#define TCP_PSH 0x08

#define TCP_RST 0x04

#define TCP_SYN 0x02

#define TCP_FIN 0x01

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

//операции TCP

#define TCP_OP_SYNACK 1

#define TCP_OP_ACK_OF_FIN 2

#define TCP_OP_ACK_OF_RST 3

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

 

Далее в функции tcp_read в файле tcp.c мы вызовем функцию отправки пакета с кодом операции ответа на попытку подключения при условии, если нам придёт пакет с флагом SYN, причём только с этим флагом, никаких других не будет. Предварительно мы, конечно, подключимся к нашим заголовкам

 

HAL_UART_Transmit(&huart1,(uint8_t*)str1,strlen(str1),0x1000);

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

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

if (tcp_pkt->fl == TCP_SYN)

{

  tcp_send(ip_pkt->ipaddr_src, be16toword(tcp_pkt->port_src), TCP_OP_SYNACK);

}

 

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

 

uint16_t len=0;

static uint32_t num_seg=0;

enc28j60_frame_ptr *frame=(void*) net_buf;

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

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

 

 

Далее мы добавим условие в котором отфильтруемся именно по операции ответа на попытку подключения

 

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

if (op==TCP_OP_SYNACK)

{

}

 

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

 

if (op==TCP_OP_SYNACK)

{

  //Заполним заголовок пакета TCP

  tcp_pkt->port_dst = be16toword(port);

  tcp_pkt->port_src = be16toword(LOCAL_PORT_TCP);

  tcp_pkt->num_ask = be32todword(be32todword(tcp_pkt->bt_num_seg) + 1);

  tcp_pkt->bt_num_seg = rand();

  tcp_pkt->fl = TCP_SYN | TCP_ACK;

  tcp_pkt->size_wnd = be16toword(8192);

  tcp_pkt->urg_ptr = 0;

  len = sizeof(tcp_pkt_ptr)+4;

  tcp_pkt->len_hdr = len << 2;

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

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

  tcp_pkt->data[2]=0x05;

  tcp_pkt->data[3]=0x82;

  tcp_pkt->cs = 0;

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

}

 

Теперь обо всём по порядку.

Сначала мы в соответствующие поля заголовка заносим номера портов отправителя и получателя, затем в номер подтверждения мы заносим номер, пришедший нам в поле номера сегмента, увеличенный на 1, не забывая о перевороте байтов. На один мы его увеличиваем потому, что данных в такого роде полученном пакете не содержится, поэтому мы передаём номер следующего ожидаемого байта. В номер сегмента мы можем спокойно занести любую величину, так как это первый наш отправляемый пакет в данном потоке. Затем мы устанавливаем соответствующие флаги, необходимые для подтверждение запроса на соединение, затем заносим размер окна, можно заносить любой до 65535, но я решил занести такой же, как и в запросе сервера. Затем в поле срочных данных заносим ноль, так как его мы не используем. Затем вычисляем длину, равную длине заголовка, увеличенной на размер опций, затем заноси эту длину в соответствующие биты, сдвинув на 2 пункта. Потом заносим опции. В качестве опций мы даём максимальный размер сегмента, занося соответствующий код опции в первый байт, количество байт в опциях во 2-й байт, а в 3 и 4 байтах у нас будет сама длина, соответственно перевёрнутая, то есть 0x0582.

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

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

Далее мы заполняем заголовок IP

 

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

//Заполним заголовок пакета IP

sprintf(str1,"len:%d\r\n", len);

HAL_UART_Transmit(&huart1,(uint8_t*)str1,strlen(str1),0x1000);

len+=sizeof(ip_pkt_ptr);

sprintf(str1,"len:%d\r\n", len);

HAL_UART_Transmit(&huart1,(uint8_t*)str1,strlen(str1),0x1000);

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=IP_TCP;

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.

 

 

Затем заполняем заголовок Ethernet и отправляем наш пакет клиенту

 

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

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

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

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

frame->type=ETH_IP;

len+=sizeof(enc28j60_frame_ptr);

enc28j60_packetSend((void*)frame,len);

 

Далее давайте отобразим в терминальной программе длину нашего пакета и тип нашего отправленного пакета TCP

 

  enc28j60_packetSend((void*)frame,len);

  sprintf(str1,"len:%d\r\n", len);

  HAL_UART_Transmit(&huart1,(uint8_t*)str1,strlen(str1),0x1000);

  HAL_UART_Transmit(&huart1,(uint8_t*)"SYN ACK\r\n",9,0x1000);

}

 

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

 

image05

 

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

 

image06

 

В WireShark мы также видим, что подтверждение наше корректно. А видим мы это потому, что клиент нам ответил на наше подтверждение подтверждением с флагом ACK (нажмите на картинку для увеличения изображения)

 

image07_0500

 

То есть соединение у нас установилось.

Давайте также для полноты картины в функции tcp_read отобразим в терминальной программе то, что нам от клиента пришло подтверждение

 

  tcp_send(ip_pkt->ipaddr_src, be16toword(tcp_pkt->port_src), TCP_OP_SYNACK);

}

else if (tcp_pkt->fl == TCP_ACK)

{

  HAL_UART_Transmit(&huart1,(uint8_t*)"ACK\r\n",5,0x1000);

}

 

Соберём ещё раз код, прошьём контроллер и попробуем ещё раз соединиться. Правда перед этим прийдётся разъединиться, что корректно сделать нам пока не получится, так как мы не сможем ответить ибо нет у нас пока тагого кода и запросить разъединение код мы также не писали ещё. Ну что поделать, разъединимся некорректно. Чтобы разорвать соединение, в telnet нужно сначала вернуться в режим командной строки. Для этого вводим сочетание клавиш Ctrl+»]». Перед нами появится приглашение в командную строку, где мы вводим уже команду «c» вообще безо всяких параметров, что означает наше желание разорвать текущее соединение

 

image08

 

В WireShark мы также увидим некорректное разъединение

 

image10

 

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

Теперь мы должны увидеть в терминальной программе вот это

 

image11

 

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

Ну а теперь нам нужно научиться также профессионально и разъединяться. Разъединяться мы будем также по инициативе клиента. Поэтому в функции приёма пакета обработаем соответствующие флаги

 

  tcp_send(ip_pkt->ipaddr_src, be16toword(tcp_pkt->port_src), TCP_OP_SYNACK);

}

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

{

  tcp_send(ip_pkt->ipaddr_src, be16toword(tcp_pkt->port_src), TCP_OP_ACK_OF_FIN);

}

 

Затем вернёмся в нашу функции отправки TCP-пакета tcp_send и обработаем следующее условие с нашим отправленным кодом операции

 

  HAL_UART_Transmit(&huart1,(uint8_t*)"SYN ACK\r\n",9,0x1000);

}

else if (op==TCP_OP_ACK_OF_FIN)

{

}

 

А в тело данного условия мы скопируем полностью весь код из условия, расположенного выше, а затем потихоньку поправим в нём только определённые строки.

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

 

tcp_pkt->port_src = be16toword(LOCAL_PORT_TCP);

num_seg = tcp_pkt->num_ask;

 

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

 

tcp_pkt->bt_num_seg = num_seg;

 

Далее мы заносим в поле флагов только один флаг вместо двух — флаг подтверждения

 

tcp_pkt->fl = TCP_ACK;

 

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

 

len = sizeof(tcp_pkt_ptr);

 

Ну и, соответственно убираем код добавления опций

 

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

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

tcp_pkt->data[2]=0x05;

tcp_pkt->data[3]=0x82;

 

После отправки пакета мы удалим весь код до конца тела условия и покажем в терминальной программе следующую строку

 

enc28j60_packetSend((void*)frame,len);

HAL_UART_Transmit(&huart1,(uint8_t*)"ACK OF FIN\r\n",12,0x1000);

 

После отправки пакета мы должны будем отправить ещё пакет запроса на разъединение, так как эта процедура двусторонняя.

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

 

    HAL_UART_Transmit(&huart1,(uint8_t*)"ACK OF FIN\r\n",12,0x1000);

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

    len+=sizeof(enc28j60_frame_ptr);

    enc28j60_packetSend((void*)frame,len);

  }

  return res;

}

 

Соберём код, прошьём контроллер, и если у нас ещё соединение не пропало, то попробуем разъединиться таким же образом с помощью команды из telnet.

Если всё нормально, то в терминальной программе мы увидим вот это

 

image13

 

А в WireShark мы увидим вот это (нажмите на картинку для увеличения изображения)

 

image14_0500

 

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

 

 

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

 

Исходный код

 

 

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

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

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

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

 

 

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

STM Name

 

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

STM Name

Один комментарий на “STM Урок 84. LAN. ENC28J60. TCP Server. Устанавливаем и разрываем соединение. Часть 2
  1. Сергей:

    Повторил этот урок, но к моему большому сожалению, процедура «тройного рукопожатия» выполняется не до конца. Нет последнего третьего шага. А именно:

    Компьютер не посылает модулю ENC28J60 пакет с установленным флагом ACK.

    Кто знает, как это исправить?

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

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

*