ESP32 TCP/IP

Материал из razgovorov.ru
Перейти к: навигация, поиск

TCP / IP - это сетевой протокол, который используется в Интернете. Это протокол, который ESP32 изначально понимает и использует с Wi-Fi в качестве транспорта. Книги по книгам уже были написаны о TCP / IP, и наша цель - не пытаться воспроизвести подробное обсуждение того, как это работает, однако есть некоторые концепции, которые мы будем пытаться и захватывать.

Во-первых, есть IP-адрес. Это 32-битное значение и должно быть уникальным для каждого устройства, подключенного к Интернету. 32-битное значение можно рассматривать как четыре различных 8-битных значения (4 x 8 = 32). Поскольку мы можем представить 8-битное число как десятичное значение от 0 до 255, мы обычно представляем IP-адреса с обозначением <число>. <Число>. <Число>. <Число> например, 173.194.64.102. Эти IP-адреса обычно не используются в приложениях. Вместо этого набирается текстовое имя такое как «google.com» ... но не вводите их в заблуждение, эти имена являются иллюзией на уровне TCP/IP. Все работы выполняются с 32-битными IP-адресами. Существует система отображения, которая берет имя (например, «google.com») и получает соответствующий IP-адрес. Технология, которая делает это, называется «системой доменных имен» или DNS.

Когда мы думаем о TCP/IP, на самом деле есть три разных протокола. Первый - IP (Интернет-протокол). Это базовый протокол передачи дейтаграмм транспортного уровня. Над уровнем IP находится TCP (протокол управления передачей), который обеспечивает иллюзию соединения по IP-протоколу без установления соединения. Наконец, есть UDP (User Datagram Protocol). Это также живет выше IP-протокола и обеспечивает передачу дейтаграмм (без установления соединения) между приложениями. Когда мы говорим о TCP/IP, мы говорим не только о TCP, работающем поверх IP, но и фактически используем это как сокращение для основных протоколов, которые являются IP, TCP и UDP, а также дополнительные связанные протоколы уровня приложений, такие как DNS, HTTP, FTP , Telnet и многое другое.

The Lightweight IP Stack – lwip

Если мы будем рассматривать TCP/IP в качестве протокола, мы можем разбить наше понимание сетей на два разных уровня. Один из них - это аппаратный уровень, который отвечает за получение потока 1 и 0 из одного места в другое. Общие реализации для этого включают Ethernet, Token Ring и (да ... Я сейчас встречаюсь с модемами). Они характеризуются физическими проводами от ваших устройств. Wi-Fi сам по себе является транспортным уровнем. Он имеет дело с использованием радиоволн в качестве среды связи 1 и 0 между двумя точками. Спецификация WiFI - IEEE 802.11.

Как только мы сможем передавать и принимать данные, следующий уровень организует данные по этой физической сети, и именно здесь вступает в действие TCP/IP. Он обеспечивает правила и управление передачей данных, адресацией, маршрутизацией, протокольными переговорами и т.д.

Как правило, TCP/IP реализуется в программном обеспечении по основному механизму физического транспорта. Подумайте об этом мгновение. Представьте, что я сказал вам, что у меня есть «волшебная коробка», и если вы поместите что-то в эту коробку, она будет волшебным образом перенесена в другую коробку. Это аналогия физического транспорта. Программное обеспечение, которое является TCP / IP, добавляет механизмы выше этого. Например, представьте, что коробка имеет ширину всего 6 дюймов. Если вы хотите отправить мне что-то через наши ящики, вы должны расколоть его и отправить его на куски. Ваш конец истории коробки обрабатывает это. Моя коробка получит детали и соберите их для меня. Части могут прибыть в порядок, а некоторые части могут даже потеряться на маршруте и должны быть отправлены повторно с оригиналов. Аппаратное обеспечение (коробки) не имеет понятия как добиться этого. Все, что они знают, это часть данных в одном конце, мы надеемся, прибудем к другому ... но не гарантируем.

TCP/IP - большой протокол. Он содержит множество частей. К счастью, это хорошо указано и было реализовано многими поставщиками за последние 45 лет. Некоторые из реализаций всего пакета частей TCP/IP были написаны как с открытым исходным кодом и распространяются и поддерживаются сообществом. Это означает, что если у вас есть новый аппаратный уровень, можно (в принципе) поднять уже написанную реализацию TCP/IP, сопоставить его с вашим оборудованием, скомпилировать его для своей среды, и все будет работать. На самом деле это гораздо легче сказать, чем сделать ... и, к счастью для нас, наши друзья в Espressif сделали для нас работу.

Одна такая реализация с открытым исходным кодом стека TCP/IP называется «The LightweightIPStack ", который обычно называют" lwIP ", который можно подробно прочитать на домашней странице (см. Ссылки). В рамках распространения ESP-IDF у нас есть библиотеки, которые предоставляют реализацию lwIP. Это lwIP, который предоставляет ESP32 следующие услуги:

  • IP
  • ICMP
  • IGMP
  • MLD
  • ND
  • UDP
  • TCP
  • sockets API
  • DNS

Опять же, хорошая новость заключается в том, что подавляющее большинство lwIP не имеет для нас значения, дизайнеров и разработчиков приложений ESP32. Это жизненно важно ... но важно для внутренней работы ESP32 и не подвергается нам как потребителям.

See also:

  • lwIP 2.0.0

TCP

TCP-соединение - это двунаправленная труба, через которую данные могут поступать в обоих направлениях. До того, как соединение установлено, одна сторона действует как сервер. Это пассивно прослушивание входящих запросов на соединение. Он будет просто сидеть там столько, сколько потребуется, пока не поступит запрос на соединение. Другая сторона соединения отвечает за инициирование соединения и активно просит создать соединение. Как только соединение будет построено, обе стороны могут отправлять и получать данные. Чтобы «клиент» запросил соединение, он должен знать адресную информацию, по которой сервер слушает. Этот адрес состоит из двух отдельных частей. Первая часть - это IP-адрес сервера, а вторая часть - «номер порта» для конкретного слушателя. Если мы подумаем о ПК, у вас может быть много приложений, работающих на нем, каждый из которых может получать входящее соединение. Просто знание IP-адреса вашего ПК недостаточно для обращения к правильному приложению. Комбинация IP-адреса плюс номер порта обеспечивает всю необходимую адресацию.

Как аналог этого, подумайте о своем мобильном телефоне. Он пассивно сидит там, пока кто-то не назовет его. В нашей истории ваш телефон - слушатель. Адрес, который кто-то использует для формирования соединения, - это номер вашего телефона, который состоит из кода области плюс остаток. Например, номер телефона (817) 555-1234 достигнет определенного телефона. Однако код города 817 предназначен для Форт-Уэрта в Техасе ... призывая, что сам по себе недостаточно для того, чтобы связаться с человеком ... требуется полный номер телефона.

Нет, мы не будем рассматривать, как ESP32 может настроить себя как слушателя для входящего соединения TCP / IP, и это требует, чтобы мы начали понимать важные «сокеты» API.

TCP/IP Sockets

API сокетов - это программный интерфейс для работы с сетями TCP / IP. Это, вероятно, самый известный API для сетевого программирования. Программирование сокетов знакомо программистам в Linux, Windows, Java и т.д.

Сетевые потоки TCP/IP поставляются в двух вариантах ... соединение, ориентированное на TCP и дейтаграмму, ориентированную на UDP. API сокетов предоставляет различные шаблоны вызовов для обоих стилей.

Для TCP сервер построен:

  1. . Создание сокета TCP
  2. . Связывание локального порта с сокетом
  3. . Установка режима сокета в режим прослушивания
  4. . Принятие нового соединения с клиентом
  5. . Получение и отправка данных
  6. . Закройте соединение клиент / сервер
  7. . Возвращаясь к шагу 4

Для клиента TCP мы создаем:

  1. . Создание сокета TCP
  2. . Подключение к серверу TCP
  3. . Отправка данных / прием данных
  4. . Закройте соединение

Теперь давайте разложим их на фрагменты кода, которые мы можем проанализировать более подробно. Определения заголовков для API сокетов можно найти в <lwip / sockets.h>.

Для клиентских и серверных приложений задача создания сокета одинакова.

Это вызов API функции socket ().

int sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)

Возврат из socket () - это целочисленный дескриптор, который используется для обозначения сокета. Сокеты имеют много состояний, связанных с ними, однако это состояние является внутренним для реализации TCP/IP и сокетов, и его не нужно подвергать сетевому программисту. Таким образом, нет необходимости раскрывать эти данные программисту. Мы можем думать о вызове socket (), запрашивая время выполнения для создания и инициализации всех данных необходимых для сетевого общения. Эти данные принадлежат времени выполнения, и нам передается «ссылочный номер» или дескриптор, который действует как прокси-сервер для данных. Когда мы захотим впоследствии выполнить работу над этим сетевым подключением, мы перейдем к тому дескриптору, который был ранее выпущен нам, и мы можем связать его с соединением. Это изолирует и изолирует программиста от кишок реализации TCP/IP и оставляет нам полезную абстракцию.

Когда мы создаем серверный сокет, мы хотим, чтобы он прослушивал входящие запросы на соединение. Для этого нам нужно указать сокет, номер порта TCP/IP которого он должен прослушивать. Обратите внимание, что мы не предоставляем номер порта a непосредственно из значения int / short. Вместо этого мы возвращаем значение, возвращаемое функцией htons(). Эта функция выполняет преобразование числа в так называемый «порядок сетевого байта». Это порядок байтов, который был выбран по соглашению, который используется для передачи двоичных двоичных данных без знака через Интернет. Фактический формат - это «большой конец», что означает, что если мы возьмем число, такое как 9876 (десятичное), то оно представлено в двоичном виде как 00100110 10010100 или 0x26D4 в шестнадцатеричном формате. Для порядка сетевых байтов мы сначала передаем 00100110 (0x26), а затем 10010100 (0xD4). Важно понимать, что ESP32 представляет собой небольшую внутреннюю архитектуру, которая означает, что мы должны обязательно преобразовать 2 байта и 4 байта в сетевой порядок байтов (большой конец).

Приложение на ESP32 в один момент может использовать только один порт. Если мы хотим связать номер порта с приложением, например, наше серверное приложение, в этом случае мы выполняем задачу с именем «привязка», которая связывает (или присваивает) номер порта сокету, который, в свою очередь, принадлежит приложению.

struct sockaddr_in serverAddress;
serverAddress.sin_family = AF_INET;
serverAddress.sin_addr.s_addr = htonl(INADDR_ANY);
serverAddress.sin_port = htons(portNumber);
bind(sock, (struct sockaddr *)&serverAddress, sizeof(serverAddress));

Теперь, когда сокет связан с номером локального порта, мы можем запросить, чтобы среда выполнения начала прослушивать входящие соединения. Мы делаем это, вызывая API listen (). Перед вызовом listen() соединения с клиентами были бы отклонены с указанием клиенту, что на соответствующем целевом адресе ничего не было. Как только мы вызываем listen (), сервер начнет принимать входящие клиентские соединения. API выглядит так:

listen(sock, backlog)

Залогом является количество запросов на соединение, которые будут выполняться во время выполнения и принимаются до их передачи в приложение для обработки. Способ думать об этом - это представить, что вы являетесь приложением, и вы можете делать только одно за раз. Например, вы можете разговаривать только с одним человеком за раз по телефону. Теперь представьте, что у вас есть секретарь, который обрабатывает ваши входящие звонки. Когда звонок прибывает, и вы не заняты, секретарь передает вам звонок. Теперь представьте, что вы заняты. В это время секретарь отвечает на звонок и просит абонента подождать. Когда вы освобождаетесь, она передает вам ожидающий звонок. Теперь предположим, что вы все еще заняты, когда звонит еще один клиент. Она также призывает этого звонящего подождать. Мы начинаем строить очередь абонентов. И именно здесь начинается игра с отставанием. Заготовка указывает время выполнения, сколько звонков может быть получено, и попросили подождать. Если поступит больше вызовов, чем позволяет наш backlog, время выполнения немедленно отклонит вызов. Это не только предотвращает потребление ресурсов при запуске на сервера, а также может использоваться в качестве индикатора для вызывающего абонента, что его лучше обслуживать в другом месте.

Теперь с точки зрения сервера мы готовы сделать некоторые работы. Серверное приложение теперь может блокировать ожидания входящих клиентских подключений. Мысль о том, что цель сервера в жизни - обрабатывать клиентские запросы, а когда нет активного запроса клиента, нет ничего, что можно было бы сделать, кроме как ждать запроса. Хотя это, безусловно, одна модель, это не обязательно единственная модель или даже лучшая модель (во всех случаях). Обычно нам нравятся наши процессоры для «использования». Используемый означает, что, хотя он имеет продуктивную работу, он может сделать, тогда он должен это сделать. Если единственное, что может сделать наша программа, это вызов клиентских услуг, то исходная модель имеет смысл. Тем не менее, есть определенные программы, которые, если у них нет клиентского запроса на немедленную службу, могут потратить время на то, что полезно. Мы вернемся к этому понятию позже. На данный момент мы рассмотрим вызов функции accept(). Когда вызывается accept(), произойдет одна из двух вещей. Если соединение с клиентом не будет немедленно ждать нас, мы будем блокировать до такого времени в будущем, когда произойдет соединение с клиентом. В это время мы проснемся и получим связь с недавно прибывшим клиентом. Если, с другой стороны, мы вызываем accept(), и нас ждет клиентское соединение, мы немедленно получим это соединение, и мы продолжим. В обоих случаях мы вызываем accept() и возвращаем соединение с клиентом. Различие между случаями заключается в том, нужно ли нам ждать прибытия соединения. Вызов API выглядит так:

struct sockaddr_in clientAddress;
socklen_t clientAddressLength = sizeof(clientAddress);
int clientSock = accept(sock, (struct sockaddr *)&clientAddress, &clientAddressLength);

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

Если мы хотим создать сокет-клиент, история похожа. Снова мы создаем socket(), но на этот раз нет необходимости в истории bind() / listen() / accept(). Вместо этого мы используем API connect() для подключения к целевой конечной точке TCP/IP.

Например:

struct sockaddr_in serverAddress;
serverAddress.sin_family = AF_INET;
inet_pton(AF_INET, "192.168.1.200", &serverAddress.sin_addr.s_addr);
serverAddress.sin_port = htons(9999);
int rc = connect(sock, (struct sockaddr *)&serverAddress, sizeof(struct sockaddr_in));

See also:

  • Native byte order, endian and network byte order
  • socket
  • bind
  • listen
  • accept
  • send
  • recv
  • connect
  • Wikipedia – Berkeley Sockets
  • Beej's Guide to Network Programming

Обработка ошибок

Большинство API-интерфейсов сокетов возвращают код возврата int. Если этот код <0, то произошла ошибка.

Характер ошибки можно найти, используя глобальный int, называемый «errno». Однако в среде многозадачности не рекомендуется работать с глобальными переменными. В области сокетов мы можем запросить сокет для последней ошибки, с которой он столкнулся, используя следующий фрагмент кода:

int espx_last_socket_errno(int socket) {
   int ret = 0;
   u32_t optlen = sizeof(ret);
   getsockopt(socket, SOL_SOCKET, SO_ERROR, &ret, &optlen);
   return ret;
}

Значения ошибок можно сравнить с константами. Вот таблица констант, используемых в текущей реализации FreeRTOS:

Символ Значение Описание
EPERM 1 Operation not permitted
ENOENT 2 No such file or directory
ESRCH 3 No such process
EINTR 4 Interrupted system call
EIO 5 I/O error
ENXIO 6 No such device or address
E2BIG 7 Arg list too long
ENOEXEC 8 Exec format error
EBADF 9 Bad file number
ECHILD 10 No child processes
EAGAIN 11 Try again
ENOMEM 12 Out of memory
EACCES 13 Permission denied
EFAULT 14 Bad address
ENOTBLK 15 Block device required
EBUSY 16 Device or resource busy
EEXIST 17 File exists
EXDEV 18 Cross-device link
ENODEV 19 No such device
ENOTDIR 20 Not a directory
EISDIR 21 Is a directory
EINVAL 22 Invalid argument
ENFILE 23 File table overflow
EMFILE 24 Too many open files
ENOTTY 25 Not a typewriter
ETXTBSY 26 Text file busy
EFBIG 27 File too large
ENOSPC 28 No space left on device
ESPIPE 29 Illegal seek
EROFS 30 Read-only file system
EMLINK 31 Too many links
EPIPE 32 Broken pipe
EDOM 33 Math argument out of domain of func
ERANGE 34 Math result not representable
EDEADLK 35 Resource deadlock would occur
ENAMETOOLONG 36 File name too long
ENOLCK 37 No record locks available
ENOSYS 38 Function not implemented
ENOTEMPTY 39 Directory not empty
ELOOP 40 Too many symbolic links encountered
EWOULDBLOCK EAGAIN 41 Operation would block
ENOMSG 42 No message of desired type
EIDRM 43 Identifier removed
ECHRNG 44 Channel number out of range
EL2NSYNC 45 Level 2 not synchronized
EL3HLT 46 Level 3 halted
EL3RST 47 Level 3 reset
ELNRNG 48 Link number out of range
EUNATCH 49 Protocol driver not attached
ENOCSI 50 No CSI structure available
EL2HLT 51 Level 2 halted
EBADE 52 Invalid exchange
EBADR 53 Invalid request descriptor
EXFULL 54 Exchange full
ENOANO 55 No anode
EBADRQC 56 Invalid request code
EBADSLT 57 Invalid slot
EBFONT 59 Bad font file format
ENOSTR 60 Device not a stream
ENODATA 61 No data available
ETIME 62 Timer expired
ENOSR 63 Out of streams resources
ENONET 64 Machine is not on the network
ENOPKG 65 Package not installed
EREMOTE 66 Object is remote
ENOLINK 67 Link has been severed
EADV 68 Advertise error
ESRMNT 69 Srmount error
ECOMM 70 Communication error on send
EPROTO 71 Protocol error
EMULTIHOP 72 Multihop attempted
EDOTDOT 73 RFS specific error
EBADMSG 74 Not a data message
EOVERFLOW 75 Value too large for defined data type
ENOTUNIQ 76 Name not unique on network
EBADFD 77 File descriptor in bad state
EREMCHG 78 Remote address changed
ELIBACC 79 Can not access a needed shared library
ELIBBAD 80 Accessing a corrupted shared library
ELIBSCN 81 .lib section in a.out corrupted
ELIBMAX 82 Attempting to link in too many shared libraries
ELIBEXEC 83 Cannot exec a shared library directly
EILSEQ 84 Illegal byte sequence
ERESTART 85 Interrupted system call should be restarted
ESTRPIPE 86 Streams pipe error
EUSERS 87 Too many users
ENOTSOCK 88 Socket operation on non-socket
EDESTADDRREQ 89 Destination address required
EMSGSIZE 90 Message too long
EPROTOTYPE 91 Protocol wrong type for socket
ENOPROTOOPT 92 Protocol not available
EPROTONOSUPPORT 93 Protocol not supported
ESOCKTNOSUPPORT 94 Socket type not supported
EOPNOTSUPP 95 Operation not supported on transport endpoint
EPFNOSUPPORT 96 Protocol family not supported
EAFNOSUPPORT 97 Address family not supported by protocol
EADDRINUSE 98 Address already in use
EADDRNOTAVAIL 99 Cannot assign requested address
ENETDOWN 100 Network is down
ENETUNREACH 101 Network is unreachable
ENETRESET 102 Network dropped connection because of reset
ECONNABORTED 103 Software caused connection abort
ECONNRESET 104 Connection reset by peer
ENOBUFS 105 No buffer space available
EISCONN 106 Transport endpoint is already connected
ENOTCONN 107 Transport endpoint is not connected
ESHUTDOWN 108 Cannot send after transport endpoint shutdown
ETOOMANYREFS 109 Too many references: cannot splice
ETIMEDOUT 110 Connection timed out
ECONNREFUSED 111 Connection refused
EHOSTDOWN 112 Host is down
EHOSTUNREACH 113 No route to host
EALREADY 114 Operation already in progress
EINPROGRESS 115 Operation now in progress
ESTALE 116 Stale NFS file handle
EUCLEAN 117 Structure needs cleaning
ENOTNAM 118 Not a XENIX named type file
ENAVAIL 119 No XENIX semaphores available
EISNAM 120 Is a named type file
EREMOTEIO 121 Remote I/O error
EDQUOT 122 Quota exceeded
ENOMEDIUM 123 No medium found
EMEDIUMTYPE 124 Wrong medium type

Configuration settings

Внутри «menuconfig» есть некоторые параметры, относящиеся к TCP / IP, и их можно найти в настройках lwIP. Настройки:

  • Максимальное количество открытых сокетов - целое число - CONFIG_LWIP_MAX_SOCKETS - Это количество одновременно открытых сокетов. Значение по умолчанию - 4, а максимальное значение - 16.
  • Включить SO_REUSEADDR - boolean - LWIP_SO_REUSE -

Using select()

Представьте, что у нас есть несколько сокетов, каждый из которых может быть источником входящих данных. Если мы попытаемся и читаем () данные из сокета, мы обычно блокируем, пока данные не будут готовы. Если мы это сделаем, то, если данные станут доступными в другом сокете, мы не узнаем. Альтернативой является попытка считывать данные неблокируемым способом. Это тоже было бы полезно, но потребовало бы, чтобы мы каждый раз тестировали каждый сокет в режиме занятости или опроса. Это оо не является оптимальным. В идеале, что мы хотели бы сделать, это блокировать, одновременно наблюдая за несколькими сокетами и просыпаясь, когда у первого есть что-то полезное для нас.

See also:

  • select
  • The world of select()

Отличия от «стандартных» сокетов

Два файла заголовка, которые обычно встречаются в других реализациях сокетов, не являются частью определения ESP-IDF. Они есть:

  • Netinet / in.h
  • Arpa / inet.h

Несмотря на отсутствие присутствия, никаких очевидных проблем не обнаружено, и предполагается, что содержание, обычно содержащееся внутри, было распределено по другим заголовкам.

UDP/IP Sockets

Если мы думаем о TCP как о формировании связи между двумя сторонами, подобными телефонному звонку, то UDP походит на отправку письма через почтовую систему. Если бы я отправил вам письмо, мне нужно было узнать ваше имя и адрес. Ваш адрес необходим, чтобы письмо могло быть доставлено в правильный дом, пока ваше имя гарантирует, что оно окажется в ваших руках, в отличие от кого-то, кто может жить с вами. В терминах TCP / IP адрес - это IP-адрес, а имя - номер порта.

С помощью телефонного разговора мы можем обмениваться такой же информацией, как нам нравится. Иногда я говорю, иногда вы говорите ... но нет максимального ограничения на то, сколько информации мы можем обменять в одном разговоре. Вместе с письмом есть только так много страниц бумаги, которые будут вписываться в конверты, которые у меня есть.

Понятие почтовой аналогии - это то, как мы можем думать о UDP. Аббревиатура обозначает протокол User Datagram Protocol, и это понятие дейтаграммы, которое сродни букве. Дейтаграмма представляет собой массив байтов, которые передаются от отправителя в приемник как единое целое. Максимальный размер дейтаграммы с использованием UDP - 64 Кбайт. Не нужно устанавливать соединение между двумя сторонами, прежде чем данные начнут течь. Однако есть нижняя сторона. Отправитель данных не будет уведомлен о невозможности получения данных получателем. С TCP у нас есть установление связи между двумя сторонами, что позволяет отправителю знать, что данные были получены, а если нет, может автоматически повторно передавать до тех пор, пока он не будет получен или мы не откажемся. С UDP и точно так же, как письмо, когда мы отправляем дейтаграмму, мы теряем из виду, действительно ли он прибывает благополучно в пункт назначения.

Сейчас самое время вернуться к IP-адресам и номерам портов. Мы должны начать понимать, что на ПК только одно приложение может прослушивать любой порт. Например, если мое приложение прослушивает порт 12345, тогда никакое другое приложение также не может прослушивать тот же порт ... не ваше приложение, а другое копия / экземпляр моего экземпляра.

Когда входящее соединение или датаграмма поступает на машину, оно прибыло, потому что IP-адрес отправленных данных соответствует IP-адресу устройства, на котором он был отправлен.

Затем мы маршрутизируем внутри устройства на основе номеров портов. И здесь я хочу уточнить детали. Мы прокладываем маршрут внутри машины на основе пары как протокола, так и номера порта.

Так, например, если запрос поступает на машину для порта 12345 по TCP-соединению, он маршрутизируется в порт наблюдения 123 приложения TCP. Если запрос поступает на тот же компьютер для порта 12345 через UDP, он направляется в UDP Порт наблюдения приложений 12345. Это означает, что мы можем иметь два приложения, прослушивающих один и тот же порт, но по разным протоколам. Если сделать это более формально, пространство распределения для номеров портов является функцией протокола, и двум приложениям не разрешается одновременно резервировать один и тот же порт в одном и том же пространстве распределения протокола.

Хотя я использовал историю ПК с несколькими приложениями, в нашем ESP32 история похожа, хотя мы просто запускаем одно приложение на устройстве. Если вашему одиночному приложению необходимо прослушивать несколько портов, не пытайтесь использовать один и тот же порт с тем же протоколом, что и второй вызов функции, первый из которых уже назначил порт. Это деталь, которую я рад за вас забыть, потому что вы редко сталкиваетесь с ней, но я хотел поймать ее здесь для полноты.

Чтобы запрограммировать UDP, мы снова используем сокеты. Чтобы снова настроить сервер сокетов с помощью UDP, мы вызываем socket () для создания сокета, и снова мы вызываем bind (), чтобы указать номер порта, который мы хотим прослушать. Нет необходимости в вызове listen (). Когда сервер готов принять входящий запрос, мы вызываем recvfrom (), который блокируется до получения дейтаграммы. Как только человек прибывает, мы просыпаемся и можем обрабатывать запрос.

Запрос содержит обратный адрес, и мы можем отправить ответ, используя sendto (), если мы пожелаем.

На стороне клиента мы создаем сокет (), а затем можем вызывать sendto (). Вызов sendto () принимает IP-адрес и порт цели как параметры, так и данные полезной нагрузки.

Например:

int socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
sendto(socket_fd, data, size, 0, destAddr, destAddrLen);
  • socket
  • sendto
  • recvfrom

TLS, SSL and security

До сих пор мы думали о создании сокетов, которые формируют сетевое соединение, а затем отправляют и получают данные по этому соединению. Однако у нас есть проблема с безопасностью. Данные, которые протекают по проводу, не зашифрованы. Это означает, что если кто-то должен «обнюхать» или иным образом исследовать сетевые данные, мы увидим содержимое передаваемых данных. Например, если я отправляю пароль, используемый для аутентификации, если мы будем изучать содержимое данных, мы сможем определить пароль, который я использую.

На самом деле не так сложно распознать отправленные и полученные данные. Отличные инструменты, такие как wirehark, используются для отладки и могут быть легко использованы для проверки содержимого сетевых пакетов или потоков. Очевидно, что мы уже обмениваемся данными кредитной карты, электронной почтой и другой конфиденциальной информацией через Интернет, как это делается?

Ответ - это концепция под названием «Secure Socket Layer» или SSL. SSL обеспечивает возможность шифрования данных перед передачей, так что только предполагаемый получатель может ее расшифровать. И наоборот, любые ответы, отправленные получателем, также зашифровываются таким образом, что только мы можем расшифровать данные. Если кто-то должен был захватывать или иным образом проверять данные, отправляемые по проводам, им не удастся вернуться к исходному содержанию данных.

То, как это работает, - это концепция секретных ключей. Представьте себе, что я думаю о очень большом случайном числе (и большим я имею в виду ОЧЕНЬ большой). Мы называем этот частный номер своим личным ключом. Теперь представьте, что связанный с закрытым ключом соответствующий номер (открытый ключ), который можно использовать для дешифрования сообщения, которое было закодировано с помощью закрытого ключа. Теперь представьте себе, что я хочу, чтобы наравне с партнером. Я отправляю запрос (незашифрованный) партнеру и запрос его открытого ключа. Он отправляет это обратно, и я посылаю ему копию своего открытого ключа зашифрованного его открытым ключом. Поскольку для дешифрования данных можно использовать только подходящую пару открытых / закрытых ключей, только желаемый получатель может расшифровать сообщение, после чего у него будет копия моего открытого ключа.

Теперь в будущем я могу отправить ему сообщения, зашифрованные моим личным ключом и зашифрованные его открытым ключом, и он сможет их расшифровать с помощью своей копии своего закрытого ключа и моего открытого ключа, пока он может отправлять мне зашифрованные сообщения, закодированные его Закрытый ключ, который я могу расшифровать с помощью моей копии открытого ключа. Поменяв открытые ключи, мы теперь можем продолжать обмен данными, не опасаясь, что это увидит кто-то еще.

Все это шифрование данных происходит вне и выше знаний о сетях TCP/IP. TCP/IP обеспечивает доставку данных, но ничего не заботится о его содержании.

Таким образом, и на высоком уровне, если мы хотим обмениваться защищенными данными, мы должны выполнить шифрование и дешифрование с использованием алгоритмов и библиотек, которые живут за пределами API сокетов, и использовать сокеты в качестве транспорта для передачи и получения зашифрованных данных, которые Поданных и полученных от алгоритмов шифрования.

При использовании mbed TLS нам нужен большой размер стека. Я еще не знаю, как мало мы можем уйти, но я использовал 8000 байт.

See also:

  • mbed TLS
  • mbed TLS home page
  • mbed TLS tutoria l
  • mbed TLS API reference

mbedTLS app structure

Давайте начнем разбирать структуру приложения TLS, которое использует API-интерфейсы mbedTLS.

Во-первых, существует понятие сетевого контекста, который инициализируется вызовом mbedtls_net_init ().

mbedtls_net_context server_fd;
mbedtls_net_init(&server_fd);

Здесь больше нечего объяснять. Инициализированные данные являются «непрозрачными» для нас, и вызов этой функции является частью правил.

Далее идет инициализация контекста SSL с вызовом:

mbedtls_ssl_init().
mbedtls_ssl_context ssl;
mbedtls_ssl_init(&ssl);

Опять же, здесь больше нечего объяснять. Данные снова непрозрачны, и эта функция просто инициализирует его для нас. Вызов этой функции также является частью правил.

Теперь мы вызываем mebtls_ssl_config_init ().

mbedtls_ssl_config config;
mbedtls_ssl_config_init(&config);

Эти инициализации повторяются для других типов данных, включая:

mbedtls_ctr_drbg_context ctr_drbg;
mbedtls_ctr_drbg_init(&ctr_drbg);
mbedtls_entropy_context entropy;
mbedtls_entropy_init(&entropy);
mbedtls_x509_crt cacert;
mbedtls_x509_crt_init(&cacert);

SSL использует хорошие генераторы случайных чисел. Что такое «хорошее» случайное число?

Поскольку компьютеры являются детерминированными устройствами, генерация случайного числа выполняется посредством выполнения алгоритма, и поскольку алгоритмы детерминированы, то последовательность чисел, порожденных этими функциями, может в принципе быть предсказуемой. Хорошим генератором случайных чисел является тот, где последовательность произведённых чисел вовсе не легко предсказуема и генерирует значения без смещения к их значениям с равной вероятностью любого числа в пределах выбранного диапазона.

Мы инициализируем генератор случайных чисел с вызовом bedtls_ctr_drbg_seed (). Примечание. Мы видим фразу «ctr_drbg» ..., которая является аббревиатурой «Генератор случайных байтов детерминированного режима». Это отраслевая стандартная спецификация / алгоритм для генерации случайных чисел.

[1]

С установкой под нашими ремнями пришло время начать рассмотрение связи на основе SSL. Поскольку мы рассматриваем SSL через сокеты, если мы не использовали сокеты TLS, мы бы выполнили вызов socket () для создания сокета, а затем connect () для подключения к нашему партнеру. В мире mbedtls мы называем mbedtls_net_connect (). Это имеет вид:

mbedtls_net_connect(&server_fd, <hostname>, <port>, MBEDTLS_NET_PROTO_TCP);

Имя хоста и порт определяют, к чему мы подключаемся. Обратите внимание на первый параметр.

Это структура mbedtls_net_context, которую мы инициализировали с помощью вызова mbedtls_net_init () ранее. Мы всегда должны проверять код возврата, чтобы убедиться, что соединение было успешным. Теперь мы настроим настройки SSL по умолчанию с вызовом mbedtls_ssl_config_defaults (). Например:

mbedtls_ssl_config_defaults(
   &conf, MBEDTLS_SSL_IS_CLIENT,
   MBEDTLS_SSL_TRANSPORT_STREAM,
   MBED_SSL_PRESET_DEFAULT)

Когда мы общаемся через SSL, мы обычно хотим подтвердить, что учетные данные, предоставленные нашим партнером, указывают на то, что они являются теми, кого они утверждают. Этот процесс называется аутентификацией. Мы можем определить, какую аутентификацию мы хотим выполнить, вызывая mbedtls_ssl_conf_authmode ().

mbedtls_ssl_conf_authmode(&ssl, MBEDTLS_SSL_VERIFY_NONE)

Ранее мы говорили, что SSL сильно зависит от хорошего генератора случайных чисел.

Теперь мы расскажем об окружающей среде, какой генератор случайных чисел мы хотим использовать:

mbedtls_ssl_conf_rng(&ssl, mbedtls_ctr_drbg_random, &ctr_drbg)

Затем мы создадим еще несколько настроек контекста SSL, вызывая mbedtls_ssl_set_hostname().

mbedtls_ssl_set_hostname(&ssl, "name")

Теперь мы инструктируем среду SSL, которая функционирует для отправки и получения данных, вызывая mbedtls_ssl_set_bio ().

mbedtls_ssl_set_bio(&ssl, &server_fd, mbedtls_net_send, mbedtls_net_recv, NULL)

На этом этапе мы создали связь с нашим партнером и настроили среду SSL. Остается на самом деле читать и писать данные. Для записи данных мы называем mbed_ssl_write ().

mbedtls_ssl_write(&ssl, buf, len)

and to read data we call mbedtls_ssl_read().

mbedtls_ssl_read(&ssl, buf, len)

See also:

  • mbedtls_net_init
  • mbedtls_ssl_init
  • mbedtls_ssl_config_init
  • mbedtls_net_connect
  • mbedtls_ssl_config_defaults
  • mbedtls_ssl_conf_authmode
  • mbedtls_ssl_conf_rng
  • mbedtls_ssl_set_hostname
  • mbedtls_ssl_set_bio
  • mbedtls_ssl_write
  • mbedtls_ssl_read

mbedTLS Example

Вот примерная функция, которая была протестирована на ESP32, чтобы вызвать HTTPS-вызов на сервер HTTPS для получения некоторых результатов.

#include "mbedtls/platform.h"
#include "mbedtls/ctr_drbg.h"
#include "mbedtls/debug.h"
#include "mbedtls/entropy.h"
#include "mbedtls/error.h"
#include "mbedtls/net.h"
#include "mbedtls/ssl.h"
#include "esp_log.h"
#include "string.h"
#include "stdio.h"
#define SERVER_NAME "httpbin.org"
#define SERVER_PORT "443"
static char tag[] = "callhttps";
static char errortext[256];

static void my_debug(void *ctx, int level, const char *file, int line, const char *str) {
   ((void) level);
   ((void) ctx);
   printf("%s:%04d: %s", file, line, str);
}
void callhttps() {
   ESP_LOGD(tag, "--> callhttps\n");
   mbedtls_net_context server_fd;
   mbedtls_entropy_context entropy;
   mbedtls_ctr_drbg_context ctr_drbg;
   mbedtls_ssl_context ssl;
   mbedtls_ssl_config conf;
   mbedtls_x509_crt cacert;
   int ret;
   int len;
   char *pers = "ssl_client1";
   unsigned char buf[1024];
   mbedtls_net_init(&server_fd);
   mbedtls_ssl_init(&ssl);
   mbedtls_ssl_config_init(&conf);
   mbedtls_x509_crt_init(&cacert);
   mbedtls_ctr_drbg_init(&ctr_drbg);
   mbedtls_entropy_init(&entropy);
   mbedtls_ssl_conf_dbg(&conf, my_debug, stdout);
   mbedtls_debug_set_threshold(2); // Log at error only
   ret = mbedtls_ctr_drbg_seed(
   &ctr_drbg,
   mbedtls_entropy_func, &entropy, (const unsigned char *) pers,             strlen(pers));
   if (ret != 0) {
      ESP_LOGE(tag, " failed\n ! mbedtls_ctr_drbg_seed returned %d\n", ret);
      return;
   }
   ret = mbedtls_net_connect(&server_fd, SERVER_NAME, SERVER_PORT, MBEDTLS_NET_PROTO_TCP);
   if (ret != 0) {
      ESP_LOGE(tag, " failed\n ! mbedtls_net_connect returned %d\n\n", ret);
      return;
   }
   ret = mbedtls_ssl_config_defaults(
      &conf,
      MBEDTLS_SSL_IS_CLIENT,
      MBEDTLS_SSL_TRANSPORT_STREAM,
      MBEDTLS_SSL_PRESET_DEFAULT);
   if (ret != 0) {
      ESP_LOGE(tag, " failed\n ! mbedtls_ssl_config_defaults returned %d\n\n", ret);
   return;
   }
   mbedtls_ssl_conf_authmode(&conf, MBEDTLS_SSL_VERIFY_NONE);
   mbedtls_ssl_conf_rng(&conf, mbedtls_ctr_drbg_random, &ctr_drbg);
   ret = mbedtls_ssl_setup(&ssl, &conf);
   if (ret != 0) {
      mbedtls_strerror(ret, errortext, sizeof(errortext));
      ESP_LOGE(tag, "error from mbedtls_ssl_setup: %d - %x - %s\n", ret, ret, errortext);
      return;
   }
   ret = mbedtls_ssl_set_hostname(&ssl, "httpbin.org");
   if (ret != 0) {
         mbedtls_strerror(ret, errortext, sizeof(errortext));
         ESP_LOGE(tag, "error from mbedtls_ssl_set_hostname: %d - %x -       %s\n", ret, ret, errortext);
      return;
   }
   mbedtls_ssl_set_bio(&ssl, &server_fd, mbedtls_net_send,      mbedtls_net_recv, NULL);
   char *requestMessage = \
      "GET /ip HTTP/1.1\r\n" \
      "User-Agent: kolban\r\n" \
      "Host: httpbin.org\r\n" \
      "Accept-Language: en-us\r\n" \
      "Accept-Encoding: gzip, deflate\r\n" \
      "\r\n";
   sprintf((char *)buf, requestMessage);
   len = strlen((char *)buf);
   ret = mbedtls_ssl_write(&ssl, buf, len);
   if (ret < 0) {
      mbedtls_strerror(ret, errortext, sizeof(errortext));
      ESP_LOGE(tag, "error from write: %d -%x - %s\n", ret, ret,       errortext);
      return;
   }
   len = sizeof(buf);
   ret = mbedtls_ssl_read(&ssl, buf, len);
   if (ret < 0) {
      ESP_LOGE(tag, "error from read: %d\n", len);
      return;
   }
   printf("Result:\n%.*s\n", len, buf);
   mbedtls_net_free(&server_fd);
   mbedtls_ssl_free(&ssl);
   mbedtls_ssl_config_free(&conf);
   mbedtls_ctr_drbg_free(&ctr_drbg);
   mbedtls_entropy_free(&entropy);
   ESP_LOGV(tag, "All done");
}

Заметки:

При отладке MBED в Curl установите значение MBEDTLS_DEBUG на 1 в curl_config.h

OpenSSL

OpenSSL - популярная реализация стека SSL. В среде ESP32 выбранный стек для SSL / TLS является mbedTLS, который не совпадает с OpenSSL. В рамках ESP-IDF был предоставлен слой отображения, который предоставляет API OpenSSL поверх реализации mbedTLS.

Name Service

В Интернете серверные машины можно найти по их службе имен доменов (DNS). Это сервис, который переводит читаемое человеком представление машины, например «google.com», в необходимое значение IP-адреса (например, 216.58.217.206).

Чтобы это преобразование произошло, ESP32 должен знать IP-адрес одного или более DNS-серверов, которые затем будут использоваться для выполнения преобразования имени в IP-адрес.

Если мы используем DHCP, тогда больше ничего не нужно делать - DHCP-сервер автоматически предоставляет адреса DNS-серверов.

Однако, если мы не используем DHCP (например если мы используем статические IP-адреса), тогда нам нужно проинструктировать ESP32 о местоположения DNS-серверов вручную. Мы можем сделать это с помощью функции dns_setserver().

Это принимает IP-адрес как входной вместе с каким из двух возможных DNS-серверов для использования. ESP32 настроен на то, чтобы знать личность до двух внешних серверов имен.

Причина для двоих заключается в том, что если попытка достичь первого не удастся, мы будем использовать второй. Мы можем получить наши текущие идентификаторы DNS-сервера, используя dns_getserver (). Google публично предоставляет два сервера имен с адресами 8.8.8.8 и 8.8.4.4. Как только мы определим серверы имен, мы можем найти адрес имени хоста используя функцию gethostbyname().

Во время разработки мы можем протестировать конкретный DNS-сервер, чтобы проверить, что он может определить по имени хоста.

Отличным инструментом Linux для выполнения этой задачи является «nslookup». В нем есть многие варианты, но для наших целей мы можем предоставить им имя хоста для поиска и использовать DNS-сервер:

$ nslookup example.com 8.8.8.8
Server: 8.8.8.8
Address: 8.8.8.8#53
Non-authoritative answer:
Name: example.com
Address: 93.184.216.34

See also:

  • gethostbyname
  • dns_getserver
  • dns_setserver
  • Wikipedia: Domain Name System
  • Google: Public DNS

Multicast Domain Name Systems

В локальной сети с приходом и выходом динамических устройств мы можем захотеть, чтобы одно устройство находило IP-адрес другого устройства, чтобы они могли взаимодействовать друг с другом. Проблема состоит в том, что IP-адреса могут динамически выделяться сервером DHCP, работающим на точке доступа WiFi. Это означает, что IP-адрес устройства, скорее всего, не будет статичным. Кроме того, это не очень полезная история, чтобы ссылаться на устройства по их IP-адресам. Нам нужна некоторая форма службы динамического имени для поиска устройств по имени, где их IP-адреса не настроены администратором. Именно здесь вступает в действие система доменных имен многоадресной рассылки (mDNS). На высоком уровне, когда устройство хочет найти другое устройство с заданным именем, он передает запрос всем членам сети, запрашивая ответ от устройства с таким именем. Если машина считает, что она имеет эту личность, она отвечает своей собственной трансляцией, которая включает его имя и IP-адрес. Это не только удовлетворяет первоначальному запросу, но и другие машины в сети могут видеть это взаимодействие и кэшировать ответ для себя. Это означает, что если им нужно будет разрешить один и тот же хост в будущем, у них уже есть ответ. Используя многоадресную систему доменных имен (mDNS), ESP32 может попытаться разрешить имя хоста компьютера в локальной сети на его IP-адрес. Он делает это, передавая пакет, запрашивая, чтобы машина с этим идентификатором отвечала. Демоны службы имен реализованы Bonjour и nss-mdns (Linux). Обычно хосты, расположенные с использованием этого метода, принадлежат домену, заканчивающемуся «.local». Чтобы определить, участвует ли ваш компьютер в mDNS, вы можете проверить, прослушивает ли он порт UDP 5353. Это порт, используемый для связи mDNS.

См. Также:

  • Wikipedia – Multicast DNS
  • IETF RFC 6762: Multicast DNS
  • Multicast DNS
  • New DNS Technologies in the Lan
  • Avah i – Implementation of mDNS … source project for Unix machines
  • Adafruit – Bonjour (Zeroconf) Networking for Windows and Linux
  • chrome.mdns – API description for Chrome API for mDNS
  • Android – ZeroConf Browser

mDNS API programming

ESP-IDF предоставляет набор богатых API для программирования mDNS на ESP32.

В частности, мы можем либо рекламировать себя в mDNS, либо запросить существующую информацию mDNS.

Атрибуты записи сервера mDNS выглядят следующим образом:

  • hostname – mdns_set_hostname()
  • default instance – mdns_set_instance()
  • service – mdns_service_add()
    • type – _http, _ftp etc etc
    • protocol – _tcp, _udp etc etc
    • port – port number
  • instance name for service – mdns_service_instance_set()
  • TXT data for service – mdns_service_txt_set()

See also:

  • mdns_set_hostname
  • mdns_set_instance
  • mdns_service_add
  • mdns_service_instance_set
  • mdns_service_port_set

Installing Bonjour

Launch the Bonjour installer:

Если все пошло хорошо, мы найдем новую службу Windows, называемую "Bonjour Service": There is also a Bonjour browser available here …

Avahi

Реализация многоадресной DNS в Linux называется Avahi. Avahi работает как демон systemd под названием «avahi-daemon». Мы можем определить, работает ли он с:

$ systemctl status avahi-daemon
● avahi-daemon.service - Avahi mDNS/DNS-SD Stack
Loaded: loaded (/lib/systemd/system/avahi-daemon.service; enabled)
Active: active (running) since Wed 2016-01-20 22:13:35 CST; 1 day 13h ago Main PID: 384 (avahi-daemon)
Status: "avahi-daemon 0.6.31 starting up."
CGroup: /system.slice/avahi-daemon.service
├─384 avahi-daemon: running [raspberrypi.local]
└─426 avahi-daemon: chroot helper
The avahi-daemon utilizes a configuration file found at /etc/avahi/avahi-daemon.conf.

Имя по умолчанию, которое avahi рекламирует как локальное имя хоста. Когда выполняется разрешение имени хоста, системный файл с именем /etc/nsswitch.conf используется для определения порядка разрешения. В частности, запись хостов содержит разрешение имен. Примером может служить:

hosts: files mdns4_minimal [NOTFOUND=return] dns

Что говорит «сначала посмотрите в / etc / hosts, затем обратитесь к mDNS, а затем используйте полный DNS». Это означает, что устройство, которое рекламирует себя с помощью mDNS, может быть найдено с помощью поиска «<hostname> .local». Например, если я загружаю машину Linux, которая получает динамический IP-адрес через DHCP, а имя хоста этого компьютера - «chip1», то я могу связаться с ним с адресом домена «chip1.local». Если IP-адрес устройства изменяется, последующие разрешения имени домена будут продолжать корректно разрешаться.

Инструменты Avahi не устанавливаются по умолчанию, но могут быть установлены с использованием пакета «avahi-utils»:

$ sudo apt-get install avahi-utils

Чтобы просмотреть список устройств mDNS в вашей сети, мы можем использовать команду avahi-browse. Например:

$ avahi-browse -at
+ wlan1 IPv6 chip1 [ce:79:cf:21:db:95] Workstation local
+ wlan1 IPv4 chip1 [ce:79:cf:21:db:95] Workstation local
+ wlan0 IPv6 pizero [00:36:76:21:97:a3] Workstation local
+ wlan0 IPv6 raspi3 [b8:27:eb:9d:fc:60] Workstation local
+ wlan0 IPv6 chip1 [cc:79:cf:21:db:95] Workstation local
+ wlan0 IPv4 pizero [00:36:76:21:97:a3] Workstation local
+ wlan0 IPv4 raspi3 [b8:27:eb:9d:fc:60] Workstation local
+ wlan0 IPv4 chip1 [cc:79:cf:21:db:95] Workstation local
+ wlan0 IPv6 pizero Remote Disk Management
local
+ wlan0 IPv6 raspi3 Remote Disk Management
local
+ wlan0 IPv4 raspi3 Remote Disk Management
local
+ wlan0 IPv4 pizero Remote Disk Management
local
+ wlan0 IPv4 WDMyCloud Apple File Sharing local
+ wlan0 IPv4 WDMyCloud _wd-2go._tcp local
+ wlan0 IPv4 WDMyCloud Web Site local
+ wlan0 IPv4 Living Room _googlecast._tcp local
+ wlan0 IPv4 123456789 _teamviewer._tcp local

Чтобы получить доступ к объявленному сервером mDNS из Microsoft Windows, вам понадобится служба, аналогичная установленному Bonjour от Apple. Bonjour распространяется как часть продукта iTunes от Apple. После установки мы должны иметь доступ к опубликованным серверам по адресу <name> .local. Доступен инструмент партнерских окон под названием «Bonjour Browser» Который отображает список серверов mDNS на окнах.

See also:

  • avahi home page
  • man(1) – avahi-browse
  • man(5) – avahi-daemon.conf

Working with SNTP

SNTP is the Simple Network Time Protocol and allows a device connected to the Internet to learn the current time. In order to use this, you must know of at least one time server located on the Internet. The US National Institute for Science and Technology (NIST) maintains a number of these which can be found here:

http://tf.nist.gov/tf-cgi/servers.cg i

Other time servers can be found all over the globe and I encourage you to Google search for your nearest or country specific server.

Once you know the identity of a server by its host name or IP address, you can call either of the functions called sntp_setservername() or sntp_setserver() to declare that we wish to use that time server instance. The ESP32 can be configured with up to three different time servers so that if one or two are not available, we might still get a result.

The ESP32 must also be told the local timezone in which it is running. This is set with a call to sntp_set_timezone() which takes the number of hours offset from UTC. For example, I am in Texas and my timezone offset becomes "-5". Although this function is present, I would suggest using the POSIX tzset() function instead.

With these configured, we can start the SNTP service on the ESP32 by calling sntp_init(). This will cause the device to determine its current time by sending packets over the network to the time servers and examining their responses. It is important to note that immediately after calling sntp_init(), you will not yet know what the current time may be. This is because it may take a few seconds for the ESP32 to sends the time requests and get their responses and this will all happen asynchronously to your current commands and won't complete till sometime later.

When ready, we can retrieve the current time with a call to sntp_get_current_timestamp() which will return the number of seconds since the 1st of January 1970 UTC. We can also call the function called sntp_get_real_time() which will return a string representation of the time. While these functions obviously exist, I would not recommend using them. Instead look at the POSIX alternatives which are time() and asctime().

Here is an example of using SNTP to set the time:

ip_addr_t addr; sntp_setoperatingmode(SNTP_OPMODE_POLL); inet_pton(AF_INET, "129.6.15.28", &addr); sntp_setserver(0, &addr); sntp_init();

The time can be accessed from a variety of POSIX compliant functions including:

  • asctime – Build a string representation of time.
  • clock – Return processor time.
  • ctime – Build a string representation of time.
  • difftime – Calculate a time difference.
  • gettimeofday – Retrieve the current time of day.
  • gmtime – Produce a struct tm from a time_t.
  • localtime – Produce a struct tm from a time_t.
  • settimeofday – Set the current time.
  • strftime – Format a time_t to a string.
  • time – Get the current time as a time_t (seconds since epoch).

See also:

  • SNTP API
  • Timers and time
  • asctime
  • ctime
  • gmtime
  • localtime
  • strftime
  • IETF RFC5905: Network Time Protocol Version 4: Protocol and

Algorithms Specification

Java Sockets

The sockets API is the defacto standard API for programming against TCP/IP. My programming language of choice is Java and it has full support for sockets. What this means is that I can write a Java based application that leverages sockets to communicate with the ESP32. I can send and receive data through quite easily.

In Java, there are two primary classes that represents sockets, those are java.net.Socket which represents a client application which will form a connection and the second class is java.net.ServerSocket which represents a server that is listening on a socket awaiting a client connection. Since the ESP32 can be either a client or a server, both of these Java classes will come into play.

To connect to an ESP32 running as a server, we need to know the IP address of the device and the port number on which it is listening. Once we know those, we can create an instance of the Java client with: Socket clientSocket = new Socket(ipAddress, port);

This will form a connection to the ESP32. Now we can ask for both an InputStream from which to receive partner data and an OutputStream to which we can write data.

InputStream is = clientSocket.getInputStream(); OutputStream os = clientSocket.getOutputStream();

When we are finished with the connection, we should call close() to close the Java side of the connection:

clientSocket.close();

It really is as simple as that. Here is an example application: package kolban;

import java.io.OutputStream;
import java.net.Socket;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.Options;
public class SocketClient {
private String hostname;
private int port;
public static void main(String[] args) {
Options options = new Options();
options.addOption("h", true, "hostname");
options.addOption("p", true, "port");
CommandLineParser parser = new DefaultParser();
try {
CommandLine cmd = parser.parse(options, args);
SocketClient client = new SocketClient();
client.hostname = cmd.getOptionValue("h");
client.port = Integer.parseInt(cmd.getOptionValue("p"));
client.run();
} catch (Exception e) {
e.printStackTrace();
}
}
public void run() {
try {
int SIZE = 65000;
byte data[] = new byte[SIZE];
for (int i = 0; i < SIZE; i++) {
data[i] = 'X';
}
Socket s1 = new Socket(hostname, port);
OutputStream os = s1.getOutputStream();
os.write(data);
s1.close();
System.out.println("Data sent!");
} catch (Exception e) {
e.printStackTrace();
}
}
} // End of class
// End of file

To configure a Java application as a socket server is just as easy. This time we create an instance of the SocketServer class using:

SocketServer serverSocket = new SocketServer(port) The port supplied is the port number on the machine on which the JVM is running that will be the endpoint of remote client connection requests. Once we have a ServerSocket instance, we need to wait for an incoming client connection. We do this using the blocking API method called accept().

Socket partnerSocket = serverSocket.accept(); This call blocks until a client connect arrives. The returned partnerSocket is the connected socket to the partner which can used in the same fashion as we previously discussed for client connections.

This means that we can request the InputStream and OutputStream objects to read and write to and from the partner. Since Java is a ultithreaded language, once we wake up from accept() we can pass off the received partner socket to a new thread and repeat the accept() call for other parallel connections. Remember to close() any partner socket connections you receive when you are done with them.

So far, we have been talking about TCP oriented connections where once a connection is opened it stays open until closed during which time either end can send or receive independently from the other. Now we look at datagrams that use the UDP protocol.

The core class behind this is called DatagramSocket. Unlike TCP, the DatagramSocket class is used both for clients and servers. First, let us look at a client. If we wish to write a Java UDP client, we will create an instance of a DatagramSocket using:

DatagramSocket clientSocket = new DatagramSocket();

Next we will "connect" to the remote UDP partner. We will need to know the IP address and port that the partner is listening upon. Although the API is called "connect", we need to realize that no connection is formed. Datagrams are connectionless so what we are actually doing is associating our client socket with the partner socket on the other end so that when we actually wish to send data, we will know where to send it to.

clientSocket.connect(ipAddress, port);

Now we are ready to send a datagram using the send() method:

DatagramPacket data = new DatagramPacket(new byte[100], 100); clientSocket.send(data);

To write a UDP listener that listens for incoming datagrams, we can use the following:

DatagramSocket serverSocket = new DatagramSocket(port);

The port here is the port number on the same machine as the JVM that will be used to listen for incoming UDP connections. To wait for an incoming datagram, call receive().

DatagramPacket data = new DatagramPacket(new byte[100], 100); clientSocket.receive(data);

If you are going to use the Java Socket APIs, read the JavaDoc thoroughly for these classes are there are many features and options that were not listed here.

See also:

  • Java tutorial: All About Sockets
  • JDK 8 JavaDoc