Продолжаем работу по программированию микроконтроллера ESP8266 с использованием операционной системы реального времени FREEFTOS, а также продолжаем работу с протоколом TCP (Transmission Control Protocol). И на данном уроке мы уже попытаемся создать простенький TCP сервер, который позволит нам обрабатывать пришедшие пакеты от клиентов, а также отвечать на них. Напомню также, что мы также этим раньше подобные задачи решали с использованием других контроллеров, поэтому нам будет гораздо легче справиться с нашей задачей.
Схема наша осталась прежняя
А проект мы, как обычно, за основу возьмём из прошлого урока с именем WIFI_STA_TCP_CLIENT_RTOS и дадим ему новое имя WIFI_STA_TCP_SERVER_RTOS. Откроем наш проект в Eclipse.
Я думаю, что ни для кого не секрет, что работа любого сервера связана с затратами ресурсов, в том числе памяти, поэтому давайте будем это дело отслеживать, чтобы знать, какие у нас имеются возможности. Для этого перейдём в файл main.c и в функции дополнительной задачи task1 добавим следующую строку, которая нам покажет в терминальной программе, сколько у нас осталось памяти в куче, а строку с версией SDK можно будет удалить
os_printf("SDK version:%s\n", system_get_sdk_version());
os_printf("Heap free size: %d\n", xPortGetFreeHeapSize());
Вполне возможно, что для работы функции xPortGetFreeHeapSize потребуется внести некоторые изменения в настройках FreeRTOS в конфигурационных файлах. Что требуется для какого функционала, мы изучали, когда работали с контроллерами STM32 и, думаю, что возвращаться к этому сейчас — дополнительная трата времени.
Давайте сразу проверим, работает ли наша функция, для чего запустим терминальную программу и прошьём наш контроллер.
Мы должны получить примерно следующее

Также предлагаю здесь смотреть сетевой адрес нашей платы, так как не всегда можно подключиться из терминальной программы сразу, а он нам будет нужен, чтобы отфильтровать по нему пакеты затем в программе анализатора сетевого трафика.
В этой же функции объявим несколько переменных
|
1 2 3 4 5 |
void ICACHE_FLASH_ATTR task1(void *pvParameters) { struct ip_info ipinfo; uint32_t ip; char ip_char[17]; |
Пропишем в строковый массив сначала хотя бы какой-то адрес, так как вдруг мы его сразу не получим
|
1 2 |
char ip_char[17]; snprintf(ip_char, sizeof(ip_char), "%s", "192.168.0.1"); |
Пропишем его в переменную структуры
|
1 2 3 |
snprintf(ip_char, sizeof(ip_char), "%s", "192.168.0.1"); ip = ipaddr_addr(ip_char); memcpy(&ipinfo.ip, &ip, 4); |
В бесконечном цикле получим адрес нашей станции и отобразим его в терминальной программе
|
1 2 3 |
os_printf("Heap free size: %d\n", xPortGetFreeHeapSize()); wifi_get_ip_info(STATION_IF, &ipinfo); os_printf("Addr STA: %s\n", inet_ntoa(ipinfo.ip)); |
Проверим отображение адреса, собрав код и прошив контроллер

Займёмся теперь собственно сервером.
Перейдём в файл wifi.c и удалим для начала функцию recv_task вместе с её телом.
Вместо неё добавим другую функцию, для задачи, которая будет заниматься отдельным клиентом, подключенным к серверу
|
1 2 3 4 5 |
//------------------------------------------------ void ICACHE_FLASH_ATTR client_socket_task(void *pvParameters) { } //------------------------------------------------ |
В функции tcp_task удалим следующие строки
//Заполнение информации о клиенте
cliaddr.sin_family = AF_INET; // IPv4
cliaddr.sin_addr.s_addr = INADDR_ANY;
cliaddr.sin_port = htons(CLIENT_PORT);
А четыре строки, где инициализируется информация о сервере, перенесём выше, до установки связи с сокета с адресом сервера
|
1 2 3 4 5 6 7 |
memset(&cliaddr, 0, sizeof(cliaddr)); //Заполнение информации о сервере servaddr.sin_family = AF_INET; // IPv4 servaddr.sin_addr.s_addr = inet_addr(SERVER_IP); servaddr.sin_port = htons(SERVER_PORT); //Свяжем сокет с адресом сервера if (bind(sockfd, (const struct sockaddr *)&cliaddr, sizeof(struct sockaddr_in)) < 0 ) |
Изменим адрес сервера в данной строке, так как данный адрес может быть разным и это адрес нашего узла
servaddr.sin_addr.s_addr = INADDR_ANY;
Исправим также здесь, изменив в параметрах структуру клиента на структуру сервера
if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(struct sockaddr_in)) < 0 )
Удалим следующие строки:
if (connect(sockfd, (struct sockaddr *)&servaddr,sizeof(struct sockaddr_in)) >= 0)
{
snprintf(str1, sizeof(str1), "Connected");
xQueueSendToBack(xQueue, &xLCDData, 0);
recv_socket01.y_pos = 2;
recv_socket01.sock = sockfd;
xTaskCreate(recv_task, "recv_task", 2048, (void*)&recv_socket01, 3, &recv_handle);
vTaskDelay( 2000 / portTICK_RATE_MS);
for(int i=1; i<10; i++)
{
snprintf(str1, sizeof(str1), "Hello from ESP!!!\n");
write(sockfd,(void *) str1,strlen(str1));
vTaskDelay( 2000 / portTICK_RATE_MS);
}
}
char fl = 1;
xQueueSendToBack(xQueueClose, &fl, 0);
for(;;)
{
xQueueReceive(xQueueCloseAsk, &fl, 0);
if(fl==1)
{
os_printf("task delete\n");
break;
}
vTaskDelay( 10 / portTICK_RATE_MS);
}
После связи сокета с адресом сервера начнём слушать наш сокет
|
1 2 |
os_printf("socket binded\n"); listen(sockfd, 5); |
Надеюсь, мы все помним, что даёт нам цифра во втором параметре. Так и быть, напомню тем, кто забыл. Эта цифра — максимальное количество попыток соединений в очереди.
Объявим локальную переменную для идентификации сокета, который будет использоваться для клиента, пытающегося соединиться с нашим сервером
int sockfd, accept_sock;
Объявим также переменную для хранения размера адреса сокета
|
1 2 |
int sockfd, accept_sock; socklen_t sockaddrsize; |
Также нам будет нужна переменная для хранения сетевого адреса клиента
struct sockaddr_in servaddr, cliaddr, remotehost;
Добавим бесконечный цикл, в котором начнём ожидать подключения клиента, пока клиент не подключится, мы будем висеть в этом месте
|
1 2 3 4 5 |
listen(sockfd, 5); for(;;) { accept_sock = accept(sockfd, (struct sockaddr *)&cliaddr, (socklen_t *)&sockaddrsize); } |
Затем нам надо будет создать задачу для работы с сокетом клиента, подключившегося к нам, передав туда ряд параметров. Для этого объявим глобальную структуру и сразу же объявим и переменную тип данной структуры
|
1 2 3 4 5 6 7 8 |
struct_recv_socket recv_socket01; 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_task и в бесконечном цикле покажем в терминальной программе значение переменной идентификатора сокета
|
1 2 |
accept_sock = accept(sockfd, (struct sockaddr *)&cliaddr, (socklen_t *)&sockaddrsize); os_printf(" socket: %d\n", accept_sock); |
Проинициализируем поля переменной и создадим задачу, передав в качестве параметра нашу переменную
|
1 2 3 4 5 6 7 8 9 |
os_printf(" socket: %d\n", accept_sock); if(accept_sock >= 0) { client_socket01.accept_sock = accept_sock; client_socket01.remotehost = remotehost; client_socket01.sockaddrsize = sockaddrsize; client_socket01.y_pos = accept_sock-1; xTaskCreate(client_socket_task, "client_socket_task", 4096, (void*)&client_socket01, 5, NULL); } |
Теперь займёмся задачей клиента, перейдя в функцию client_socket_task и объявим символьный массив, а также некоторые переменные
|
1 2 3 4 5 6 |
void ICACHE_FLASH_ATTR client_socket_task(void *pvParameters) { char str1[21]; int ret, accept_sock; qData xLCDData; struct_client_socket *arg_client_socket; |
Присвоим адрес параметров задачи объявленному указателю
|
1 2 |
struct_client_socket *arg_client_socket; arg_client_socket = (struct_client_socket*) pvParameters; |
Объявим переменную структуры адреса сокета, а также размера данного адреса
|
1 2 3 |
arg_client_socket = (struct_client_socket*) pvParameters; struct sockaddr_in remotehost; socklen_t sockaddrsize; |
Объявим переменную длины буфера и сразу инициализируем её
|
1 2 |
socklen_t sockaddrsize; int buflen = 150; |
Объявим и инициализируем массив для хранения данных буфера
|
1 2 |
int buflen = 150; char data_buffer[22] = {}; |
Инициализируем поля переменной структуры для задачи дисплея
|
1 2 3 |
char data_buffer[22] = {}; xLCDData.y_pos = arg_client_socket->y_pos; xLCDData.str = str1; |
Присвоим идентификатор сокета локальной переменной
|
1 2 |
xLCDData.str = str1; accept_sock = arg_client_socket->accept_sock; |
Аналогично инициализируем другие переменные
|
1 2 3 |
accept_sock = arg_client_socket->accept_sock; remotehost = arg_client_socket->remotehost; sockaddrsize = arg_client_socket->sockaddrsize; |
Добавим бесконечный цикл, в котором попытаемся принять пакет от клиента
|
1 2 3 4 5 |
sockaddrsize = arg_client_socket->sockaddrsize; for(;;) { ret = recvfrom( accept_sock,data_buffer, buflen, 0, (struct sockaddr *)&remotehost, &sockaddrsize); } |
Если пакет валидный, то есть, если мы провалились не по ошибке, в том числе не по истечению таймаута, который можно также настроить, то обнулим сначала строку в буфере
|
1 2 3 4 5 |
ret = recvfrom( accept_sock,data_buffer, buflen, 0, (struct sockaddr *)&remotehost, &sockaddrsize); if(ret > 0) { data_buffer[ret] = 0; } |
Если пришел пакет с определённой строкой, которая будет служить командой разрыва соединения от клиента, то закроем наш сокет и удалим задачу
|
1 2 3 4 5 6 7 |
data_buffer[ret] = 0; } if(strcmp(data_buffer, "-c") == 0) { close(accept_sock); vTaskDelete(NULL); } |
А если пришел непустой пакет, то скопируем данные из буфера в строковую переменную, остаток до 20 забьём пробелами, завершим нулём и отправим на дисплей в нужную позицию, соответствующую идентификатору (номеру) сокета. Также отправим строку с данными в терминальную программу и покажем в терминальной программе порт и сетевой адрес клиента
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
vTaskDelete(NULL); } if(strcmp(data_buffer, "\r\n") != 0) { snprintf(str1, sizeof(str1), "%s", data_buffer); for(unsigned char i=ret;i<20;i++) {str1[i]=' ';} str1[20] = 0; xQueueSendToBack(xQueue, &xLCDData, 0); strcat(data_buffer,"\n"); os_printf("client port: %5u\n", ntohs(remotehost.sin_port)); os_printf("client addr: %s\n", inet_ntoa(*(struct in_addr*)&remotehost.sin_addr)); os_printf("String: %s\n", data_buffer); } |
В функции инициализации init_esp_wifi удалим создание следующих очередей, которые не используются в проекте, для экономии памяти
xQueueClose = xQueueCreate(10, sizeof(unsigned char));
xQueueCloseAsk = xQueueCreate(10, sizeof(unsigned char));
Также можно удалить и их объявление
xQueueHandle xQueue, xQueueClose, xQueueCloseAsk;
Ну, вроде всё. Наконец-то настал час испытания нашего проекта.
Соберём его и прошьём контроллер, запустим терминальную программу.
В качестве программы-клиента будем использовать программу Putty на компьютере.
Также запустим анализатор пакетов WireShark, в котором отфильтруемся по адресу нашей платы, который мы теперь постоянно видим в терминальной программе.
Попытаемся соединиться с нашим сервером

Увидим в Wireshark, что соединение удалось
В терминальной программе мы также видим, что сокет валидный и после соединения у нас просела память.
Попробуем что-нибудь передать нашей плате

Мы видим, что пакет успешно передан
Также мы видим всю информацию в терминальной программе

На дисплее мы также видим нашу строку

Можно попытаться передать ещё какие-нибудь строки. Всё нормально передаётся.
Запустим ещё один клиент, запустив для этого ещё раз программу Putty и соединившись с сервером. Мы увидим, что создался ещё один сокет и память просела ещё

Попробуем передать строку из этого клиента

В терминальной программе мы увидим данную строку и заметим ещё то, что IP-адрес клиента не изменился, а порт уже другой

На дисплей наша строка также пришла в соответствующую позицию второго сокета

Попытаемся разъединиться с сервером с обеих клиентов, отправив строки -c, увидим следующее в терминальной программе

Мы видим, что объём памяти опять увеличился. Таким образом, мы можем быть уверены, что задачи клиентов успешно удалились.
Теперь посмотрим разъединение узлов в анализаторе трафика
Всё происходит корректно. Не обращайте внимание на разницу в номерах портов при соединении и разъединении. Статья писалась не сразу, происходили коллизии, разъединения с виртуальным портом и т.д.
Можете также попробовать запустить клиенты ещё раз, всё должно работать корректно, я проверял.
Таким образом, на данном занятии нам удалось создать простенький сервер, работающий по протоколу TCP с несколькими клиентами. Мы увидели, что количество таких клиентов ограничено в связи с небольшим размером кучи. Можно поэкспериментировать с размером этой кучи, также с размерами стеков для задач клиентов и других задач и увеличить практическое количество клиентов.
Всем спасибо за внимание!
Предыдущий урок Программирование МК ESP8266 Следующий урок
Модуль ESP NodeMCU можно купить здесь: Модуль ESP NodeMCU
Различные модули ЕSP8266 можно приобрести здесь Модули ЕSP8266
Переходник I2C to LCD можно приобрести здесьI2C to LCD1602 2004
Смотреть ВИДЕОУРОК в RuTube (нажмите на картинку)
Смотреть ВИДЕОУРОК в YouTube (нажмите на картинку)





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