Продолжаем работать с интерфейсом SOCKET с протоколом TCP и сегодня мы попытаемся создать клиент. Как мы убедились на примере клиента для интерфейса NETCONN, что с клиентом работать не только не легче, чем с сервером, но ещё и тяжелее.
В качестве сервера теперь у нас, наоборот, будет ПК.
Проект за основу мы возьмём с прошлого урока с именем LAN8742_TCP_SERVER_SOCKET и теперь у него, соответственно, будет имя LAN8742_TCP_CLIENT_SOCKET.
Откроем наш проект в Cube MX, сначала в настройки ETH и поменяем там MAC-адрес, так как, я думаю вы уже догадались, что мы будем наш клиент затем соединять с сервером на отладочной плате
В настройках LWIP изменим сетевой адрес
Сгенерируем проект для System Workbench и откроем его там. Установим уровень оптимизации в 1 и уберём при наличии отладочные настройки.
В файле main.c в функции main() исправим шапку
TFT_DisplayString(0, 10, (uint8_t *)"TCP Client", CENTER_MODE);
Так как не на всех платах имеется память SDRAM, то по просьбам посетителей ресурса попытаемся обойтись без его использования для массивов.
Поэтому удалим данные глобальные указатели
unsigned char *out_buffer;
char *str_usart;
char *str_out;
unsigned char *send_ws_buf;
char* str_buf1;
char* str_buf2;
char* str_buf3;
Вместо этого добавим глобальные массивы в обычной ОЗУ
1 2 3 4 |
#define MAIL_SIZE (uint32_t) 5 char str_usart[512] = {}; char str_out[32] = {}; char str_buf1[30] = {}; |
Также в функции main() удалим инициализацию массивов в памяти SDRAM
//str_usart 512 bytes
str_usart = (char *) 0xC0400000;
//str_out 32 bytes
str_out = (char *) 0xC0400200;
//out_buffer 192 bytes
out_buffer = (uint8_t *) 0xC0400220;
memset(out_buffer,0,192);
//send_ws_buf 1312 bytes
send_ws_buf = (uint8_t *)0xC04002E0;
//str_buf1 256 bytes
str_buf1 = (char *) 0xC0400800;
//str_buf2 256 bytes
str_buf2 = (char *) 0xC0400900;
//str_buf2 1024 bytes
str_buf3 = (char *) 0xC0400A00;
Также в функции задачи вывода строк на дисплей очистим память очереди
1 2 |
TFT_DisplayString(qstruct->x_pos, qstruct->y_pos, (uint8_t *)str_out, LEFT_MODE); osMailFree(strout_Queue, qstruct); |
Очистка очереди должна происходить именно после того, как мы её воспользовались, то есть забрали оттуда элемент.
Я проанализировал функции выделения и очистки памяти очереди и пришёл именно к такому выводу. До этого я считал, что мы освобождаем память локальной структуры.
Спасибо за наводку автора комментария в группе «В Контакте», который мне это подсказал, иначе я бы до сих пор думал по-прежнему!
Функцию client_socket_thread переименуем в recv_thread и удалим полностью код из её тела.
Аналогично поступим и с функцией tcp_thread, переименовав её в send_thread и также очистим её тело.
В функции задачи по умолчанию StartDefaultTask изменим номер порта нашего устройства-клиента
sock01.port = 1551;
А здесь изменим имя задачи
sys_thread_new("send_thread", send_thread, (void*)&sock01, DEFAULT_THREAD_STACKSIZE * 2, osPriorityNormal);
В функции send_thread добавим несколько локальных переменных, заберём параметры, а также добавим небольшой локальный буфер
1 2 3 4 5 6 7 8 9 |
static void send_thread(void *arg) { struct_out *qstruct; struct_sock *arg_sock; int sock; struct sockaddr_in localhost, remotehost; arg_sock = (struct_sock*) arg; uint32_t syscnt = 0; char buf[15] = {}; |
Создадим сокет для клиента, выделим память под адресную структуру и заполним её поля
1 2 3 4 5 6 7 8 |
char buf[15] = {}; if ((sock = socket(AF_INET,SOCK_STREAM, 0)) >= 0) { memset(&localhost, 0, sizeof(struct sockaddr_in)); localhost.sin_family = AF_INET; localhost.sin_port = htons(arg_sock->port); localhost.sin_addr.s_addr = INADDR_ANY; } |
Свяжем структуру с сокетом. Если связь удачная, то выделим память под адресную структуру сервера и также её проинициализируем, внеся IP-адрес и порт сервера, к которому будем подключаться. А если связь не удалась, то закроем сокет и выйдем из функции
1 2 3 4 5 6 7 8 9 10 11 12 13 |
localhost.sin_addr.s_addr = INADDR_ANY; if (bind(sock, (struct sockaddr *)&localhost, sizeof(struct sockaddr_in)) == 0) { memset(&remotehost, 0, sizeof(struct sockaddr_in)); remotehost.sin_family = AF_INET; remotehost.sin_port = htons(5444); ip4addr_aton("192.168.1.85",(ip4_addr_t*)&remotehost.sin_addr); } else { close(sock); return; } |
Попытаемся связаться с сервером при помощи команда connect() и, если всё прошло нормально, то похвалимся об этом через шину USART в терминальной программе
1 2 3 4 5 6 |
ip4addr_aton("192.168.1.85",(ip4_addr_t*)&remotehost.sin_addr); if (connect(sock, (struct sockaddr *)&remotehost,sizeof(struct sockaddr_in)) >= 0) { sprintf(str_usart,"Connected\r\n"); HAL_UART_Transmit(&huart1,(uint8_t*)str_usart,strlen(str_usart),0x1000); } |
Соединим плату с сетью, подключим её к ПК с помощью кабеля USB, соберём код, прошьём контроллер, запустим анализатор трафика WireShark, отфильтровавшись там по сетевому адресу нашего клиента, также запустим терминальную программу и соединимся с виртуальным портом платы и запустим ещё программу Netcat, только не с командной строки, а с помощью запуска исполняемого файла nc.exe или nc64.exe. Затем в появившейся командной строке введём команду для того, чтобы начать слушать порт сервера (5444)
Перезагрузим контроллер и в анализаторе трафика увидим, что клиент с сервером соединились
Также мы это видим и в терминальной программе
Чтобы разъединиться с клиентом, в программе netcat введём комбинацию клавиш «Ctrl + C«
Соединение разорвётся хоть и некорректно, но всё же порт освободится от прослушки
С корректностью разберёмся позже. Разрывать соединение будем по инициативе клиента, вот тогда всё и нормализуется.
Добавим глобальный идентификатор задачи приёма пактов
1 2 |
#define MAIL_SIZE (uint32_t) 5 osThreadId ThreadRecvHandle; |
Удалим глобальную структуру struct_client_socket, а также переменную типа этой структуры, а вместо неё добавим другую структуру и переменную её типа
1 2 3 4 5 6 |
struct_conn conn01; typedef struct struct_recv_socket_t { int sock; uint16_t y_pos; } struct_recv_socket; struct_recv_socket recv_socket01; |
Вернёмся в функцию , проинициализируем поля нашей переменной и передадим указатель на неё в качестве параметра в задачу приёма пакетов от сервера, создав такую задачу
1 2 3 4 |
HAL_UART_Transmit(&huart1,(uint8_t*)str_usart,strlen(str_usart),0x1000); recv_socket01.y_pos = arg_sock->y_pos; recv_socket01.sock = sock; ThreadRecvHandle = sys_thread_new("recv_thread", recv_thread, (void*)&recv_socket01, DEFAULT_THREAD_STACKSIZE, osPriorityNormal); |
Перейдём теперь в функцию этой задачи recv_thread, заберём указатель на параметры в переменную структуры, добавим локальную переменную и буфер
1 2 3 4 5 6 7 |
static void recv_thread(void *arg) { struct_out *qstruct; struct_recv_socket *arg_recv_socket; arg_recv_socket = (struct_recv_socket*) arg; int recv_data; char data_buffer[30] = {}; |
Добавим бесконечный цикл, в котором попытаемся принять пакет
1 2 3 4 5 |
char data_buffer[30] = {}; for(;;) { recv_data = recv(arg_recv_socket->sock,data_buffer,sizeof(data_buffer),0); } |
Если это пакет с данными, то обнулим в буфере символ перевода строки и отобразим строку на дисплее
1 2 3 4 5 6 7 8 9 10 11 |
recv_data = recv(arg_recv_socket->sock,data_buffer,sizeof(data_buffer),0); if(recv_data > 0) { data_buffer[recv_data-1] = 0; qstruct = osMailAlloc(strout_Queue, osWaitForever); sprintf(qstruct->str,"%-20s", (char*)data_buffer); qstruct->y_pos = arg_recv_socket->y_pos; qstruct->x_pos = 10; qstruct->sfont = Font24; osMailPut(strout_Queue, qstruct); } |
В принципе, задача по приёму пакетов готова. Только мы ещё ничего не передавали серверу, поэтому вернёмся в функцию send_thread и добавим там бесконечный цикл, в котором определим количество системных квантов с момента включения контроллера или его перезагрузки
1 2 3 4 5 |
ThreadRecvHandle = sys_thread_new("recv_thread", recv_thread, (void*)&recv_socket01, DEFAULT_THREAD_STACKSIZE, osPriorityNormal); for(;;) { syscnt = osKernelSysTick(); } |
Если этих квантов больше 50000, то попытаемся закрыть наше соединение, чтобы проверить процесс разъединения с сервером
1 2 3 4 5 |
syscnt = osKernelSysTick(); if(syscnt>50000) { close(sock); } |
Отчитаемся об этом также на дисплее клиента, уничтожим задачу по приёму пакетов и отправимся висеть в бесконечный цикл. Сделано это для того, чтобы процесс разъединения дошёл до конца, иначе задача будет висеть в нормальном приоритете и не даст процессу разъединения завершиться
1 2 3 4 5 6 7 8 9 10 11 12 |
close(sock); qstruct = osMailAlloc(strout_Queue, osWaitForever); sprintf(qstruct->str,"Disconnected"); qstruct->y_pos = arg_sock->y_pos + 40; qstruct->x_pos = 10; qstruct->sfont = Font24; osMailPut(strout_Queue, qstruct); osThreadTerminate(ThreadRecvHandle); for(;;) { osDelay(1); } |
А если количество системных квантов не достигло ещё 50000, то мы, соответственно, в это тело условия не попадаем и продолжаем нашу программу, в которой отобразим количество системных квантов в терминальной программе, а также отправим их серверу. Затем применим задержку в 1 секунду до следующего цикла
1 2 3 4 5 6 7 |
osDelay(1); } } sprintf(buf,"%lu\r\n",syscnt); HAL_UART_Transmit(&huart1,(uint8_t*)buf,strlen(buf),0x1000); write(sock,(void *) buf,strlen(buf)); osDelay(1000); |
Соберём код, прошьём контроллер, опять запустим netcat и начнём слушать порт.
Перезагрузим контроллер и увидим, что наши пакеты нормально приходят в netcat
Также пакеты наши видны и в терминальной программе
И также в программе анализатора трафика мы видим, что по достижении максимального количества квантов у нас соединение благополучно разорвалось
Также информацию о том, что соединение разорвано, мы видим на нашем дисплее
Попробуем во время передачи данных попытаемся что-то передать из программы netcat нашему клиенту
Клиент принял пакет и отобразил на дисплее результат в виде строки
Мы убедились в том, что наш клиент полностью работоспособен.
Всем спасибо за внимание!
Предыдущий урок Программирование МК STM32 Следующий урок
Отладочную плату можно приобрести здесь 32F746G-DISCOVERY
Смотреть ВИДЕОУРОК (нажмите на картинку)
Добавить комментарий