Урок 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
Также мы всё наблюдаем в терминальной программе
В WireShark мы также видим, что подтверждение наше корректно. А видим мы это потому, что клиент нам ответил на наше подтверждение подтверждением с флагом ACK (нажмите на картинку для увеличения изображения)
То есть соединение у нас установилось.
Давайте также для полноты картины в функции 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» вообще безо всяких параметров, что означает наше желание разорвать текущее соединение
В WireShark мы также увидим некорректное разъединение
Ну что поделать, так или иначе соединение у нас разорвано, и мы можем заново пытаться соединиться.
Теперь мы должны увидеть в терминальной программе вот это
Это означает то, что пакет подтверждения до нас дошёл и мы его получили.
Ну а теперь нам нужно научиться также профессионально и разъединяться. Разъединяться мы будем также по инициативе клиента. Поэтому в функции приёма пакета обработаем соответствующие флаги
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.
Если всё нормально, то в терминальной программе мы увидим вот это
А в WireShark мы увидим вот это (нажмите на картинку для увеличения изображения)
Судя по этой информации, мы с вами добились немалого результата, установив и разорвав соединение TCP корректно. Я думаю урок по передаче полезной нагрузки по протоколу TCP нам будет изучить намного легче, так как заголовок мы изучили, а также часть теории уже закрепили на практике. Впоследствии мы также, я думаю, напишем и клиент, который уже сам будет запрашивать у сервера разрешение на соединение, разъединение и передачу данных.
Предыдущая часть Программирование МК STM32 Следующий урок
Отладочную плату STM32F103C8T6 можно приобрести здесь STM32F103C8T6
Программатор недорогой можно купить здесь ST-Link V2
Ethernet LAN Сетевой Модуль можно купить здесь ENC28J60 Ethernet LAN Сетевой Модуль.
Переходник USB to TTL можно приобрести здесь ftdi ft232rl
Смотреть ВИДЕОУРОК в RuTube (нажмите на картинку)
Смотреть ВИДЕОУРОК в YouTube (нажмите на картинку)
Повторил этот урок, но к моему большому сожалению, процедура «тройного рукопожатия» выполняется не до конца. Нет последнего третьего шага. А именно:
Компьютер не посылает модулю ENC28J60 пакет с установленным флагом ACK.
Кто знает, как это исправить?