В предыдущей части урока мы создали и настроили проект, написали несколько функций и попробовали на практике соединиться с сервером TCP.
Теперь надо как-то разъединиться, а мы ещё не писали код. В командной строке с netcat применим комбинацию клавиш Ctrl+C и сервер разорвёт соединение с клиентом
Давайте попробуем разъединиться с нашим сервером.
Для этого в net_cmd напишем код тела второго условия.
if(usartprop.is_tcp_connect==2)//статус попытки разорвать соединение TCP с сервером
{
ip_extract(buf_str,usartprop.usart_cnt-1,ip);
port=port_extract(buf_str,usartprop.usart_cnt-1);
usartprop.usart_cnt=0;
usartprop.is_tcp_connect=0;
//проверим что IP правильный
if(!memcmp(ip,ipaddr_dest,4))
{
//также проверим, что порт тоже правильный
if(port==port_dest)
{
/* close tcp connection */
tcp_recv(client_pcb, NULL);
tcp_sent(client_pcb, NULL);
tcp_poll(client_pcb, NULL,0);
tcp_close(client_pcb);
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_15, GPIO_PIN_SET);
}
}
}
Здесь мы также извлекаем из строки IP-адрес и номер порта сервера, заносим их в локальные переменные, сравниваем с глобальными, и если сходится то разъединяемся с сервером, уничтожив сначала адреса callback-функций соединения.
Давайте соберём код, прошьём контроллер и проверим, корректно ли происходит разрыв соединения с сервером по соответствующей команде.
Опять таким же образом с помощью команды соединяемся с сервером, а затем с помощью команды «c:» просим клиент разорвать соединение
Мы видим, что разрыв соединения произошел корректно.
Отлично! Осталось нам научить наш клиент принимать и передавать данные.
Сначала давайте допишем наши все служебные функции.
Для этого создадим ещё четыре callback-функции, добавив их над функцией инициализации
//-----------------------------------------------
static err_t tcp_client_recv(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err)
{
err_t ret_err;
return ret_err;
}
//----------------------------------------------------------
static void tcp_client_send(struct tcp_pcb *tpcb, struct client_struct * es)
{
}
//----------------------------------------------------------
static err_t tcp_client_sent(void *arg, struct tcp_pcb *tpcb, u16_t len)
{
return ERR_OK;
}
//----------------------------------------------------------
static err_t tcp_client_poll(void *arg, struct tcp_pcb *tpcb)
{
err_t ret_err;
return ret_err;
}
//----------------------------------------------------------
С назначением данных функций мы познакомимся попозже.
Добавим для этих функций также прототипы в текущем файле
static err_t tcp_client_connected(void *arg, struct tcp_pcb *tpcb, err_t err);
static err_t tcp_client_recv(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err);
static void tcp_client_send(struct tcp_pcb *tpcb, struct client_struct * es);
static err_t tcp_client_sent(void *arg, struct tcp_pcb *tpcb, u16_t len);
static err_t tcp_client_poll(void *arg, struct tcp_pcb *tpcb);
Создадим перечисление для состояний соединения
struct tcp_pcb *client_pcb;
//-----------------------------------------------
enum client_states
{
ES_NOT_CONNECTED = 0,
ES_CONNECTED,
ES_RECEIVED,
ES_CLOSING,
};
//-------------------------------------------------------
Затем ещё структуру для свойств клиентского соединения
//-------------------------------------------------------
struct client_struct
{
enum client_states state; /* connection status */
struct tcp_pcb *pcb; /* pointer on the current tcp_pcb */
struct pbuf *p_tx; /* pointer on pbuf to be transmitted */
};
struct client_struct *cs;
//-----------------------------------------------
Назначения полей расшифрованы в комментариях.
Теперь добавим функцию для корректного разрыва соединения с сервером после функции tcp_client_connected
//----------------------------------------------------------
static void tcp_client_connection_close(struct tcp_pcb *tpcb, struct client_struct * es)
{
/* remove callbacks */
tcp_recv(tpcb, NULL);
tcp_sent(tpcb, NULL);
tcp_poll(tpcb, NULL,0);
if (es != NULL)
{
mem_free(es);
}
/* close tcp connection */
tcp_close(tpcb);
}
//----------------------------------------------------------
В теле данной функции мы уничтожаем адреса callback-функций, тажке освобождаем память, отведённую под поля нашей структуры, и разъединяемся с сервером.
Добавим для этой функции также прототип в текущем файле
static err_t tcp_client_connected(void *arg, struct tcp_pcb *tpcb, err_t err);
static void tcp_client_connection_close(struct tcp_pcb *tpcb, struct client_struct * es);
static err_t tcp_client_recv(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err);
Теперь начнём писать тело функции tcp_client_connected, которая уже давно у нас скучает без тела
static err_t tcp_client_connected(void *arg, struct tcp_pcb *tpcb, err_t err)
{
struct client_struct *es = NULL;
if (err == ERR_OK)
{
}
else
{
tcp_client_connection_close(tpcb, es);
}
return err;
}
Мы создаём структуру соединения, затем напишем условие, что мы попали в данную функцию без ошибки, а если с ошибкой, то разъединяемся с сервером.
темерь начнем писать тело условия в случае положительного результата
if (err == ERR_OK)
{
es = (struct client_struct *)mem_malloc(sizeof(struct client_struct));
if (es != NULL)
{
}
else
{
tcp_client_connection_close(tpcb, es);
return ERR_MEM;
}
}
Здесь мы выделяем память под нашу стркутуру и в случае невозможности выделить память разрываем соединение с сервером и уходим из функции с ошибкой памяти.
Теперь пишем код тела условия выделения памяти в случае истинного результата
if (es != NULL)
{
es->state = ES_CONNECTED;
es->pcb = tpcb;
es->p_tx = pbuf_alloc(PBUF_TRANSPORT, strlen((char*)data) , PBUF_POOL);
}
Мы присваиваем полям нашей структуры соответствующие значения — статус успешного соединения, также адрес структуры соединения, а тажке в качестве буфера присваиваем адрес нашего массива для данных.
Не выходя из условия, пишем код дальше
es->p_tx = pbuf_alloc(PBUF_TRANSPORT, strlen((char*)data) , PBUF_POOL);
if (es->p_tx)
{
/* copy data to pbuf */
pbuf_take(es->p_tx, (char*)data, strlen((char*)data));
/* pass newly allocated es structure as argument to tpcb */
tcp_arg(tpcb, es);
/* initialize LwIP tcp_recv callback function */
tcp_recv(tpcb, tcp_client_recv);
/* initialize LwIP tcp_sent callback function */
tcp_sent(tpcb, tcp_client_sent);
/* initialize LwIP tcp_poll callback function */
tcp_poll(tpcb, tcp_client_poll, 1);
/* send data */
tcp_client_send(tpcb,es);
return ERR_OK;
}
В случае успеха выделения памяти под буфер мы копируем данные из него, затем берём аргументы в нашу структуру, и затем назначаем добавленные нами callback-функции в качестве функций для различных условий нашего TCP-соединения и передаём буфер, если он не пустой. Для функции tcp_client_poll мы передаём также параметр — единицу. Это величина в секундах, означающая — как часто будет данная функция вызываться.
Теперь начнём писать тело callback-функции приёма пакетов TCP.
Также добавляем переменную нашей структуры
static err_t tcp_client_recv(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err)
{
struct client_struct *es;
err_t ret_err;
Передаём ей адрес аргументов, которые являются аналогичной стркутурой
err_t ret_err;
es = (struct client_struct *)arg;
Добавим код с несколькими условиями на разные случаи жизни
es = (struct client_struct *)arg;
if (p == NULL)
{
}
else if(err != ERR_OK)
{
}
else if(es->state == ES_CONNECTED)
{
}
else if (es->state == ES_RECEIVED)
{
}
else
{
}
return ret_err;
Обработаем первое условие — получение пакета без данных со статусом разъединения. Это как правило пакет с флагом FIN от сервера
if (p == NULL)
{
es->state = ES_CLOSING;
if(es->p_tx == NULL)
{
tcp_client_connection_close(tpcb, es);
}
re
t_err = ERR_OK;
}
В этом случае мы разъединяемся с сервером.
Напишем код тела следующего условия — пришёл непустой пакет со статусом ошибки
else if(err != ERR_OK)
{
if (p != NULL)
{
pbuf_free(p);
}
ret_err = err;
}
В данном случае мы освобождаем память буфера и уходим из тела цикла, занеся в переменную статуса возврата код ошибки.
Обрабатываем следующее условие — пришел пакет при нормальном соединении
else if(es->state == ES_CONNECTED)
{
message_count++;
tcp_recved(tpcb, p->tot_len);
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_15, GPIO_PIN_SET);
es->p_tx = p;
strncpy(str1,es->p_tx->payload,es->p_tx->len);
str1[es->p_tx->len] = '\0';
HAL_UART_Transmit(&huart6, (uint8_t*)str1,strlen(str1),0x1000);
ret_err = ERR_OK;
}
Мы наращиваем счётчик сообщений, принимаем наш пакет в буфер, присваиваем указатель на буфер нашей структуре, копируем содержимое буфера в строчный массив, заканчиваем строку нулём, передаём её в терминальную программу для просмотра и уходим из тела условия, занеся в переменную статуса возврата успех.
Пишем код тела следующего условия — успешного приёма данных
else if (es->state == ES_RECEIVED)
{
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_13, GPIO_PIN_SET);
ret_err = ERR_OK;
}
В данном случае мы просто зажигаем светодиод и уходим с успешным статусом.
И последнее тело — это случай, который не подходит под все остальные — как правило это подтверждение приёма данных
else
{
/* Acknowledge data reception */
tcp_recved(tpcb, p->tot_len);
/* free pbuf and do nothing */
pbuf_free(p);
ret_err = ERR_OK;
}
return ret_err;
Пишем код следующей callback-фукции, вызываемой при отправке данных серверу
static void tcp_client_send(struct tcp_pcb *tpcb, struct client_struct * es)
{
struct pbuf *ptr;
err_t wr_err = ERR_OK;
while ((wr_err == ERR_OK) &&
(es->p_tx != NULL) &&
(es->p_tx->len <= tcp_sndbuf(tpcb)))
{
}
}
Мы создаём указатель на буфер, создаём пееменную со статусом и создаём цикл, в котором мы будем находиться в случае выполнении сразу трёх условий — у нас не будет ошибки, будет ненулевой указатель на буфер, а также длина буфера будет меньше или равна количеству передаваемых данных.
Теперь пишем тело нашего цикла
while ((wr_err == ERR_OK) &&
(es->p_tx != NULL) &&
(es->p_tx->len <= tcp_sndbuf(tpcb)))
{
ptr = es->p_tx;
wr_err = tcp_write(tpcb, ptr->payload, ptr->len, 1);
Мы получим указатель на буфер и вызовем функцию отправки в буфер содержимого для отправки. Последняя единичка — это флаг. Если единичка — то пакет будет отправлен без флага PSH, а если двойка — то с ним.
Пишем тело цикла дальше. Создадим несколько условий
wr_err = tcp_write(tpcb, ptr->payload, ptr->len, 1);
if (wr_err == ERR_OK)
{
}
else if(wr_err == ERR_MEM)
{
}
else
{
/* other problem ?? */
}
Напишем код тела первого условия — если функция записи в буфер нам вернёт нормальный результат
if (wr_err == ERR_OK)
{
es->p_tx = ptr->next;
if(es->p_tx != NULL)
{
pbuf_ref(es->p_tx);
}
pbuf_free(ptr);
}
Здесь мы переходим на следующую цепочку данных, если она есть, то наращиваем счётчик ссылок, и затем освобождаем буфер.
Обработаем следующее условие — если ошибка памяти.
else if(wr_err == ERR_MEM)
{
es->p_tx = ptr;
}
Мы присваиваем соответствующему полю структуры указатель на буфер, то есть откладываем передачу. То есть у нас буфер пока занят.
Ну а противный случай оставим пока пустым.
Работаем с телом следующей callback-функции — tcp_client_sent, которая вызывается после передачи буфера серверу.
static err_t tcp_client_sent(void *arg, struct tcp_pcb *tpcb, u16_t len)
{
struct client_struct *es;
LWIP_UNUSED_ARG(len);
es = (struct client_struct *)arg;
if(es->p_tx != NULL)
{
tcp_client_send(tpcb, es);
}
return ERR_OK;
Мы также создаём указатель на структуру, передаём ему аргументы, и если что-то осталось в буфере, то передаём его серверу.
И последняя callback-функция, тело которой мы начнём писать это функция tcp_client_poll, которая вызывается постоянно и занимается мониторингом сети, даже если нет никакого обмена
err_t ret_err;
struct client_struct *es;
es = (struct client_struct*)arg;
HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_14);
Мы также создадим указатель на структуру, передадим ему аргументы и изменим состояние светодиода.
Пишем код тела функции дальше
HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_14);
if (es != NULL)
{
}
else
{
}
return ret_err;
Мы создали условие, которое проверит что указатель на структуру соединения существует.
Напишем сначала тело положительного результата условия
if (es != NULL)
{
if (es->p_tx != NULL)
{
}
else
{
if(es->state == ES_CLOSING)
{
tcp_client_connection_close(tpcb, es);
}
}
ret_err = ERR_OK;
}
Если у нас содержимое буфера для передачи ненулевое, то мы ничего не делаем, для этого у нас есть другая функция, а если статус закрытия соединения, то закроем его выйдем из условия с хорошим статусом.
Теперь противный случай
else
{
tcp_abort(tpcb);
ret_err = ERR_ABRT;
}
return ret_err;
В этом случае мы ничего не делаем, при чём для этого есть специальная функция tcp_abort, и выходим со специальным статусом возврата.
С функциями-обработчиками событий мы закончили.
Осталось нам обрабоать строку из терминала, не являющуюся командой.
Для этого мы над функцией разбора строки string_parse напишем функцию передачи строки серверу
//-----------------------------------------------
void sendstring(char* buf_str)
{
tcp_sent(client_pcb, tcp_client_sent);
tcp_write(client_pcb, (void*)buf_str, strlen(buf_str), 1);
tcp_output(client_pcb);
usartprop.usart_cnt=0;
usartprop.is_text=0;
}
//-----------------------------------------------
Строка (и вообще любые данные) передаются по TCP следующим образом: сначала инициализируется функция- обработчик, затем данные записываются в буфер с помощью специальной функции библиотеки, а затем вызывается функция передачи подготовленного буфера. после передачи мы сбросим счётчик и статус обычной строки.
Ну и понятно, функция никогда не будет работать, если её не вызвать.
Перейдём в функцию string_parse и напишем тело условия разбора строки
else
{
usartprop.is_text=1;
sendstring(buf_str);
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_12|GPIO_PIN_13, GPIO_PIN_RESET);
}
Вот вроде бы и всё с кодом. Осталось посмотреть результат наших трудов.
Соберём код, прошьём контроллер, запустим netcat на прослушку порта, соединимся с ним с помощью специальной команды и попробуем что-нибудь послат компьютеру их терминальной программы
Посмотрим, пришла ли строка в консоль netcat
Всё пришло! Отлично!
Теперь наоборот, передадим строку из консоли в наш МК
Посмотрим, пришло ли сообщение в контроллер, в окне терминальной программы
Всё пришло.
Передадим команду на разъединение.
Клиент с сервером успешно разъединились.
Посмотрим обмен в WireShark
Обмен происходил успешно. Можно также посмотреть. все ли байты пришли, но мы этого делать не будем, так как я это неоднократно проверял, и если и были ошибки, я их уже устранил по мере сочинения кода.
Таким образом, мы узнали, как можно организовать простейший TCP-клиент с использованием стека протоколов LWIP при помощи микросхемы LAN8720, включающей в себя только физический уровень.
Спасибо за внимание!
Предыдущая часть Программирование МК STM32 Следующий урок
Отладочную плату можно приобрести здесь: STM32F4-DISCOVERY
Модуль LAN можно приобрести здесь: LAN8720
Плату расширения можно приобрести здесь: STM32F4DIS-BB
Переходник USB to TTL можно приобрести здесь ftdi ft232rl
Смотреть ВИДЕОУРОК (нажмите на картинку)
«которая уже давно у нас скучает без тела» — минутка шутки юмора)))
Спасибо большое за урок, все заработало.Скажите, а каким образом IP address назначения можно в общепринятом виде задать типа narodstream.ru ?
Это уже другой уровень — это DNS.
У вас ошибка в коде на сайте. А в архиве ошибки нет.
str1[es->p_tx->len] = »;
, а должно быть
str1[es->p_tx->len] = '\0';
Спасибо!
Опять WordPress глючит
добрый день.
Соединение на момент написания поста не удается установить коннект stm32f407 c netcat x64 запущенной на ОС Windows 10 с последними обновлением. Акула фиксирует
870 159.220181 192.168.1.193 192.168.1.35 TCP 60 49153 → 3333 [SYN] Seq=0 Win=2048 Len=0 MSS=460
871 159.220377 192.168.1.35 192.168.1.193 TCP 58 3333 → 49153 [SYN, ACK] Seq=0 Ack=1 Win=65392 Len=0 MSS=1460
872 159.221911 192.168.1.193 192.168.1.35 TCP 60 49153 → 3333 [ACK] Seq=1 Ack=1 Win=2048 Len=0
И тут сервер разрывает соединение
873 159.224575 192.168.1.35 192.168.1.193 TCP 54 3333 → 49153 [RST, ACK] Seq=1 Ack=1 Win=0 Len=0
Танцы с бубном ничего не дали. В итоге я поднял linux там запустил netcat и коннект и разъединение происходит корректно.
Такое ощущение что блокирует firewall виндовс блокирует корректное TCP соединение У кого нибудь были такие траблы?
ola amigo!
eu consigo usar tcp cliente e tcp server juntos no mesmo LAN8720?
Последняя единичка – это флаг. Если единичка – то пакет будет отправлен без флага PSH, а если двойка – то с ним.»
Как бы правильно насчёт PSH, но вот единичка как бы независимый флажок:)
===== include/lwip/tcp.h =====
/* Flags for «apiflags» parameter in tcp_write */
#define TCP_WRITE_FLAG_COPY 0x01
#define TCP_WRITE_FLAG_MORE 0x02
================================
После отправок с сервера 17-ти штук «Hello» STM32 разрывает соединение, соответственно, больше не принимает сообщения от сервера. В чем дело не могу понять. Всегда именно после 17-го «Hello».
– NUCLEO-STM32F746
– CoolTerm
– Hercules
Скрин не прикрепляю, потому что комментарий уходит в спам.
В чем дело, подскажите. Хотя бы направьте. Ощущение, что некий буфер переполняется и выходит в ошибку. Не могу найти, что и где ломается.
Нашел, в чем была проблема. Не знаю, правильно ли её решил, но суть такая:
— в фале net.c в функции static err_t tcp_client_recv(…) в условии нормального приема посылки нужно вставить pbuf_free(p).
Не уверен, что это на 100% правильно, но по крайней мере завелось и заработало корректно.
Кусок области кода:
else if(es->state == ES_CONNECTED)
{
message_count++;
tcp_recved(tpcb, p->tot_len);
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_7);
es->p_tx = p;
strncpy(str1,es->p_tx->payload,es->p_tx->len);
str1[es->p_tx->len] = '\0';
HAL_UART_Transmit(&huart3, (uint8_t*)str1,strlen(str1),0x1000);
pbuf_free(p);
ret_err = ERR_OK;
}
И все бы ничего, но после нескольких отправок по TCP на STM32 и обратно снова происходит дисконнект. И снова непонятно, в чем причина.