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

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

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

 

 

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

 

STM Name

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

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

*