STM Урок 133. LAN8742A. LWIP. SOCKET. TCP Server
Продолжаем работу с интерфейсом SOCKET библиотеки стека протоколов LWIP и переходим к следующему транспортному протоколу TCP (Transmission Control Protocol, протокол управления передачей). С данным протоколом мы уже встречались неоднократно. Поэтому, хоть он и является непростым, мы его изучили очень неплохо. Мы знаем, как именно происходит создание и разрыв соединения, знаем, как передаются пакеты TCP, как они делятся на сегменты. Поэтому работать нам с ним и анализировать определённые создающиеся нестандартные ситуации мы уже можем спокойно. Изучая стек протоколов LWIP, мы также сталкивались с протоколом TCP, даже соединяли с помощью него два контроллера, организовывали между ними передачу данных. Правда, всё это происходило с использованием интерфейсов RAW и NETCONN. Второй тип интерфейса уже использовался с участием операционной системы реального времени FREERTOS. Теперь нам уже предстоит поработать с протоколом TCP с использованием интерфейса SOCKET, в котором, как мы уже убедились на протоколе UDP имеется ряд тонкостей, которые помогают нам упростить и практически полностью автоматизировать обмен данными между устройствами.
Со всеми тонкостями программирования протокола TCP мы будем знакомиться в процессе написания кода.
В качестве платы для испытания мы возьмём STM32F746-Discovery, а проект для прототипа мы возьмём из урока 131 с именем LAN8742_UDP_SERVER_SOCKET. Присвоим ему теперь соответствующее имя LAN8742_TCP_SERVER_SOCKET и откроем его в Cube MX.
Добавим в настройках LWIP в разделе Key Options размер кучи до максимального, так как мы уже можем спокойно работать с увеличенным размером кучи

Сгенерируем проект для System Workbench, откроем его там, уберём отладочные настройки при их наличии, а также изменим уровень оптимизации на 1.
Откроем файл main.c и первым делом поправим шапку проекта в main()
TFT_DisplayString(0, 10, (uint8_t *)"TCP Server", CENTER_MODE);
Попробуем собрать проект. Если всё нормально, движемся дальше.
Так как протокол TCP ориентирован на соединение, то целесообразно в одной задаче создать соединение, а обмен с клиентом (или с несколькими клиентами) осуществлять в другой.
Сначала переименуем функцию udp_thread в tcp_thread, удалим из её тела полностью весь код, чтобы не запутаться впоследствии, а также в функции задачи по умолчанию StartDefaultTask изменим имя в создании задачи. Также для задачи увеличим вдвое размер стека
sys_thread_new("tcp_thread", tcp_thread, (void*)&sock01, DEFAULT_THREAD_STACKSIZE * 2, osPriorityNormal);
Над функцией tcp_thread создадим ещё одну функцию для обмена пакетами с клиентом (или клиентами)
|
1 2 3 4 5 |
//--------------------------------------------------------------- static void client_socket_thread(void *arg) { } //--------------------------------------------------------------- |
В функции tcp_thread добавим ряд локальных переменных и указателей
|
1 2 3 4 5 6 7 |
static void tcp_thread(void *arg) { struct_sock *arg_sock; int sock, accept_sock; struct sockaddr_in address, remotehost; socklen_t sockaddrsize; arg_sock = (struct_sock*) arg; |
Создадим сокет
|
1 2 3 4 |
arg_sock = (struct_sock*) arg; if ((sock = socket(AF_INET,SOCK_STREAM, 0)) >= 0) { } |
Мы видим, что во втором аргументе у нас появился параметр SOCK_STREAM, который говорит библиотеке, чтобы сокет создался именно для потоковой передачи. В третьем аргументе поставим ноль, что заставит функцию подобрать параметр по умолчанию, а это и будет протокол TCP.
Если сокет нормально создался, то проинициализируем поля структуры нашего интерфейса (интерфейса сервера)
|
1 2 3 4 5 |
if ((sock = socket(AF_INET,SOCK_STREAM, 0)) >= 0) { address.sin_family = AF_INET; address.sin_port = htons(arg_sock->port); address.sin_addr.s_addr = INADDR_ANY; |
Здесь по сравнению с протоколом UDP ничего не изменилось.
Свяжем сокет со структурой интерфейса и проверим то, что всё прошло нормально. В противном случае сокет закроем
|
1 2 3 4 5 6 7 8 9 |
address.sin_addr.s_addr = INADDR_ANY; if (bind(sock, (struct sockaddr *)&address, sizeof (address)) == 0) { } else { close(sock); return; } |
А в случае положительно результата начнём слушать сокет, применив для этого функцию listen, первым аргументом которой будет дескриптор сокета, а вторым — максимальное количество попыток соединений в очереди
|
1 2 3 |
if (bind(sock, (struct sockaddr *)&address, sizeof (address)) == 0) { listen(sock, 5); |
Добавим бесконечный цикл, в котором ответим на попытку соединения с клиентом, если таковая будет иметь место
|
1 2 3 4 5 |
listen(sock, 5); for(;;) { accept_sock = accept(sock, (struct sockaddr *)&remotehost, (socklen_t *)&sockaddrsize); } |
Данная функция создаёт ещё один сокет — сокет для клиента. Соединений может быть не одно, поэтому и дескрипторы будут под разными номерами. Выведем номер дескриптора через шину USART в терминальную программу на ПК
|
1 2 3 |
accept_sock = accept(sock, (struct sockaddr *)&remotehost, (socklen_t *)&sockaddrsize); sprintf(str_usart," socket: %d\r\n", accept_sock); HAL_UART_Transmit(&huart1,(uint8_t*)str_usart,strlen(str_usart),0x1000); |
Создадим глобальную структуру для параметра передачи в задачу обмена пакетами с клиентом и создадим переменную типа данной структуры
|
1 2 3 4 5 6 7 8 |
struct_conn conn01; typedef struct struct_client_socket_t { struct sockaddr_in remotehost; socklen_t sockaddrsize; int accept_sock; uint16_t y_pos; } struct_client_socket; struct_client_socket client_socket01; |
Я думаю, смысл полей структуры понятен. А если не понятен, то сейчас мы всё увидим.
Вернёмся в функцию tcp_thread и в случае положительного результата создания сокета для соединения с клиентом заполним поля данной структуры и затем создадим задачу для обмена пакетами с клиентом
|
1 2 3 4 5 6 7 8 9 |
HAL_UART_Transmit(&huart1,(uint8_t*)str_usart,strlen(str_usart),0x1000); if(accept_sock >= 0) { client_socket01.accept_sock = accept_sock; client_socket01.remotehost = remotehost; client_socket01.sockaddrsize = sockaddrsize; client_socket01.y_pos = arg_sock->y_pos; sys_thread_new("client_socket_thread", client_socket_thread, (void*)&client_socket01, DEFAULT_THREAD_STACKSIZE, osPriorityNormal ); } |
В функции client_socket_thread добавим несколько структур и переменных, также объявим размер буфера для приёма и передачи пакетов, а также заберём наши параметры в соответствующую переменную структуры
|
1 2 3 4 5 6 7 8 |
static void client_socket_thread(void *arg) { int buflen = 150; int ret, accept_sock; struct sockaddr_in remotehost; socklen_t sockaddrsize; struct_client_socket *arg_client_socket; arg_client_socket = (struct_client_socket*) arg; |
Присвоим параметры, пришедшие из другой задачи, соответствующим переменным
|
1 2 3 4 |
arg_client_socket = (struct_client_socket*) arg; remotehost = arg_client_socket->remotehost; sockaddrsize = arg_client_socket->sockaddrsize; accept_sock = arg_client_socket->accept_sock; |
Добавим бесконечный цикл, в котором попытаемся принять пакет от клиента
|
1 2 3 4 5 |
accept_sock = arg_client_socket->accept_sock; for(;;) { ret = recvfrom( accept_sock,out_buffer, buflen, 0, (struct sockaddr *)&remotehost, &sockaddrsize); } |
Приём пакета происходит аналогично тому, как и в случае с UDP.
В качестве буфера используем одну из областей памяти, объявленных в предыдущих занятиях в области памяти SDRAM.
Если пакет пришел, то добавим ноль к пакету, чтобы использовать его как строку для отображения на дисплее
|
1 2 3 4 5 |
ret = recvfrom( accept_sock,out_buffer, buflen, 0, (struct sockaddr *)&remotehost, &sockaddrsize); if(ret > 0) { out_buffer[ret] = 0; } |
Чтобы нам закрыть соединение, мы будем с клиента посылать команду, например «-c«, поэтому давайте напишем обнаружение подобной строки, в теле которого закроем сокет и уничтожим задачу
|
1 2 3 4 5 6 |
out_buffer[ret] = 0; if(strcmp((char*)out_buffer, "-c") == 0) { close(accept_sock); osThreadTerminate(NULL); } |
В качестве клиента мы будем использовать программу Putty, которая любит перевод каретки и возврат строки оформлять в отдельный пакет. Поэтому давайте отфильтруем такие пакеты и не будем на них никак реагировать. А в случае нормального пакета мы, наоборот, добавим к пакету возврат каретки и перевод строки
|
1 2 3 4 5 6 |
osThreadTerminate(NULL); } if(strcmp((char*)out_buffer, "\r\n") != 0) { strcat((char*)out_buffer,"\r\n"); } |
Для начала отобразим пришедшую строку в терминальной программе
|
1 2 |
strcat((char*)out_buffer,"\r\n"); HAL_UART_Transmit(&huart1,out_buffer,strlen((char*)out_buffer),0x1000); |
Также отобразим в терминальной программе количество байтов в пакете
|
1 2 3 |
HAL_UART_Transmit(&huart1,out_buffer,strlen((char*)out_buffer),0x1000); sprintf(str_usart,"%d\r\n", ret); HAL_UART_Transmit(&huart1,(uint8_t *)str_usart,strlen(str_usart),0x1000); |
Уберём возврат строки и перевод каретки, вставив вместо них ноль
|
1 2 |
HAL_UART_Transmit(&huart1,(uint8_t *)str_usart,strlen(str_usart),0x1000); out_buffer[ret] = 0; |
Отобразим порт и строку из пакета на дисплее
|
1 2 3 |
out_buffer[ret] = 0; sprintf(str_usart,"%5u %-20s", ntohs(remotehost.sin_port), (char*)out_buffer); TFT_DisplayString(0, 40, (uint8_t *)str_usart, LEFT_MODE); |
Отобразим ещё минимальный оставшийся размер кучи
|
1 2 3 |
TFT_DisplayString(0, 40, (uint8_t *)str_usart, LEFT_MODE); sprintf(str_usart,"%7u",xPortGetMinimumEverFreeHeapSize()); TFT_DisplayString(0, 80, (uint8_t *)str_usart, LEFT_MODE); |
И напоследок отправим строку в качестве «эха» клиенту в ответ, добавив к ней возврат каретки и перевод строки
|
1 2 3 |
TFT_DisplayString(0, 80, (uint8_t *)str_usart, LEFT_MODE); strcat((char*)out_buffer,"\r\n"); sendto(accept_sock,out_buffer,strlen((char*)out_buffer),0,(struct sockaddr *)&remotehost, sockaddrsize); |
Соберём код, прошьём контроллер и запустим программу WireShark, отфильтровавшись по нужному IP сервера.
Также запустим терминальную программу, соединившись в ней с нужным портом.
Запустим программу Putty, настроив необходимый порт и адрес. Запустим соединение, нажав кнопку Open

Посмотрим в анализаторе трафика, что мы благополучно соединились с нашим сервером
Также мы видим в терминальной программе, что у нас создался сокет с дескриптором 1

Попробуем что-нибудь передать серверу

Мы видим, что пакет вернулся.
Также мы видим, что обмен нормально идёт в анализаторе
Также мы видим пришедшую строку и её размер в терминальной программе

Данная строка также отображается и на нашем дисплее

Попробуем создать ещё одно соединение, запустив ещё одну сессию программы Putty.
Соединение у нас создалось благополучно
Сокет с дескриптором 2 также создан

Попробуем передать что-нибудь серверу. Все строки доходят до него отлично

Запустим ещё одну сессию Putty, чтобы создать третье соединение.
У нас появился теперь сокет уже под номером 3
![]()
Строки также отлично передаются.
А вот если мы попытаемся создать четвёртый сокет, то получим скорее всего ошибку, хотя когда я писал первую версию проекта, мне удавалось создать 4 соединения после увеличения кучи. Возможно, что я что-то забыл

В терминальной программе мы видим также ошибку -1
![]()
Ну нам в принципе и трёх соединений хватит. Вы можете поиграть с размерами, возможно у вас получится создать одновременно больше соединений.
Теперь попробуем закрыть одно из трёх соединений, передав известную команду в командной строке Putty. Соединение закроется

Также мы видим обоюдное закрытие соединения в Wireshark
Если мы запустим опять сессию Putty, то у нас создастся сокет снова с номером 3

Давайте закроем все сессии с помощью команды и запустим опять сессию Putty.
У нас снова создастся сокет с дескриптором 1
![]()
Это подтверждает то, что мы спокойно можем сколь угодно закрывать соединения и заново их открывать, самое главное, не превышая максимум в одно время.
Итак, в данном занятии мы научились работать с использованием интерфейса SOCKET уже с протоколом TCP, который ориентирован на обязательное создание соединения. Мы создали несложный, но вполне работоспособный сервер.
Всем спасибо за внимание!
Отладочную плату можно приобрести здесь STM32F746G-DISCOVERY
Смотреть ВИДЕОУРОК в RuTube (нажмите на картинку)
Смотреть ВИДЕОУРОК в YouTube (нажмите на картинку)




Здравствуйте, создал программу по вашему уроку, все работает, но после 5 установленных и затем разорванных соединений не удается создать новое, выдает ошибку Out of memory error, буду очень благодарен за помощь
если ты еще не разобрался в проблеме, дам наводку. Для каждого соединения создается свой нетбуффер и неткон. Если при разрыве соединения их не удалять, то вполне справедливо, памяти будет не хватать. Пользуйся функцией close(sock);
Сам сейчас стою перед проблемой как в lwip отслеживать состояние socket'a? Использую freertos, TCP-server. У меня постоянно крутится задача в которой постоянно запускаю lwip_accept(s,addr,addrlen). Если соединение произошло, создаю две задачи, одну на прием, другую на передачу с информацией по текущему сокету. Все работает отлично. Прием и передача через очередь. Проблема заключается в отслеживании состоянии сокета, пока не разобрался какой функцией оно происходит. Сейчас кроме как отследить неудачную попытку отправки в сокет, клиенту ничего придумать не могу. Ресурсы запустить еще одну задачу на проверку состояния сокета есть.