разработка и программирование электронных устройств

Пример использования BSD Socket API в стеке LwIP

Сокеты Беркли (BSD Socket API) впервые появились как интерфейс прикладного программирования в операционной системе BSD Unix. Сокеты являются интерфейсом межпроцессного взаимодействия (IPC) в ОС Unix, они могут использоваться для передачи данных между отдельными процессами как на одном компьютере, так и на разных компьютерах через компьютерную сеть.

Интерфейс BSD Socket API стал стандартом, поэтому все, что вы прочитаете о сокетах Беркли в контексте ОС Unix можно использовать и при изучении этого интерфейса для стека LwIP. Очень хорошо интерфейс сокетов описан в книге Андрея Робачевского «Операционная система Unix» (третья книга в списке литературы).
В этой книге также приведены примеры программ сервера и клиента, использующих Socket API с протоколом TCP на транспортном уровне. Этими примерами я воспользуюсь для демонстрации работы со стеком LwIP через интерфейс сокетов. Для компиляции под NUCLEO_F429ZI их придется немного переделать, но логика работы примеров от этого не изменится.

Я буду связывать плату NUCLEO_F429ZI с Linux -машиной (это может быть ПК, плата Raspberry PI или другая) для демонстрации одинаковой работы интерфейса BSD Socket API на различных устройствах.
Но сначала скомпилируем и запустим оба примера (сервер и клиент) на одной Linux -машине.
Исходный код сервера в файле tcp_server.c :

Исходный код клиента находится в файле tcp_client.c :

Я собирал и запускал эти примеры на своем компьютере с ОС Fedora Linux 28 и на плате Raspberry Pi3 под управлением ОС Raspbian (Debian Linux).
Для компиляции необходимо выполнить команды :

Далее в терминале Linux запускаем сначала сервер :

Потом клиента :

Для клиента в качестве параметра нужно указать IP-адрес сервера, к которому будет выполнено подсоединение. Поскольку я запустил обе программы на одной машине, то в качестве адреса можно указать «localhost».

Убедившись, что все работает, можем подробно рассмотреть работу сервера и клиента.


Сервер.

В задачи сервера входит прослушивание порта с заданным номером. Сервер и клиент используют для связи выбранный номер порта, известный обеим сторонам. В нашем примере используется порт номер 1500. В примере сервера под Unix(Linux) при установлении соединения создается дочерний процесс, который отвечает за обмен данными между клиентом и сервером. Родительский процесс (сервер) продолжает прослушивать порт 1500 и при новых запросах от клиентов (их может быть большое количество , но в нашем примере одновременно не более 5) создает новый дочерний процесс для каждого нового клиентского запроса.
В нашем простом примере сервер принимает текстовое сообщение то клиента и отправляет его обратно клиенту без какого-либо анализа.

Каждый коммуникационный узел компьютерной сети однозначно идентифицируется адресом хоста (IP адрес) и номером процесса (номером порта) . Это отображено в структуре sockaddr_in :

  • sin_len – размер структуры sockaddr_in
  • sin_family — коммуникационный домен AF_INET
  • sin_port – номер порта
  • sin_addr – IP адрес
  • sin_zero — зарезервировано

Для создания сокета используется функция socket() (в UNIX это системный вызов) , прототип которой показан ниже:

  • domain – коммуникационный домен :
    • AF_INET – протоколы IPv4
    • AF_INET6 — протоколы IPv6
    • AF_UNSPEC – IPv4 или IPv6
  • type – тип сокета :
    • SOCK_DGRAM – сокет датаграмм
    • SOCK_STREAM – сокет потока
    • SOCK_RAW – сокет низкого уровня
  • protocol – протокол зависит от коммуникационного домена и типа сокета. Для AF_INET доступны такие протоколы :
    • IPPROTO_IP
    • IPPROTO_ICMP
    • IPPROTO_TCP
    • IPPROTO_ICMP
    • IPPROTO_UDP
    • IPPROTO_UDPLITE
    • IPPROTO_RAW

    Протокол может не указываться, в этом случае должно быть передано значение 0.

В случае успеха socket() возвращает дескриптор сокета. Для стека LwIP дескриптор может принимать значение от 0 до NUM_SOCKETS — 1.
В случае неудачи будет возвращено значение -1 .

После создания сокет необходимо связать с определенным IP адресом и портом. Для этого используется функция bind(), прототип которой показан ниже :

  • s – дескриптор (идентификатор) сокета
  • name – указатель на структуру struct sockaddr, которая является общей структурой адреса сокета, частным случаем которой для протоколов интернета является struct sockaddr_in
  • namelen – размер структуры struct sockaddr

Поскольку сокеты — это интерфейс межпроцессного взаимодействия, то они могут использоваться как на одной локальной машине между разными процессами (программными потоками), так и между процессами на разделенных компьютерной сетью хостах. Структура struct sockaddr показана ниже :

  • sa_len – размер структуры
  • sa_family – семейство протоколов(коммуникационный домен). Вот несколько примеров:
    • AF_UNIX, AF_LOCAL – локальные коммуникации в ОС семейства Unix
    • AF_INET – протоколы интернета IPv4
    • AF_IPX — IPX -протоколы Novell
  • sa_data – содержит адрес, формат которого специфичен для каждого домена (мы рассмотрели частный случай структуры sockaddr_in для домена AF_INET)

В случае успеха bind() возвращает 0, при неудаче из функции возвращается -1 .

После вызова listen() сервер будет готов принимать запросы от клиентов.
Прототип listen() показан ниже :

  • s – дескриптор сокета
  • backlog – максимальное количество запросов на установление связи, которые могут ожидать обработки сервером

В случае успеха listen() возвращает 0, при неудаче из функции возвращается -1 .

Обработка запросов на установление связи от клиентов выполняется с помощью функции accept() , прототип которой показан ниже :

  • s – дескриптор сокета
  • addr – указатель на адрес клиента, который установил соединение, возвращается из accept()
  • addrlen – указатель на длину структуры адреса клиента, возвращается из accept()

accept() извлекает первый запрос из очереди и создает новый сокет, аналогичный сокету , дескриптор которого был передан через параметр s . Возвращает дескриптор созданного сокета в случае успеха или -1 при ошибке .

После установления соединения обмен данными производится с помощью двух функций send() и recv(), прототипы которых приведены ниже :

  • s -дескриптор сокета
  • data – указатель на передаваемые данные
  • size – размер передаваемых данных
  • flags – флаги, которые являются общими для send() и recv() и могут принимать следующие значения :
    • MSG_PEEK – просмотреть данные, не удаляя их из системного буфера(при следующем чтении будут получены те же данные)
    • MSG_DONTWAIT -неблокирующий ввод/вывод только для этой операции
    • MSG_MORE -отправитель еще будет передавать данные

В случае ошибки send() вернет код -1, при успешной отправке данных будет возвращено количество отправленных байт.

  • s -дескриптор сокета
  • mem – указатель на буфер, в который будут скопированы принятые данные
  • len -длина читаемых данных
  • flags – флаги, описанные выше

В случае успеха recv() возвращает количество принятых данных, в случае ошибки будет возвращен код -1.

Схема установления соединения и обмена данными между сервером и клиентом изображена на следующем рисунке :


Клиент.

В задачи клиента входит установление соединения и обмен данными с сервером. Клиенту необходимо знать IP- адрес сервера и порт, прослушиванием которого занимается сервер.
В нашем примере клиент устанавливает соединение с сервером, дальше отправляет ему текстовое сообщение и принимает его обратно. Принятое от сервера сообщение будет выведено на экран или через UART в случае использования NUCLEO_F429ZI.

IP адрес клиента и номер порта, с которого клиент будет осуществлять соединения, не имеют значения для сервера. Поэтому клиент не выполняет связывание с помощью bind(), как это делает сервер.
Прослушивание порта для клиента также не нужно. В следствии всего этого инициализация клиента выглядит намного проще,
чем сервера (смотрите диаграмму выше).

Клиент, так же как и сервер, для создания сокета использует функцию socket() . После этого он выполняет попытку установления соединения с сервером с помощью функции connect(), прототип которой приведен ниже :

  • s – дескриптор сокета
  • name – указатель на структуру адреса сервера
  • namelen – размер структуры адреса

В случае ошибки connect() возвращает код ошибки -1, в случае успеха будет возвращен 0.

Обратите внимание, что клиент для обмена данными с сервером при помощи функций send()/recv() использует дескриптор сокета, полученный после вызова функции socket().
Тогда как сервер для обмена с каждым отдельным клиентом использует разные дескрипторы сокетов, полученные после вызова accept() для каждого из клиентов.
В то время как дескриптор сокета , полученный после вызова socket() , используется для установления новых соединений с клиентами.

При настройке стека LwIP для сервера нужно учитывать общее количество сокетов, максимальное количество которых равно MEMP_NUM_NETCONN.
Для нашего примера с пятью соединениями максимальное количество сокетов равно шести, поскольку нужно учитывать сокет, созданный с помощью функции socket() и пять сокетов, создаваемых с помощью accept().

Общее количество структур MEMP_NUM_TCP_PCB также должно быть не менее шести, поскольку на транспортном уровне мы используем протокол TCP.
Общее количество прослушиваемых блоков MEMP_NUM_TCP_PCB_LISTEN в нашем примере может быть равно одному.
Функция close() используется для закрытия сокета , ее прототип показан ниже :

  • s – дескриптор закрываемого сокета

При успешном закрытии функция возвращает 0, в противном случае будет возвращено значение -1.

В примере также используются дополнительные функции , такие как :

Функция htons() выполняет преобразование беззнакового 16-битного числа из локального (Little-Endian) в сетевой (Big-Endian) порядок байт.

Функция gethostbyname() выполняет трансляцию доменного имени хоста в его IP адрес.

Функция bzero() выполняет обнуление буфера заданной длинны.

Функция bcopy() выполняет копирование данных, обратите внимание, что в параметрах функции сначала указывается источник, потом приемник.


NUCLEO_F429ZI.

Мы разобрались в работе TCP- сервера и TCP -клиента под Linux на примерах из книги Андрея Робачевcкого «Операционная система Unix».
Теперь можно продемонстрировать работу этих примеров на плате NUCLEO_F429ZI со стеком LwIP.
Если у вас есть в наличии две такие платы, то вы можете связать их между собой, загрузив в одну из плат прошивку сервера, а в другую — клиента.
У меня данная плата имеется только в одном экземпляре, поэтому я буду поочередно пробовать связать сервер под NUCLEO_F429ZI с клиентом под Linux и клиент под NUCLEO_F429ZI с сервером под Linux.

Для создания прошивок я воспользовался генератором кода STM32CubeMX , файл проекта имеет расширение .ioc.

Ссылки на проекты находятся в конце статьи.

Код сгенерирован для среды разработки SW4STM32, если вы хотите использовать другую IDE, то необходимо ее выбрать в STM32CubeMX и сгенерировать код заново.
Для этого запускаем STM32CubeMX и выбираем меню Project->Settings , дальше выбираем в выпадающем списке Toolchain / Ide желаемую среду разработки и генерируем проект заново.

По-умолчанию STM32CubeMX генерирует прошивку в формате Intel HEX. Чтобы при сборке создавался бинарный файл необходимо заменить одну строчку в настройках SW4STM32:

Логика работы примеров для Unix/Linux сохранена, но все-таки пришлось внести некоторые изменения.
Например для сервера под микроконтроллер STM32F429ZI нет системного вызова fork().
Чтобы создать новый поток , он должен быть объявлен на этапе компиляции, количество новых потоков в примере ограничено пятью.
После заполнения очереди запросов к серверу, запущенные потоки должны закончить свою работу и дескрипторы этих потоков могут быть использованы для создания новых потоков.

Логика работы сервера реализована в файле tcp_socket_server.c .

Для работы клиента используется пользовательская кнопка B1 на плате NUCLEO_F429ZI, при нажатии на которую отправляется сигнал (сущность CMSIS_RTOS) в задачу клиента.
Задача клиента ожидает поступления сигнала от пользовательской кнопки B1 и когда тот поступает выполняется отправка запроса к серверу.
Клиент устанавливает соединение с сервером, отправляет ему текстовую строку , затем принимает строку обратно и выводит ее через USART3 в терминал.

Логика работы клиента реализована в файле tcp_socket_client.c .

Хочу отдельно остановиться на настройке LwIP через графическое меню утилиты STM32CubeMX. По сути те же действия можно выполнить записью значений макросов в файле lwipopt.h .
На вкладке Configuration в STM32CubeMX необходимо нажать на кнопку LWIP, после чего станет доступно окно настройки стека LWIP Configuration.

Вкладка General Settings содержит основные настройки LwIP. Здесь нужно деактивировать DHCP и назначить значение IP адреса вручную в зависимости от допустимых адресов в вашей локальной сети.
Количество соединений по TCP должно быть не менее шести (MEMP_NUM_TCP_PCB = 6).

На вкладке Debug можно включить/выключить вывод отладочной информации от различных подсистем LwIP. Я включил вывод отладочной информации на уровне сетевого интерфейса (NETIF_DEBUG) и интерфейса сокетов (SOCKETS_DEBUG).
Общее разрешение вывода отладочной информации LWIP_DEBUG пришлось назначить вручную в файле lwipopt.h , поскольку этот макрос не был установлен поcле сохранения конфигурации и генерации кода в STM32CubeMX.
Заслуживает также внимания вкладка Key Options , хотя на этой вкладке я оставил все без изменений.

Благодаря включенным опциям вывода отладочных сообщений при подключении терминала к USART3 можно увидить сообщения LwIP, что может существенно облегчить отладку программы.
Лог работы сервера под NUCLEO_F429ZI изображен на следующей картинке :

Лог работы клиента под NUCLEO_F429ZI изображен на этой картинке :

Эти примеры достаточно простые, но тем не менее могут послужить основой для реализации реального обмена данными между сервером и клиентом.
Вы можете самостоятельно реализовать TCP – сервер под линукс (обычно это фоновый процесс, так называемый «демон»), который будет прослушивать заданный порт и обмениваться данными с клиентами, работающими через стек LwIP.
На прикладном уровне можно реализовать собственный протокол обмена данными или взять за основу стандартизированный, например HTTP.

HTTP протокол является текстовым протоколом, на нем можно реализовать встроенный HTTP -клиент поверх Socket API LwIP , который с помощью простых POST и GET запросов сможет загружать и скачивать файлы с стандартного HTTP- сервера (Apache, lighttpd и т.д.) .

Также можно реализовать HTTP – сервер на утсройстве для его настройки, обновления прошивки и других целей. Пример такого сервера можно увидеть в любом роутере, имеющем WEB – интерфейс для администрирования.
Для создания защищенного соединения с помощью HTTPS протокола для микроконтроллеров STM32 в утилите STM32CubeMX есть возможность добавить библиотеку mbed TLS .

В следующей статье я планирую уделить внимание созданию HTTPS– клиента с использованием библиотеки mbed TLS и загрузить в NUCLEO_F429ZI какой-нибудь файл с WEB — сервера, запущенного на Raspberry Pi 3.

Ссылки на исходники:

tcp_server.c
tcp_client.c
nucleo_f429zi_tcp_socket_server
nucleo_f429zi_tcp_socket_client

Viewed 5927 times by 973 viewers

Leave a Reply

You must be logged in to post a comment.