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

 

 

 

 

Урок 48

 

Часть 2

 

LAN. ENC28J60. TCP Server.

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

 

 

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

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

 

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

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

{

  uint8_t res=0;

  uint16_t len=0;

  return res;

}

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

 

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

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

 

#include "usart.h"

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

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 "usart.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

uint8_t tcp_read(enc28j60_frame_ptr *frame, uint16_t len);

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

 

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

 

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

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;

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:%drn", len);

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

len+=sizeof(ip_pkt_ptr);

sprintf(str1,"len:%drn", len);

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

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;

ip_pkt->ipaddr_dst = ip_addr;

ip_pkt->ipaddr_src = IP_ADDR;

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:%drn", len);

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

  USART_TX((uint8_t*)"SYN ACKrn",9);

}

 

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

 

index08

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

 

 index09

 

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

 

index10

 

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

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

 

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

}

else if (tcp_pkt->fl == TCP_ACK)

{

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

}

 

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

 

index11

 

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

 

index12

 

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

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

 

index13

 

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

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

 

  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 и обработаем следующее условие с нашим отправленным кодом операции

 

  USART_TX((uint8_t*)"SYN ACKrn",9);

}

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

USART_TX((uint8_t*)"ACK OF FINrn",12);

 

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

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

 

  USART_TX((uint8_t*)"ACK OF FINrn",12);

  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.

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

 

index14

 

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

 

index15_0500

 

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

 

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

 

Исходный код

 

 

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

Программатор (продавец надёжный) USBASP USBISP 2.0

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

 

 

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

 

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

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

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

*