ESP32入门基础之UDP和TCP实验


本章实验主要内容是 lwIP,知识点参考如下:

  1. 乐鑫官方资料
  2. [野火EmbedFire]《[野火]LwIP应用开发实战指南—基于野火STM32》
  3. 计算机网络(第7版)-谢希仁

1 用户数据协议报UDP简介

  1. UDP是无连接的, 即发送数据之前 不需要建立连接(当然, 发送数据结束时也没有 连接可释放), 因此减少了开销和发送数据之前的时延。
  2. UDP使用尽最大努力交付, 即不保证可靠交付。
  3. UDP是面向报文的。 发送方的UDP 对应用 程序交下来的报文,在添加首部后就向下交付IP层 。UDP对应用层交下来的报文, 既不 合井, 也不拆分,而是保留这些报文的边界。
  4. UDP没有拥塞控制, 因此网络出现的拥塞不 会使源主机的发送速率降低。
  5. UDP支持一对一、一对多、多对一、多对多的交互通信。
  6. UDP首部开销小,只有8个字节,比TCP的20个字节要短。

在使用过程中,由于UDP的实时性较好,常用于实时视频的传输,比如直播、网络电话等,因为即使是出现了数据丢失的情况,导致视频卡帧,影响不大,所以,UDP 协议还是会被应用与对传输速度有要求,并且可以容忍出现差错的数据传输中。

1.1 UDP作为client进行数据收发实验

参考例程:esp-idf/examples/protocols/sockets/udp_client
本次实验以 app-wifi-station 工程为基础进行实验,并将工程名改为app-wifi-udp-client。当然也可以直接使用官方例程进行实验,这里使用app-wifi-station工程进行实验是因为为了学习的连贯性,因为在app-wifi-station工程中实现了连接wifi热点;另一方面也是为了学习工程的移植。

1.1.1 向app-wifi-udp-client工程添加udp client相关文件

将参考例程esp-idf/examples/protocols/sockets/udp_client/main文件下的udp_client.c文件复制到app-wifi-udp-client/main文件夹下,修改如下:

  1. udp_client.c 该名为user_udp_client.c,并添加相应的h文件。该步骤依个人习惯。
    在这里插入图片描述
  2. CMakeLists.txt 文件中添加 user_udp_client.c
    在这里插入图片描述
  3. 修改工程名
    在这里插入图片描述

1.1.2 网络调试助手

打开网络调试助手,选择协议类型,查找本地主机地址、本地主机端口号
在这里插入图片描述
在这里插入图片描述

1.1.3 向app-wifi-udp-client工程添加IP相关参数

  1. esp-idf/examples/protocols/sockets/udp_client/main下找到 Kconfig.projbuild文件,将该文件复制并覆盖app-wifi-udp-client/main下的 Kconfig.projbuild文件,该操作的目的是增加 IP Version、IPV4 Address、Port等参数。

  2. 使用命令 idf.py menuconfig 打开配置菜单,在菜单中配置本地IP AddressPort,该参数值对照网络调试助手。最终配置完成后会保存在app-wifi-udp-client文件夹下的sdkconfig文件。
    在这里插入图片描述

或者在Kconfig.projbuild 文件中直接将本地主机地址改为默认地址。

```c
    config EXAMPLE_IPV4_ADDR
        string "IPV4 Address"
        default "192.168.3.113"
        depends on EXAMPLE_IPV4
        help
            IPV4 address to which the client example will send data.
            
    config EXAMPLE_PORT
        int "Port"
        range 0 65535
        default 3333
        help
            The remote port to which the client example will send data.
```

1.1.4 udp client相关代码解析

  1. 实际代码中IP AddressPort参数如下

    #if defined(CONFIG_EXAMPLE_IPV4)
    #define HOST_IP_ADDR CONFIG_EXAMPLE_IPV4_ADDR
    #elif defined(CONFIG_EXAMPLE_IPV6)
    #define HOST_IP_ADDR CONFIG_EXAMPLE_IPV6_ADDR
    #else
    #define HOST_IP_ADDR ""          /*如果使用官网说明手册中 netcat 工具,则需要手动输入主机IP地址*/
    #endif
    #define PORT CONFIG_EXAMPLE_PORT
    

    在app-wifi-udp-client文件夹下的sdkconfig文件可以找到相应的配置
    在这里插入图片描述

  2. user_udp_client.c 代码中,client task执行如下

static void udp_client_task(void *pvParameters)
{
    while (1) {
        /*1. 设置目标UDP server地址、端口等*/
        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(PORT);
        addr_family = AF_INET;
        ip_protocol = IPPROTO_IP;
        /*2. 创建socket*/
        int sock = socket(addr_family, SOCK_DGRAM, ip_protocol);
        while (1) 
		{
            /* 3. 向目标UDP server发送一次数据*/
            int err = sendto(sock, payload, strlen(payload), 0, (struct sockaddr *)&dest_addr, sizeof(dest_addr));
            ESP_LOGI(TAG, "Message sent");
            struct sockaddr_in source_addr; // Large enough for both IPv4 or IPv6
            socklen_t socklen = sizeof(source_addr);
			/*4. 程序会阻塞在此,直到收到UDP server数据*/
            int len = recvfrom(sock, rx_buffer, sizeof(rx_buffer) - 1, 0, (struct sockaddr *)&source_addr, &socklen);
            // Data received
            /*5. 收到目标UDP server数据,并打印*/
            rx_buffer[len] = 0; // Null-terminate whatever we received and treat like a string
            ESP_LOGI(TAG, "Received %d bytes from %s:", len, host_ip);
            ESP_LOGI(TAG, "%s", rx_buffer);
        }
    }
}

代码中使用到函数 socketsendtorecvfrom等。可参考[野火]LwIP应用开发实战指南—基于野火STM32

1.1.4 实验现象分析

在这里插入图片描述其中192.168.1.109属于本地局域网IP

2 传输控制协议(TCP Transmission Control Protocol)简介

  1. TCP是面向连接的运输层协议。 这就是说, 应用程序在使用TCP协议之前, 必须先建立TCP连接。 在传送数据完毕后, 必须释放已经建立的TCP连接。 也就是说, 应用进程之间的通信好像在 “ 打电话”:通话前要先拨号建立连接, 通话结束后要挂机释放连接。
  2. 每一条TCP连接只能有两个端点(endpoint), 每一条TCP连接只能是点对点的(一对一 )。
  3. TCP提供可靠交付的服务。 通过TCP连接传送的数据, 无差错、 不丢失、 不重复,并且按序到达。
  4. TCP 提供全双工通信。 TCP允许通信双方的应用进程在任何时候都能发送数据。 TCP连接的两端都设有发送缓存和接收缓存, 用来临时存放双向通信的数据。 在发送时,应用程序在把数据传送给TCP的缓存后, 就可以做自己的事, 而TCP在合适的时候把数据发送出去。 在接收时, TCP把收到的数据放入缓存, 上层的应用进程在合适的时候读取缓 存中的数据。
  5. 面向字节流。 TCP中的 “ 流” (stream)指的是流入到进程或从进程流出的字节序列。 面向字节流” 的含义是: 虽然应用程序和TCP的交互是一次 一个数据块(大小不等), 但TCP把应用程序交下来的数据仅仅看成是一连串的无结构的字节流。

2.1 ESP32作TCP client,网络调试助手做TCP server

参考例程:examples\protocols\sockets\tcp_client
本次实验以 app-wifi-station 工程为基础进行实验,并将工程名改为app-tcp-client。当然也可以直接使用官方例程进行实验,这里使用app-wifi-station工程进行实验是因为为了学习的连贯性,因为在app-wifi-station工程中实现了连接wifi热点;另一方面也是为了学习工程的移植。

2.2.1 工程移植与配置

后续相关操作参考上文《1.1 UDP作为client进行数据收发实验》

2.2.2 代码分析

查看工程代码,执行流程和《1.1 UDP作为client进行数据收发实验》类似。

2.2.3 实验分析

  1. 程序烧录成功后,需要先开启网络调试助手,再复位ESP32
    在这里插入图片描述在这里插入图片描述

2.2 ESP32作TCP server,网络调试助手作TCP client

参考例程:examples\protocols\sockets\tcp_server
本次实验以 app-wifi-station 工程为基础进行实验,并将工程名改为app-tcp-server。当然也可以直接使用官方例程进行实验,这里使用app-wifi-station工程进行实验是因为为了学习的连贯性,因为在app-wifi-station工程中实现了连接wifi热点;另一方面也是为了学习工程的移植。

2.2.1 工程移植与配置

后续相关操作参考上文《1.1 UDP作为client进行数据收发实验》

2.2.2 代码分析

查看工程代码,

2.2.3 实验分析

  1. 复位后查看ESP32作为TCP server时的IP地址和端口号
    在这里插入图片描述2. 将上一步查到的ESP32作为TCP server时的IP地址和端口号输入网络调试助手
    在这里插入图片描述

3 LwIP函数分析

在上文三个实验中,执行流程较为类似,重点内容在于TCP协议和UDP协议,而代码中用到较多的是套接字(socket),详细内容请参考:

  1. 乐鑫官方资料
  2. [野火]LwIP应用开发实战指南—基于野火STM32

3.1 函数socket

socket这个函数的功能是向内核申请一个套接字

#define socket(domain,type,protocol)   lwip_socket(domain,type,protocol)

int lwip_socket(int domain, int type, int protocol);

#define AF_UNSPEC       0
#define AF_INET         2
#if LWIP_IPV6
#define AF_INET6        10
#else /* LWIP_IPV6 */
#define AF_INET6        AF_UNSPEC

/* Socket 服务类型 (TCP/UDP/RAW) */
#define SOCK_STREAM 1
#define SOCK_DGRAM 2
#define SOCK_RAW 3

参数 domain : 表示该套接字使用的协议簇,对于 TCP/IP 协议来说,IPv4为 AF_INET;IPv6为AF_INET6。
参数 type : 指定了套接字使用的服务类型,可能的类型有 3 种:

  1. SOCK_STREAM:提供可靠的(即能保证数据正确传送到对方)面向连接的 Socket 服务,多
    用于资料(如文件)传输,如 TCP 协议。
  2. SOCK_DGRAM:是提供无保障的面向消息的 Socket 服务,主要用于在网络上发广播信息,
    如 UDP 协议,提供无连接不可靠的数据报交付服务。
  3. SOCK_RAW:表示原始套接字,它允许应用程序访问网络层的原始数据包,这个套接字用
    得比较少,暂时不用理会它。

参数 protocol : 指定了套接字使用的协议,在 IPv4 中,只有 TCP 协议提供 SOCK_STREAM 这种可靠的服务,只有 UDP 协议提供 SOCK_DGRAM 服务,对于这两种协议,protocol 的值均为 0。
当申请套接字成功的时候,该函数返回一个 int 类型的值,也是 Socket 描述符,用户通过这个值
可以索引到一个 Socket 连接结构——lwip_sock,当申请套接字失败时,该函数返回-1。

3.2 函数sendto

这个函数主要是用于 UDP 协议传输数据中,它向另一端的 UDP 主机发送一个 UDP 报文,参数 dataptr 指定了要发送数据的起始地址,而 参数size 则指定数据的长度,参数 flag 指定了发送时候的一些处理,比如外带数据等,此时我们不需要理会它,一般设置为 0 即可,参数 to 是一个指向 sockaddr 结构体的指针,在这里需要我们自己提供远端主机的 IP 地址与端口号,并且用 tolen 参数指定这些信息的长度,具体如下

#define sendto(s,dataptr,size,flags,to,tolen)  lwip_sendto(s,dataptr,size,flags,to,tolen)
ssize_t lwip_sendto(int s, const void *data, size_t size, int flags,const struct sockaddr *to, socklen_t tolen)

3.3 函数read()、recv、recvfrom

ead() 与 recv() 函数的核心是调用 recvfrom() 函数,recv() 与 read() 函数用于从 Socket 中接收数据,它们可以是 TCP 协议和 UDP 协议,具体

#define read(s,mem,len)   lwip_read(s,mem,len)
ssize_t lwip_read(int s, void *mem, size_t len)
{
return lwip_recvfrom(s, mem, len, 0, NULL, NULL);
}

#define recv(s,mem,len,flags)  lwip_recv(s,mem,len,flags)
ssize_t lwip_recv(int s, void *mem, size_t len, int flags)
{
return lwip_recvfrom(s, mem, len, flags, NULL, NULL);
}

#define recvfrom(s,mem,len,flags,from,fromlen)  lwip_recvfrom(s,mem,len,flags,from,fromlen)
ssize_t lwip_recvfrom(int s, void *mem, size_t len, int flags,struct sockaddr *from, socklen_t *fromlen)

men 参数记录了接收数据的缓存起始地址,
len参数 用于指定接收数据的最大长度,如果函数能正确接收到数据,将会返回一个接收到数据的长度,否则将返回-1,若返回值为 0,表示连接已经终止,应用程序可以根据返回的值进行不一样的操作。
flags 参数我们暂时可以直接忽略它,设置为 0 即可。
注意,如果接收的数据大于用户提供的缓存区,那么多余的数据会被直接丢弃。

3.4 函数shutdown

3.5 函数close

close() 函数是用于关闭一个指定的套接字,在关闭套接字后,将无法使用对应的套接字描述符索引到连接结构,该函数的本质是对 netconn_delete() 函数的封装(真正处理的函数是 net-conn_prepare_delete()),如果连接是 TCP 协议,将产生一个请求终止连接的报文发送到对端主机中,如果是 UDP 协议,将直接释放 UDP 控制块的内容,具体如下

#define close(s)   lwip_close(s)
int lwip_close(int s)

3.6 函数connect

connect这个函数用于客户端中,将 Socket 与远端 IP 地址、端口号进行绑定,在 TCP 客户端连接中,调用这个函数将发生握手过程(会发送一个 TCP 连接请求),并最终建立新的 TCP 连接,而对于 UDP协议来说,调用这个函数只是在 UDP 控制块中记录远端 IP 地址与端口号,而不发送任何数据,参数信息与 bind() 函数是一样的

#define connect(s,name,namelen)  lwip_connect(s,name,namelen)
intlwip_connect(int s,const struct sockaddr *name,socklen_t namelen);

3.7 函数send

send() 函数可以用于 UDP 协议和 TCP 连接发送数据。在调用 send() 函数之前,必须使用 connect()函数将远端主机的 IP 地址、端口号与 Socket 连接结构进行绑定。对于 UDP 协议,send() 函数将调用 lwip_sendto() 函数发送数据,而对于 TCP 协议,将调用 netconn_write_partly() 函数发送数据。相对于 sendto() 函数,参数基本是没啥区别的,但无需我们设置远端主机的信息,更加方便操作,因此这个函数在实际中使用也是很多的,具体

#define send(s,dataptr,size,flags)   lwip_send(s,dataptr,size,flags)
ssize_t lwip_send(int s, const void *data, size_t size, int flags)

3.8 函数bind

bind该函数用于服务器端绑定套接字与网卡信息

#define bind(s,name,namelen)   lwip_bind(s,name,namelen)
int lwip_bind(int s,const struct sockaddr *name,socklen_t namelen);

参数 s 是表示要绑定的 Socket 套接字,注意了,这个套接字必须是从 socket() 函数中返回的索引,否则将无法完成绑定操作。
参数 name 是一个指向 sockaddr 结构体的指针,其中包含了网卡的 IP 地址、端口号等重要的信息,LwIP 为了更好描述这些信息,使用了 sockaddr 结构体来定义了必要的信息的字段,它常被用于Socket API 的很多函数中,我们在使用 bind() 的时候,只需要直接填写相关字段即可,sockaddr 结构体如下

struct sockaddr {
  u8_t        sa_len;        /*长度*/
  sa_family_t sa_family;     /*协议簇*/
  char        sa_data[14];   /* 连续的 14 字节信息 */
};

我们需要填写的 IP 地址与端口号等信息,都在 sa_data 连续的 14 字节信息里面,但是这个数据对我们不友好,因此 LwIP还定义了另一个对开发者更加友好的结构体——sockaddr_in,我们一般也是用这个结构体,

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];
};

这个结构体的前两个字段是与 sockaddr 结构体的前两个字段一致,而剩下的字段就是 sa_data 连续的 14 字节信息里面的内容,只不过从新定义了成员变量而已,sin_port 字段是我们需要填写的端口号信息,sin_addr 字段是我们需要填写的 IP 地址信息,剩下 sin_zero 区域的 8 字节保留未用。
但在本章实验中为了兼容IPv6,所以初始定义时都是使用sockaddr_in6结构体,如下所示

struct sockaddr_in6 {
  u8_t            sin6_len;      /* length of this structure    */
  sa_family_t     sin6_family;   /* AF_INET6                    */
  in_port_t       sin6_port;     /* Transport layer port #      */
  u32_t           sin6_flowinfo; /* IPv6 flow information       */
  struct in6_addr sin6_addr;     /* IPv6 address                */
  u32_t           sin6_scope_id; /* Set of interfaces for scope */
};

如果使用的是IPv4则会强制转化为sockaddr_in

struct sockaddr_in6 dest_addr;
if (addr_family == AF_INET)
{
   struct sockaddr_in *dest_addr_ip4 = (struct sockaddr_in *)&dest_addr;
}

3.9 函数listen

该函数只能在 TCP 服务器中使用,让服务器进入监听状态,等待远端的连接请求,LwIP 中可以接收多个客户端的连接,因此参数 backlog 指定了请求队列的大小,具体如下

#define listen(s,backlog)   lwip_listen(s,backlog)
intlwip_listen(int s, int backlog);

参数 s 是表示要绑定的 Socket 套接字,注意了,这个套接字必须是从 socket() 函数中返回的索引,否则将无法完成绑定操作。
参数 backlog指定服务器可连接客户端的数量。

3.10 函数accept

accept() 函数用于 TCP 服务器中,等待着远程主机的连接请求,并且建立一个新的 TCP 连接,在调用这个函数之前需要通过调用 listen() 函数让服务器进入监听状态。accept() 函数的调用会阻塞应用线程直至与远程主机建立 TCP 连接。同时函数返回一个 int 类型的套接字描述符,根据它能索引到连接结构,如果连接失败则返回-1,具体如下

#define accept(s,addr,addrlen)   lwip_accept(s,addr,addrlen)
int  lwip_accept(int s,struct sockaddr *addr,socklen_t *addrlen)

参数 s 是表示要绑定的 Socket 套接字,注意了,这个套接字必须是从 socket() 函数中返回的索引,否则将无法完成绑定操作。
参数 addr 是一个返回结果参数,其实就是远程主机的地址与端口号等信息,当新的连接已经建立后,远端主机的信息将保存在连接句柄中,它能够唯一的标识某个连接对象。

3.11 函数setsockopt

看名字就知道,这个函数是用于设置套接字的一些选项的,参数 level 有多个常见的选项,如:
• SOL_SOCKET:表示在 Socket 层。
• IPPROTO_TCP:表示在 TCP 层。
• IPPROTO_IP:表示在 IP 层。
参数 optname 表示该层的具体选项名称,比如:

  1. 对于 SOL_SOCKET 选项,可以是 SO_REUSEADDR(允许重用本地地址和端口) 、
    SO_SNDTIMEO(设置发送数据超时时间)、SO_SNDTIMEO(设置接收数据超时时间)、
    SO_RCVBUF(设置发送数据缓冲区大小)等等。
  2. 对于 IPPROTO_TCP 选项,可以是 TCP_NODELAY(不使用 Nagle 算法)、TCP_KEEPALIVE
    (设置 TCP 保活时间)等等。
  3. 对于 IPPROTO_IP 选项,可以是 IP_TTL(设置生存时间)、IP_TOS(设置服务类型)等等。
#define setsockopt(s,level,optname,opval,optlen) \
lwip_setsockopt(s,level,optname,opval,optlen)
int
lwip_setsockopt(int s,
int level,
int optname,
const void *optval,
socklen_t optlen)
  • 2
    点赞
  • 39
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
以下是Arduino ESP32自动重连WiFi和TCP的代码示例: ```c++ #include <WiFi.h> #include <WiFiClient.h> #include <WiFiMulti.h> #include <WiFiClientSecure.h> #include <ESPmDNS.h> #include <WiFiUdp.h> #include <ArduinoJson.h> const char* ssid = "your_SSID"; const char* password = "your_PASSWORD"; const char* host = "your_host"; const int port = 8080; WiFiMulti wifiMulti; WiFiClient client; void setup() { Serial.begin(115200); wifiMulti.addAP(ssid, password); // 添加WiFi网络 } void loop() { if ((wifiMulti.run() == WL_CONNECTED) && (client.connected() == false)) { // 检查是否连接WiFi和TCP Serial.println("WiFi connected"); Serial.println("Connecting to server..."); if (client.connect(host, port)) { // 连接TCP服务器 Serial.println("TCP connected"); } else { Serial.println("TCP connection failed"); } } if (client.connected() && client.available()) { // 检查TCP连接和数据 String response = client.readStringUntil('\n'); Serial.println(response); } if (!client.connected()) { // 若TCP连接断开,重连 client.stop(); delay(1000); if (wifiMulti.run() == WL_CONNECTED) { client.connect(host, port); } } } ``` 该代码使用WiFiMulti库添加WiFi网络并检查连接状态,使用WiFiClient库连接TCP服务器并检查连接状态和数据,如果连接中断,则使用client.stop()断开连接并等待1秒钟,然后尝试重新连接。注意,该代码仅适用于单个TCP连接。如果需要处理多个TCP连接,则需要使用WiFiClient类的数组。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值