Переходим к следующему, более серьёзному типу соединения транспортного уровня — TCP (Transmission Control Protocol, протокол управления передачей). С данным протоколом мы уже встречались неоднократно. Поэтому, хоть он и является непростым, мы его изучили очень неплохо. Мы знаем, как именно происходит создание и разрыв соединения, знаем, как передаются пакеты TCP, как они делятся на сегменты. Поэтому работать нам с ним и анализировать определённые создающиеся нестандартные ситуации мы уже вполне можем. Изучая стек протоколов LWIP, мы также сталкивались с протоколом TCP, даже соединяли с помощью него два контроллера, организовывали между ними передачу данных. Но всё это происходило с использованием интерфейса RAW. Теперь настала пора поработать с данным протоколом при помощи интерфейса NETCOON, который, как известно, работает с использованием системы реального времени FREERTOS.
В качестве платы для испытания мы возьмём STM32F746-Discovery, а проект для прототипа мы возьмём из урока 120 с именем LAN8742_UDP_SERVER_NETCONN. Присвоим ему теперь соответствующее имя LAN8742_TCP_SERVER_NETCONN и откроем его в Cube MX.
Немного подправим настройки LWIP в разделе Key Options, чтобы они соответствовали настройкам клиентов в будущих уроках
Сгенерируем проект для System Workbench и откроем его там. Установим уровень оптимизации в 1, уберём при наличии отладочные настройки и закомментируем неизвестные компилятору строки в файле main.c.
Попробуем собрать проект и начнём работать с файлом main.c.
Первым делом исправим шапку в main()
TFT_DisplayString(0, 10, (uint8_t *)"TCP Server", CENTER_MODE);
Наш проект будет поддерживать два соединения (или, как принято называть, сокета). Отличие протокола TCP от UDP помимо всего прочего состоит в том, что мы можем работать с двумя соединениями на одном порте сервера. Поэтому в структуре для сокета мы уберём свойство порта, но добавим свойство соединения TCP
typedef struct struct_sock_t {
uint16_t y_pos;
uint16_t port;
struct netconn *conn;
} struct_sock;
Исправим имя функции задачи соединения, удалив из неё почти весь код, за исключением лишь нескольких объявлений
static void tcp_thread(void *arg)
{
struct_out *qstruct;
err_t err, recv_err;
struct netconn *conn;
}
Теперь переходим в задачу по умолчанию StartDefaultTask и также удалим из неё почти весь пользовательский код, оставив лишь инициализацию структур в плане вертикальной координаты вывода информации на экран дисплея
/* USER CODE BEGIN 5 */
sock01.y_pos = 60;
sock02.y_pos = 180;
/* Infinite loop */
Объявим здесь же указатель на переменную структуры соединения и переменную для ошибки
/* USER CODE BEGIN 5 */
struct netconn *conn;
err_t err;
Создадим соединение
sock02.y_pos = 180;
conn = netconn_new(NETCONN_TCP);
Если всё нормально создалось, то передадим указатель на данную структуру обоим соединениям
conn = netconn_new(NETCONN_TCP);
if(conn!=NULL)
{
sock01.conn = conn;
sock02.conn = conn;
}
Свяжем структуру, например с портом 80, что пригодится нам на будущее для создания сервера HTTP
sock02.conn = conn;
err = netconn_bind(conn, NULL, 80);
Если ошибок нет, то начинаем слушать порт, а если нет — удаляем структуру
err = netconn_bind(conn, NULL, 80);
if (err == ERR_OK)
{
netconn_listen(conn);
}
else
{
netconn_delete(conn);
}
Запустим анализатор трафика Wireshark и отфильтруем там поток по IP-адресу нашего сервера
Теперь мы можем проверить создание соединения.
Соберём код, прошьём контроллер и запустим программу сетевого обмена Putty, в которой выберем созданное ранее нами соединение. Если у кого нет, то создаём подобное, на скриншоте всё есть. Запустим соединение с помощью кнопки Open
Мы видим, что соединение у нас успешно установилось, трёхкратное рукопожатие произошло (нажмите на картинку для увеличения изображения)
Закроем окно соединения в Putty, соединение разорвалось, но только в одностороннем порядке. Сервер не изъявил желание на разрыв, а мы помним, что всё здесь должно быть обоюдно (нажмите на картинку для увеличения изображения)
Желание сервера на разрыв соединения мы инициируем позже.
Также в нашем соединении мы пока не можем передавать пакеты, мы это ещё не настроили, всё это будет происходить в задачах соединений, которые мы сейчас и создадим
netconn_listen(conn);
sys_thread_new("tcp_thread1", tcp_thread, (void*)&sock01, DEFAULT_THREAD_STACKSIZE, osPriorityNormal );
sys_thread_new("tcp_thread2", tcp_thread, (void*)&sock02, DEFAULT_THREAD_STACKSIZE, osPriorityNormal );
Также здесь мы передали указатель на структуру соединения.
Перейдём теперь функцию для данных задач tcp_thread и создадим там структуру для приёмного буфера и ещё одну структуру соединения, предназначенную чисто для обмена данными. Такая структура будет у каждого соединения из двух
struct netconn *conn;
struct netbuf *inbuf;
struct netconn *newconn;
Поймаем переданный с помощью параметра задаче указатель на структуру соединения и передадим его локальному указателю
struct netconn *newconn;
struct_sock *arg_sock;
arg_sock = (struct_sock*) arg;
conn = arg_sock->conn;
Создадим переменную для хранения длины буфера, а также указатель на обычный строковый буфер
conn = arg_sock->conn;
u16_t buflen;
char* buf;
Изменим цвет вывода информации на дисплей
char* buf;
TFT_SetTextColor(LCD_COLOR_BLUE);
Организуем бесконечный цикл, в котором свяжем структуру сокета для передачи данных с общей структурой TCP
TFT_SetTextColor(LCD_COLOR_BLUE);
for(;;)
{
err = netconn_accept(conn, &newconn);
}
Затем в этом цикле проверим успешность нашей операции. В случае успеха мы попадаем ещё в один бесконечный цикл, в котором попытаемся принять пакет и записать его в буфер, а в случае неудачи перейдём в другой бесконечный цикл с задержкой, чтобы не дать из-за этого подвиснуть остальным процессам нашей программы
err = netconn_accept(conn, &newconn);
if (err == ERR_OK)
{
for(;;)
{
recv_err = netconn_recv(newconn, &inbuf);
}
}
else
{
osDelay(1);
}
Причём, когда мы ждём пакета, мы не крутимся в данном бесконечном цикле, мы «висим» в ожидании пакета и провалимся в низ только в случае его получения. Мы, конечно, не висим, в это время выполняется какой-то определённый процесс библиотеки, который даёт работать другим процессам, так как, когда мы ждём пакета, другие соединения у нас в это время должны тоже свободно передавать и принимать данные.
Теперь, когда мы приняли пакет и провалились вниз, мы проверим успешность приёма пакета и в случае успеха заберём данные из пакета в наш строчный буфер с помощью специальной функции, а в случае неудачи мы освободим память структуры приёмного буфера и разорвём соединение с клиентом, а также выйдем из цикла.
recv_err = netconn_recv(newconn, &inbuf);
if (recv_err == ERR_OK)
{
netbuf_data(inbuf, (void**)&buf, &buflen);
}
else
{
netbuf_delete(inbuf);
netconn_close(newconn);
break;
}
Вот теперь у нас разъединение с клиентом должно будет происходить обоюдно.
Проверим это, собрав код, прошив контроллер и запустив соединение с помощью программы Putty, а затем разорвав его с помощью закрытия окна соединения данной программы.
Мы должны теперь получить вот такой результат (нажмите на картинку для увеличения изображения)
Проверим концовку буфера на перевод строки и возврат каретки. Если таковой имеется, то освободим память структуры буфера и пропустим одну итерацию в цикле с помощью оператора continue
netbuf_data(inbuf, (void**)&buf, &buflen);
if((buf[0]==0x0D)||(buf[0]==0x0A))
{
netbuf_delete(inbuf);
continue;
}
А если это ещё не окончание строки, то продолжаем наш цикл.
Мы отправим содержимое буфера на дисплей с помощью очереди, обнулив определённый байт в строчном буфере, чтобы создать окончание строки
continue;
}
qstruct = osMailAlloc(strout_Queue, osWaitForever);
qstruct->y_pos = arg_sock->y_pos;
strncpy(str_buf,buf,buflen);
str_buf[buflen]=0;
sprintf(qstruct->str,"%-20s", str_buf);
osMailPut(strout_Queue, qstruct);
osMailFree(strout_Queue, qstruct);
Теперь мы в нашем строчном буфере занесём коды перевода строки и возврата каретки и отправим данный буфер назад клиенту, освободим затем память структуры буфера
osMailFree(strout_Queue, qstruct);
str_buf[buflen] = '\r';
str_buf[buflen+1] = '\n';
netconn_write(newconn, str_buf, buflen+2, NETCONN_COPY);
netbuf_delete(inbuf);
Собственно, вот весь и код.
Соберём его, прошьём контроллер, запустим программу Putty, откроем порт и попробуем что-нибудь передать нашему серверу
Мы видим, что строка от сервера нам успешно вернулась.
Также мы видим, что строка пришла и на дисплей
Попробуем передать ещё несколько строк
Создадим ещё одно соединение с клиента, запустив ещё одну сессию Putty, и проверим прохождение строк также и оттуда
Данные также успешно передались и вернулись.
Также они пришли в виде строки на дисплей в другую, более нижнюю позицию
Попробуем и здесь передать ещё несколько строк
Данные также успешно передаются и возвращаются куда надо, не путаясь между собой.
Также мы видим, что в программе анализа трафика пакеты тоже проходят нормально (нажмите на картинку для увеличения изображения)
Закроем обе сессии и посмотрим корректность разрыва соединения (нажмите на картинку для увеличения изображения)
Итак, на данном занятии мы создали несложный, но в то же время способный корректно работать сервер TCP, с которым корректно соединяются и разъединяются клиенты, а также происходит обмен пакетами между узлами.
Всем спасибо за внимание!
Предыдущий урок Программирование МК STM32 Следующий урок
Отладочную плату можно приобрести здесь 32F746G-DISCOVERY
Смотреть ВИДЕОУРОК (нажмите на картинку)
Здравствуйте, можете помочь?
Я реализовал TCP server и он работал, но в какой-то момент контроллер стал зависать при попытке подключения ПК к контроллеру. Даже не знаю какие ещё входные данные предоставить. Базовые настройки размеров буферов в cubeMX для LWIP не менял.
Как я понял зависание происходит в netconn_accept(conn, &newconn)
Кусок кода:
…
err = netconn_listen(conn);
if (err == ERR_OK)
{
for (;;)
{
accept_err = netconn_accept(conn, &newconn);
if (accept_err == ERR_OK)
{
…
Работаю на F7, поборол эту проблему. Необходимо вытащить строку
accept_err = netconn_accept(conn, &newconn);
из вечного цикла, достаточно запустить эту функцию один раз.
Хочу указать на небольшую неточность в примере, которая мне, как начинающему с lwip, доставила немало хлопот.
Функция netconn_close(newconn) разрывает соединение, но не удаляет его структуру. При разрыве соединения и новом подключении снова вызывается функция netconn_accept, которая создает новую структуру и их общее количество в стеке увеличивается на единицу. Количество структур ограничено значением MEMP_NUM_NETBUF. При достижении этого числа, структуры перестают создаваться, а следовательно, клиенту отказывается в подключении.
Проблема решается добавлением команды netconn_delete(newconn) после netconn_close(newconn).
Возможно, эта проблема имеет те же корни что и проблема предыдущего пользователя. Не знаю только, чьё решение более верное.
Здравствуйте. Как можно сделать на данной библиотеке LWIP. NETCONN. одновременно сервер и клиент?
Здравствуйте!
Одновременно в одной сети скорей всего никак. Только по очереди. А это переинициализация. Да и вряд ли это нужно для чего-нибудь. Особенно в одной сети. Чтобы перейти в режим клиента, надо убедиться, что никто соединиться не попытается, а это значит, что всем надо об этом сообщить. Думаю, что переобуться в прыжке легче.
Please, give me an advice. The example worked until I ended the connection on the client's side. Then it was no longer possible to connect.
Oh, ok. That is it, when I don't read forum posts. Resolved see. above. Add the command netconn_delete (newconn) before netconn_close (newconn).
not before, after is correct