Урок 84
Часть 1
TCP Server. Устанавливаем и разрываем соединение
Сегодня мы начинаем знакомство с другим, более сложным, но в то же время и более надёжным протоколом модели OSI транспортного уровня — TCP.
По всей видимости, ввиду своей надёжности, TCP (Transmission Control Protocol, протокол управления передачей) является самым распространённым протоколом для передачи данных.
Надёжность данного протокола заключается в том, что на каждый определённый участок данных требуется подтверждение от принимающей стороны, тем самым обеспечивается гарантированная доставка данных получателю, а также соранение порядка следования сообщений. А сложность состоит в том, что данные нескольких пакетов могут объединяться в сегменты, в окна, окна также бывают скользящими, заголовок внушительно больше и сложнее, чем заголовок UDP, а также перед отправкой каких-либо данных необходимо устанавливать соединение, а после отправки его разрывать. Причём это делается несколькими пакетами в разных направлениях. Также есть ещё кое-какие сложности протокола, но о них мы будем говорить по мере их применения при неоходимости.
Протокол TCP также обеспечивает передачу больших потоков данных, объединяя их в сегменты, тем самым позволяя передавать файлы и прочие потоки.
Также ввиду сложности протокола уроки по нему пройдут не в один и даже не в два этапа.
Цель сегодняшнего занятия — организовать сервер TCP, который в ответ на запрос клиента на соединение будет отвечать ему соответствующим подтверждением и также дожидаться подтверждения от клиента. Также при необходимости на запрос клиента на разрыв соединения наш сервер также должен адекватно среагировать и всё это подтвердить.
Поэтому нам нужно будет для начала изучить, как происходит соединение TCP и как происходит его разрыв.
Но, прежде чем это изучить, сначала необходимо изучить состав заголовка TCP и назначение его полей.
Заголовок TCP имеет следующий вид (нажмите на картинку для увеличения изображения)
Теперь кратко о назначении полей.
Порт отравителя — значение порта источника данных,
Порт получателя — значение порта приёмника данных,
Порядковый номер — номер первого байта в сегменте.
Порядковый номер выполняет две задачи:
Если установлен флаг SYN, то это изначальный порядковый номер — ISN (Initial Sequence Number), и первый байт данных, которые будут переданы в следующем пакете, будет иметь номер, равный ISN + 1.
В противном случае, если SYN не установлен, первый байт данных, передаваемый в данном пакете, имеет этот порядковый номер
Поскольку поток TCP в общем случае может быть длиннее, чем число различных состояний этого поля, то все операции с порядковым номером должны выполняться по модулю 232. Это накладывает практическое ограничение на использование TCP. Если скорость передачи коммуникационной системы такова, чтобы в течение MSL (максимального времени жизни сегмента) произошло переполнение порядкового номера, то в сети может появиться два сегмента с одинаковым номером, относящихся к разным частям потока, и приёмник получит некорректные данные.
Номер подтверждения — это номер, который говорит о том, что все байты получены.
Используется для гарантии доставки пакета. Как правило это номер последнего байта в сегменте, увеличенный на 1, тем самым получатель сегмента даёт в подтверждении отправителю информацию о том, с какого номера ему начинать нумерацию следующего сегмента. Также отправитель, получив номер подтверждения от получателя, будет уверен засчёт этого в том, что все отправленные байты получатель получил.
Длина заголовка — это длина всего заголовка, включая опции (необязательную часть заголовка). Только в отличие от UDP данные в длину заголовка уже не входят.
Флаги.
NS (ECN-nonce concealment protection) — защита от случайного или злонамеренного изменения флагов ECN.
CWR (Congestion Window Reduced) — устанавливается отправителем для подтверждения получения получения сигнала о перегрузке.
ECE (ECN-Echo) — устанавливается получателем при получении сигнала о перегрузке от маршрутизатора.
URG (Urgent pointer field is significant или указатель важности) — сигнализирует о том, что в сегменте содержатся срочные данные, которые необходимо передать сейчас же. Используется совместно с полем «Указатель на срочные данные.
ACK (Acknowledgement) — используется, если в поле «Номер подтверждения» содержатся ненулевые данные, другими словами для подтверждения ранее принятой информации.
PSH (Push) — указывает на то, что полученные данные необходимо сразу передать приложению без записи в буфер
RST (Reset) — устанавливается, если неоходимо принудительно разорвать соединение.
SYN (Synchronize) — синхронизация номеров последовательности. Устанавливается для установки соединения.
FIN () — Устанавливается в случае необходимости корректного завершения соединения.
Размер окна — здесь указывается количество байтов, которые получатель может принять от отправителя. Окно может состоять из нескольких сегментов. Используется обычно для того, чтобы передавать несколько сегментов с одним подтверждением на все эти сегменты.
Контрольная сумма — рассчитанная величина суммы полей заголовка, вклчючающих его обязательную, необязательную части, а также все данные, если таковые будут. В качестве заголовка при рассчёте контрольной суммы, как и в случае с UDP, также выступает не сам заголовок, а псевдозаголовок, к которому, как и в случае UDP прицепляется та же самая часть информации из пакета IP.
Указатель на срочные данные — это поле говорит само за себя. В настоящий момент практически не используется. 16-битовое значение положительного смещения от порядкового номера в данном сегменте. Это поле указывает порядковый номер октета, которым заканчиваются важные (urgent) данные. Поле принимается во внимание только для пакетов с установленным флагом URG.
Опции (или параметры) — необязательная часть заголовка. Но в отличии от других типов протоколов в последнее время используется почти всегда.
Примеры опций или параметров.
Первый байт всегда содержит идентификатор типа опций. Например, 2 — MSS или максимальный размер сегмента.
Далее во втором байте стоит цифра количества байтов в опциях, а в остальных двух байтах будет именно длина сегмента, если мы используем именно MSS. Итого у нас получилось в нашем случае 4 байта, поэтому во втором байте будет четвёрка.
В насшем случае в байте 3 и 4 будет максимальная длина сегмента в формате big endian.
Для полноты информации приведу также вид псевдозаголовка для рассчёта контрольной суммы (нажмите на картинку для увеличения изображения)
Ещё раз напомню, что для того чтобы передавать данные с использованием протокола TCP, мы обязаны установить соединение.
Соединение TCP обеспечивается способом так называемого трёхкратного рукопожатия, то есть состоит из трёх этапов:
1) Клиент посылает серверу пакет с установленным флагом SYN.
2) Сервер отвечает клиенту пакетом с установленными флагами SYN и ACK.
3) Клиент отвечает серверу пакетом с установленным флагом ACK.
Зачем нужно целых три этапа, вроде достаточно и двух, на первый взгляд непонятно. Я не буду рассказывать что именно обеспечивает метод соединения с помощью трёхкратного рукопожатия. Но такое решение позволило избежать ряда проблем при непрохождении пакетов.
Разрыв соединения происходит уже в два этапа в отличии от установки, но эти два этапа должны быть проделаны как со стороны клиента, так и со стороны сервера. Таким образом, получится не два, а четыре этапа
1) Клиент посылает пакет серверу с установленными флагами FIN и ACK.
2) Сервер отвечает клиенту пакетом с установленным флагом ACK.
3) Сервер посылает клиенту пакет с установленными флагами FIN и ACK.
4)Клиент отвечает серверу пакетом с установленным флагом ACK.
Процесс отправки данных по протоколу TCP мы рассмотрим в более поздних уроках, повторюсь, что целью нашего сегодняшнего занятия является установка соединения TCP и его корректный разрыв.
Поэтому начнём, как всегда с проекта. Проект мы создадим из проекта прошлого урока с именем ENC28J60_NTP и назовём ENC28J60_TCPS_CONNECT.
Запустим проект в Cube MX и, ничего не трогая, сгенерируем проект для Keil и откроем его в данной среде программирования.
Затем подключим все файлы наших библиотек и настроим программатор на автоперезагрузку.
Также давайте отключим пока оптимизацию кода, иначе некоторые значения будут теряться по ходу выполнения.
Делается это следующим образом.
Надо сначала зайти в настройки проекта, например, нажав вот на такую кнопку на панели инструментов Keil
В открывшемся диалоге перейдём по вкладке «С/С++» и в открывшейся форме в поле с заголовком слева «Optimization» выберем пункт «Level 0 (-O0)«, а затем внизу нажмём «OK«
Создадим два стандартных файла для нашей новой библиотеки, которая будет обеспечивать работу по протоколу TCP со следующим содержимым
tcp.h:
#ifndef TCP_H_
#define TCP_H_
//--------------------------------------------------
#include "stm32f1xx_hal.h"
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include "enc28j60.h"
#include "net.h"
//--------------------------------------------------
//--------------------------------------------------
#endif /* TCP_H_ */
tcp.c:
#include "tcp.h"
//--------------------------------------------------
extern UART_HandleTypeDef huart1;
//-----------------------------------------------
extern char str1[60];
extern uint8_t net_buf[ENC28J60_MAXFRAME];
extern uint8_t macaddr[6];
extern uint8_t ipaddr[4];
//--------------------------------------------------
Далее мы в файле tcp.c создадим функцию приёма пакетов TCP
//--------------------------------------------------
uint8_t tcp_read(enc28j60_frame_ptr *frame, uint16_t len)
{
uint8_t res=0;
return res;
}
//--------------------------------------------------
Создадим на данную функцию прототип, перейдём в файл net.h и подключим там нашу библиотеку внизу файла
#include "ntp.h"
#include "tcp.h"
Вызовем нашу функцию в функции ip_read в файле net.c
else if(ip_pkt->prt==IP_TCP)
{
tcp_read(frame,len);
}
И, раз мы уже в нужном файле, то заодно в функции подсчёта контрольной суммы checksum добавим ещё одно условие для рассчёта суммы для пакета TCP. В третьем входящем аргументе для TCP мы будем использовать значение 2
if(type==1)
{
sum+=IP_UDP;
sum+=len-8;
}
if(type==2)
{
sum+=IP_TCP;
sum+=len-8;
}
Вернёмся в функцию приёма пакетов TCP в файл tcp.c и отобразим в ней некоторые данные пришедшего пакета
uint8_t res=0;
sprintf(str1,"%02X:%02X:%02X:%02X:%02X:%02X-%02X:%02X:%02X:%02X:%02X:%02X; %d tcprn",
frame->addr_src[0],frame->addr_src[1],frame->addr_src[2],
frame->addr_src[3],frame->addr_src[4],frame->addr_src[5],
frame->addr_dest[0],frame->addr_dest[1],frame->addr_dest[2],
frame->addr_dest[3],frame->addr_dest[4],frame->addr_dest[5],len);
HAL_UART_Transmit(&huart1,(uint8_t*)str1,strlen(str1),0x1000);
Давайте теперь посмотрим, прийдёт ли к нам такой пакет. Запустим для начала утилиту WireShark, отфильтровавшись в ней как всегда по MAC-адресу нашего модуля, а также запустим утилиту telnet. Если у кого-то в ОС Windows её не окажется, то её можно будет установить в следующем месте. Запустим «программы и компонеты» из панели управления, включив там изображение, например, в виде мелких значков. Затем слева выберем «Включение или выключение компонетнов Windows«
В открывшемся диалоге поставим галку возле «Клиент Telnet» и нажмём ОК
Клиент установится и будет доступен для запуска из командной строки «Выполнить«, которая доступна по комбинации клавиш «Win+R«
Соберём и прошьём наш код в контроллер,в командной строке утилиты telnet попытаемся соединиться с нашим модулем по протоколу TCP, введя следующую команду
Как мы видим, соединиться нам не удастся, так как мы ещё не написали процедуру ответа на пакет, но тем не менее попытки соединения к нам пришли в терминальную программ
Также эти попытки соединения мы можем наблюдать и в WireShark
В следующей части урока мы напишем весь код организации всех ответов и запросов для установки и разрыва соединения и проверим это на практике.
Предыдущий урок Программирование МК STM32 Следующая часть
Отладочную плату STM32F103C8T6 можно приобрести здесь STM32F103C8T6
Программатор недорогой можно купить здесь ST-Link V2
Ethernet LAN Сетевой Модуль можно купить здесь ENC28J60 Ethernet LAN Сетевой Модуль.
Переходник USB to TTL можно приобрести здесь ftdi ft232rl
Смотреть ВИДЕОУРОК (нажмите на картинку)
Добавить комментарий