1. 简介
UDP协议(User Datagram Protocol),全称用户数据报协议,它是一种面向非连接的协议,面向非连接指的是在正式通信前不必与对方先建立连接, 不管对方状态就直接发送。至于对方是否可以接收到这些数据内容,UDP协议无法控制,因此说UDP协议是一种不可靠的协议。UDP协议适用于一次只传送少量数据、对可靠性要求不高的应用环境。
因为UDP的数据传输不一定是一对一的,所以也衍生出单播、组播和广播的概念。
1. 单播
单播(unicast),是指封包在计算机网络的传输中,目的地址为单一目标的一种传输方式。它是现今网络应用最为广泛,通常所使用的网络协议或服务大多采用单播传输,例如TCP协议。
2. 多播
组播(multicast),也叫多播或群播。 指把信息同时传递给一组目的地址。它使用策略是最高效的,因为消息在每条网络链路上只需传递一次,而且只有在链路分叉的时候,消息才会被复制。
3. 广播
广播(broadcast),是指封包在计算机网络中传输时,目的地址为网络中所有设备的一种传输方式。
2. lwIP
ESP-IDF使用lwIP库实现TCP/IP协议栈,这个库在大多数嵌入式系统中都有用到,它是一个轻量的TCP/IP协议层套件。
支持以下特性:
- IP (Internet Protocol, IPv4 and IPv6) including packet forwarding over multiple network interfaces
- ICMP (Internet Control Message Protocol) for network maintenance and debugging
- IGMP (Internet Group Management Protocol) for multicast traffic management
- MLD (Multicast listener discovery for IPv6). Aims to be compliant with RFC 2710. No support for MLDv2
- ND (Neighbor discovery and stateless address autoconfiguration for IPv6). Aims to be compliant with RFC 4861 (Neighbor discovery) and RFC 4862 (Address autoconfiguration)
- DHCP, AutoIP/APIPA (Zeroconf) and (stateless) DHCPv6
- UDP (User Datagram Protocol) including experimental UDP-lite extensions
- TCP (Transmission Control Protocol) with congestion control, RTT estimation fast recovery/fast retransmit and sending SACKs
- raw/native API for enhanced performance
- Optional Berkeley-like socket API
- TLS: optional layered TCP ("altcp") for nearly transparent TLS for any
- TCP-based protocol (ported to mbedTLS) (see changelog for more info)
- PPPoS and PPPoE (Point-to-point protocol over Serial/Ethernet)
- DNS (Domain name resolver incl. mDNS)
- 6LoWPAN (via IEEE 802.15.4, BLE or ZEP)
支持以下应用层协议:
- HTTP server with SSI and CGI (HTTPS via altcp)
- SNMPv2c agent with MIB compiler (Simple Network Management Protocol), v3 via altcp
- SNTP (Simple network time protocol)
- NetBIOS name service responder
- MDNS (Multicast DNS) responder
- iPerf server implementation
- MQTT client (TLS support via altcp)
3. 例程
例程分别在ESP32上实现TCP客户端和服务端,使用电脑作为另一方进行简单通信测试。需要注意的是,测试时,ESP32和电脑必须处于同一局域网。
电脑端测试使用的上位机为VOFA+,下载地址:VOFA+
3.1 发送端
这个例程配置ESP32为发送端,当连接WiFi热点成功后会向目标IP每隔1秒发送一段数据。
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/semphr.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "esp_mac.h"
#include "nvs_flash.h"
#include "sys/socket.h"
#include "lwip/err.h"
#include "lwip/sys.h"
#include "netdb.h"
#include "arpa/inet.h"
#include <string.h>
#define TAG "app"
#define HOST_IP_ADDR "172.16.10.26"
#define HOST_PORT 20001
static const char *payload = "Hello from ESP32\r\n";
static TaskHandle_t client_task_handle;
static void udp_client_task(void *args)
{
struct sockaddr_in dest_addr;
dest_addr.sin_addr.s_addr = inet_addr(HOST_IP_ADDR);
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = htons(HOST_PORT);
int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
if (sock < 0) {
ESP_LOGE(TAG, "Unable to create socket: errno %d", errno);
}
ESP_LOGI(TAG, "Socket created, sending to %s:%d", HOST_IP_ADDR, HOST_PORT);
while (1) {
int err = sendto(sock, payload, strlen(payload), 0, (struct sockaddr *)&dest_addr, sizeof(dest_addr));
if (err < 0) {
ESP_LOGE(TAG, "Error occurred during sending: errno %d", errno);
break;
}
ESP_LOGI(TAG, "Message sent");
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
close(sock);
vTaskDelete(NULL);
}
static void wifi_event_handler(void* arg,
esp_event_base_t event_base,
int32_t event_id,
void* event_data)
{
if (event_base == IP_EVENT) {
if (event_id == IP_EVENT_STA_GOT_IP) {
xTaskCreate(udp_client_task, "udp_client", 4096, NULL, 5, &client_task_handle);
}
} else if (event_base == WIFI_EVENT) {
if (event_id == WIFI_EVENT_STA_DISCONNECTED) {
vTaskDelete(client_task_handle);
} else if (event_id == WIFI_EVENT_STA_START) {
esp_wifi_connect();
}
}
}
int app_main()
{
/* 初始化NVS */
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ESP_ERROR_CHECK(nvs_flash_init());
}
/* 初始化WiFi协议栈 */
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_netif_create_default_wifi_sta();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
ESP_EVENT_ANY_ID,
&wifi_event_handler,
NULL,
NULL));
ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,
ESP_EVENT_ANY_ID,
&wifi_event_handler,
NULL,
NULL));
wifi_config_t wifi_config = {
.sta = {
.ssid = "QXL",
.password = "88888888",
.threshold.authmode = WIFI_AUTH_WPA_WPA2_PSK,
},
};
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
ESP_ERROR_CHECK(esp_wifi_start());
return 0;
}
ESP32的WiFi驱动初始化在前面的文章已经有详细的介绍了,这里不再赘述。
因为UDP是面向无连接的,所以编程会相对简单,流程如下:
1. 创建套接字
调用socket函数。第一个参数表示IP协议,这里使用IPv4,对应IP_INET;第二个参数表示socket类型,UDP协议只能填SOCK_DGRAM;第三个参数表示协议栈类型,这里填IPPROTO_IP。函数会返回套接字描述符。
2. 套接字配置(可选)
socket的接收和发送默认是阻塞的,即如果数据不到达就一直等待,所以根据应用的需要设置接收的超时时间。
调用setsockopt函数。第一个参数为套接字描述符;第二个参数为协议层,必须填SOL_SOCKET;第三个参数为设置项,填SO_RCVTIMEO,设置超时时间,当然函数还支持以下配置项:
#define SO_DEBUG 0x0001 /* Unimplemented: turn on debugging info recording */
#define SO_ACCEPTCONN 0x0002 /* socket has had listen() */
#define SO_DONTROUTE 0x0010 /* Unimplemented: just use interface addresses */
#define SO_USELOOPBACK 0x0040 /* Unimplemented: bypass hardware when possible */
#define SO_LINGER 0x0080 /* linger on close if data present */
#define SO_DONTLINGER ((int)(~SO_LINGER))
#define SO_OOBINLINE 0x0100 /* Unimplemented: leave received OOB data in line */
#define SO_REUSEPORT 0x0200 /* Unimplemented: allow local address & port reuse */
#define SO_SNDBUF 0x1001 /* Unimplemented: send buffer size */
#define SO_RCVBUF 0x1002 /* receive buffer size */
#define SO_SNDLOWAT 0x1003 /* Unimplemented: send low-water mark */
#define SO_RCVLOWAT 0x1004 /* Unimplemented: receive low-water mark */
#define SO_SNDTIMEO 0x1005 /* send timeout */
#define SO_RCVTIMEO 0x1006 /* receive timeout */
#define SO_ERROR 0x1007 /* get error status and clear */
#define SO_TYPE 0x1008 /* get socket type */
#define SO_CONTIMEO 0x1009 /* Unimplemented: connect timeout */
#define SO_NO_CHECK 0x100a /* don't create UDP checksum */
#define SO_BINDTODEVICE 0x100b /* bind to device */
比较常用的就是SO_RCVTIMEO,设置接收超时;SO_SNDTIMEO,设置发送超时。上面带Unimlemented注释的是ESP-IDF不支持的配置。
每种配置项需要传入的数据是不一样的,用之前建议参考官方的说明文档。设置超时传入的是struct timeval这个结构体。
struct timeval {
time_t tv_sec;
suseconds_t tv_usec;
};
tv_sec设置秒数,tv_usec设置微秒数。
3. 发送数据
调用sendto函数。参数1为套接字描述符;参数2为数据指针;参数3为数据长度;参数4为标志位,有以下选项:
#define MSG_PEEK 0x01
#define MSG_WAITALL 0x02
#define MSG_OOB 0x04
#define MSG_DONTWAIT 0x08
#define MSG_MORE 0x10
#define MSG_NOSIGNAL 0x20
这些标志位是发送和接收都支持的,比较常用的是MSG_DONTWAIT,像发送和接收函数是阻塞的,使能这个标志位可以让函数立即返回,不等待数据。
参数5为目标地址信息,结构体如下:
struct sockaddr_in {
u8_t sin_len;
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
#define SIN_ZERO_LEN 8
char sin_zero[SIN_ZERO_LEN];
};
#endif /* LWIP_IPV4 */
- sin_len:数据长度(一般不需要填);
- sin_family:套接字类型,IPv4填AF_INET,IPv6填AF_INET6,其他填AF_UNSPEC;
- sin_port:端口;
- sin_zero:预留字节(不用管)。
参数6为目标地址结构体长度。
4. 关闭连接
调用close函数。传入套接字描述符即可。
使用上位机时,数据引擎选择“RawData”,数据接口选择UDP,远程IP填写ESP32获取到的IP,本地端口要与ESP32发送报文的端口一致。设置好后,点击左上角的圆形按钮即可启动连接,在下面文本框能看到接收到的消息。
2.2 接收端
这个例程创建一个UDP接收端,接收所有发送至指定端口的UDP包。
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/semphr.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "esp_mac.h"
#include "nvs_flash.h"
#include "sys/socket.h"
#include "lwip/err.h"
#include "lwip/sys.h"
#include "netdb.h"
#include "arpa/inet.h"
#include <string.h>
#define TAG "app"
#define SERVER_PORT 20001
static TaskHandle_t server_task_handle;
static void udp_server_task(void *args)
{
char rx_buffer[128];
struct sockaddr_in dest_addr;
dest_addr.sin_addr.s_addr = htonl(IPADDR_ANY);
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = htons(SERVER_PORT);
int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
if (sock < 0) {
ESP_LOGE(TAG, "Unable to create socket: errno %d", errno);
}
int opt = 0;
setsockopt(sock, SOL_SOCKET, SO_BROADCAST, &opt, sizeof(int));
int err = bind(sock, (struct sockaddr *)&dest_addr, sizeof(dest_addr));
if (err < 0) {
ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno);
goto __exit;
}
ESP_LOGI(TAG, "Socket bound to port %d", SERVER_PORT);
while (1) {
struct sockaddr_in source_addr = {0};
socklen_t socklen = sizeof(source_addr);
memset(rx_buffer, 0, sizeof(rx_buffer));
int len = recvfrom(sock, rx_buffer, sizeof(rx_buffer) - 1, 0, (struct sockaddr *)&source_addr, &socklen);
if (len < 0) {
ESP_LOGE(TAG, "recvfrom failed: errno %d", errno);
} else {
ESP_LOGI(TAG, "Received %d bytes from " IPSTR ":%d, data: %s", len, IP2STR((esp_ip4_addr_t *) &source_addr.sin_addr), source_addr.sin_port, rx_buffer);
}
}
__exit:
close(sock);
vTaskDelete(NULL);
}
static void wifi_event_handler(void* arg,
esp_event_base_t event_base,
int32_t event_id,
void* event_data)
{
if (event_base == IP_EVENT) {
if (event_id == IP_EVENT_STA_GOT_IP) {
xTaskCreate(udp_server_task, "udp_server", 4096, NULL, 5, &server_task_handle);
}
} else if (event_base == WIFI_EVENT) {
if (event_id == WIFI_EVENT_STA_DISCONNECTED) {
vTaskDelete(server_task_handle);
} else if (event_id == WIFI_EVENT_STA_START) {
esp_wifi_connect();
}
}
}
int app_main()
{
/* 初始化NVS */
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ESP_ERROR_CHECK(nvs_flash_init());
}
/* 初始化WiFi协议栈 */
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_netif_create_default_wifi_sta();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
ESP_EVENT_ANY_ID,
&wifi_event_handler,
NULL,
NULL));
ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,
ESP_EVENT_ANY_ID,
&wifi_event_handler,
NULL,
NULL));
wifi_config_t wifi_config = {
.sta = {
.ssid = "Your SSID",
.password = "Your password",
.threshold.authmode = WIFI_AUTH_WPA_WPA2_PSK,
},
};
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
ESP_ERROR_CHECK(esp_wifi_start());
return 0;
}
接收者的流程和发送者差不多,下面只介绍有差异的部分。
1. 创建套接字
参考前面。
2. 配置套接字(可选)
调用setsockopt函数。除了前面介绍到的配置,UDP还支持一个SO_BROADCAST的配置,用来使能广播包的接收,我这里是配置为禁用。
3. 绑定IP(可选)
调用bind函数。参数1为套接字描述符,参数2为IP地址结构体,参数3为结构体长度。通过bind可以绑定监听的IP地址和端口,只有接收到的数据包目标IP和端口一致才会处理,我这里配置接收任意IP,端口为20001。
4. 接收数据
调用recvfrom函数。传入参数和sendto一致,不同的是后面的IP地址结构体是输出参数,不需要初始化,它会返回数据包的源IP信息。
5. 关闭套接字
参考前面。
上位机的配置和前面基本一样,唯一的不同是远程端口要保证和ESP32绑定的端口一致。