Продолжаем работу с протоколом TCP (Transmission Control Protocol). И на данном уроке мы уже попытаемся создать простенький TCP сервер, который позволит нам обрабатывать пришедшие пакеты от клиентов, а также отвечать на них. Напомню также, что мы также этим раньше подобные задачи решали с использованием других контроллеров, поэтому нам будет гораздо легче справиться с нашей задачей.
Схема наша осталась прежняя
Проект мы, за основу возьмём из прошлого урока с именем WIFI_STA_TCP_CLIENT и дадим ему новое имя WIFI_STA_TCP_SERVER.
Откроем наш проект в Espressif IDE и в файле tcp.c удалим для начала функцию recv_task вместе с её телом.
Вместо неё добавим другую функцию, для задачи, которая будет заниматься отдельным клиентом, подключенным к серверу
1 2 3 4 5 |
//------------------------------------------------ static void client_socket_task(void *pvParameters) { } //------------------------------------------------ |
В функции tcp_task удалим следующие строки
//Заполнение информации о клиенте
cliaddr.sin_family = AF_INET; // IPv4
cliaddr.sin_addr.s_addr = INADDR_ANY;
cliaddr.sin_port = htons(CONFIG_CLIENT_PORT);
Строки с созданием очередей также удалим
xQueueClose = xQueueCreate(10, sizeof(unsigned char));
xQueueCloseAsk = xQueueCreate(10, sizeof(unsigned char));
Изменим адрес сервера в данной строке, так как данный адрес может быть разным и это адрес нашего узла
servaddr.sin_addr.s_addr = INADDR_ANY;
Исправим также комментарий
//Свяжем сокет с адресом сервера
Здесь также исправим имя переменной
if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(struct sockaddr_in)) < 0 )
Выше удалим строку с инициализацией памяти под структуру адреса клиента
memset(&cliaddr, 0, sizeof(cliaddr));
Удалим следующие строки:
if (connect(sockfd, (struct sockaddr *)&servaddr,sizeof(struct sockaddr_in)) >= 0)
{
sprintf(str1, "Connected");
xQueueSendToBack(lcd_string_queue, &xLCDData, 0);
recv_socket01.y_pos = 2;
recv_socket01.sock = sockfd;
xTaskCreate(recv_task, "recv_task", 2048, (void*)&recv_socket01, 3, &xRecvTaskHandle);
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)
{
ESP_LOGI(TAG, "task delete\n");
break;
}
vTaskDelay( 10 / portTICK_RATE_MS);
}
}
После связи сокета с адресом сервера начнём слушать наш сокет
1 2 |
ESP_LOGI(TAG, "socket was binded\n"); listen(sockfd, 5); |
Напомню, что цифра во втором параметре — это максимальное количество попыток соединений в очереди.
Объявим локальную переменную для идентификации сокета, который будет использоваться для клиента, пытающегося соединиться с нашим сервером
int sockfd, accept_sock;
Объявим также переменную для хранения размера адреса сокета
1 2 |
int sockfd, accept_sock; socklen_t sockaddrsize; |
Добавим бесконечный цикл, в котором начнём ожидать подключения клиента, пока клиент не подключится, мы будем висеть в этом месте
1 2 3 4 5 6 |
listen(sockfd, 5); while(1) { memset(&cliaddr, 0, sizeof(cliaddr)); accept_sock = accept(sockfd, (struct sockaddr *)&cliaddr, (socklen_t *)&sockaddrsize); } |
Затем нам надо будет создать задачу для работы с сокетом клиента, подключившегося к нам, передав туда ряд параметров. Для этого объявим глобальную структуру и сразу же объявим и переменную тип данной структуры
1 2 3 4 5 6 7 8 9 10 |
static const char *TAG = "tcp"; //------------------------------------------------------------- typedef struct struct_client_socket_t { struct sockaddr_in cliaddr; 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); printf(" socket: %d\n", accept_sock); |
Объявим переменную для горизонтальной позиции на дисплее
1 2 |
int sockfd, accept_sock; unsigned char y_pos = 0; |
Удалим объявление хендла задачи приёма пакетов
TaskHandle_t xLCDTaskHandle = NULL
, xRecvTaskHandle = NULL;
Проинициализируем поля переменной и создадим задачу, передав в качестве параметра нашу переменную
1 2 3 4 5 6 7 8 9 10 |
printf(" socket: %d\n", accept_sock); if(accept_sock >= 0) { client_socket01.accept_sock = accept_sock; client_socket01.cliaddr = cliaddr; client_socket01.sockaddrsize = sockaddrsize; client_socket01.y_pos = y_pos%4; xTaskCreate(client_socket_task, "client_socket_task", 4096, (void*)&client_socket01, 5, NULL); y_pos++; } |
Теперь займёмся задачей клиента, перейдя в функцию client_socket_task и объявим символьный массив, а также некоторые переменные
1 2 3 4 5 6 |
static void client_socket_task(void *pvParameters) { char str1[21]; int ret, accept_sock; qLCDData 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 cliaddr; socklen_t sockaddrsize; |
Объявим переменную длины буфера и сразу инициализируем её
1 2 |
socklen_t sockaddrsize; int buflen = 150; |
Объявим и инициализируем массив для хранения данных буфера
1 2 |
int buflen = 150; char data_buffer[22] = {}; |
Инициализируем поля переменной структуры для задачи дисплея
1 2 3 |
int buflen = 150; char data_buffer[22] = {}; xLCDData.y_pos = arg_client_socket->y_pos; |
Присвоим адрес строки массиву в структуре дисплея
1 2 |
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; cliaddr = arg_client_socket->cliaddr; sockaddrsize = arg_client_socket->sockaddrsize; |
Добавим бесконечный цикл, в котором попытаемся принять пакет от клиента
1 2 3 4 5 |
sockaddrsize = arg_client_socket->sockaddrsize; while(1) { ret = recvfrom( accept_sock,data_buffer, buflen, 0, (struct sockaddr *)&cliaddr, &sockaddrsize); } |
Если пакет валидный, то есть, если мы провалились не по ошибке, в том числе не по истечению таймаута, который можно также настроить, то сначала строку в буфере завершим нулём на месте символа возврата каретки
1 2 3 4 5 |
ret = recvfrom( accept_sock,data_buffer, buflen, 0, (struct sockaddr *)&cliaddr, &sockaddrsize); if(ret > 0) { data_buffer[ret-1] = 0; } |
Если пришел пакет с определённой строкой, которая будет служить командой разрыва соединения от клиента, то закроем наш сокет и удалим задачу
1 2 3 4 5 6 7 |
data_buffer[ret] = 0; } if(strncmp(data_buffer, "-c", 2) == 0) { close(accept_sock); vTaskDelete(NULL); } |
А если пришел непустой пакет, то скопируем данные из буфера в строковую переменную, остаток до 20 забьём пробелами, завершим нулём и отправим на дисплей в нужную позицию, соответствующую идентификатору (номеру) сокета. Также отправим строку с данными в терминальную программу и обратно клиенту
1 2 3 4 5 6 7 8 9 10 11 12 |
vTaskDelete(NULL); } if(strcmp(data_buffer, "\r\n") != 0) { strcpy(str1, data_buffer); for(unsigned char i=ret-2;i<20;i++) {str1[i]=' ';} str1[20] = 0; xQueueSendToBack(lcd_string_queue, &xLCDData, 0); strcat(data_buffer,"\r\n"); ESP_LOGI(TAG, "Socket %d: %s", accept_sock, data_buffer); sendto(accept_sock,data_buffer,strlen((char*)data_buffer),0,(struct sockaddr *)&cliaddr, sockaddrsize); } |
Ну, вроде всё. Наконец-то настал час испытания нашего проекта.
Соберём его и прошьём контроллер.
В качестве программы-клиента будем использовать программу Putty на компьютере.
Также запустим анализатор пакетов WireShark, в котором отфильтруемся по адресу нашей платы, который мы увидим в терминале при загрузке программы в контроллере.
Также в терминале мы можем видеть, что мы создали сокет и связали с ним адрес сервера
Попытаемся соединиться с нашим сервером
Увидим в Wireshark, что соединение удалось
В терминале мы также видим, что сокет валидный
Попробуем что-нибудь передать нашей плате
Мы видим, что пакет нам вернулся назад, отлично. Также мы видим, что пакеты успешно передаются и принимаются в анализаторе трафика
В терминале мы также видим строку и номер сокета
На дисплее мы также видим нашу строку
Давайте запустим ещё 3 клиента, благо памяти у нашего ESP32 побольше, чем у ESP8266
У нас создались 3 новых сокета
Все клиенты соединились с платой
Теперь давайте попробуем передать из них также строку
Мы видим, что на все пакеты клиенты получили ответы, значит наш сервер клиентов не путает, это хорошо.
Здесь также всё нормально
На дисплее принятые от клиентов строки также отображаются
Попробуем разъединиться
Процесс разъединения прошел корректно
Таким образом, на данном занятии нам удалось создать простенький сервер, работающий по протоколу TCP с несколькими клиентами.
Всем спасибо за внимание!
Предыдущий урок Программирование МК ESP32 Следующий урок
Недорогие отладочные платы ESP32 можно купить здесь Недорогие отладочные платы ESP32
Недорогие отладочные платы ESP32/ESP32-C3/ESP32-S3 можно купить здесь Недорогие отладочные платы ESP32
Логический анализатор 16 каналов можно приобрести здесь
Переходник I2C to LCD можно приобрести здесьI2C to LCD1602 2004
Смотреть ВИДЕОУРОК в RuTube (нажмите на картинку)
Смотреть ВИДЕОУРОК в YouTube (нажмите на картинку)
Смотреть ВИДЕОУРОК в Дзен (нажмите на картинку)
Спасибо