基于LwIP的IoT上行数据处理
处理IoT上传数据
LwIP
LwIP即Light Weight IP协议,可以在无操作系统或在RTOS的情况下运行,资源占用仅达几十KB RAM、40KB ROM,适合在嵌入式设备中运行,现在一般的物联网设备都在使用LwIP协议,它的基本功能如下:
- 支持多网口IP转发
- 支持ICMP、DHCP
- 支持扩展UDP
- 支持阻塞控制、RTT估算和快速转发的TCP
- 提供回调接口(RAW API)
- 支持可选的Berkeley接口API
LwIP的实现非常复杂,但目前代码开源且可以较方便地在各种设备中移植,很多设备厂商也都提供了LwIP的模板和库,所以这里主要介绍LwIP的驱动编写和应用
一个LwIP设备通常需要以下层次结构支持:
-
物理层
包括PHY芯片、实现网络连接的接口与基础电路
-
数据链路层
- MAC层:硬件设备的以太网控制器
- 驱动层:以太网设备驱动
-
网络IP层与传输层
主要由LwIP核心代码构成
LwIP会自行基于硬件API构建TCP/IP协议API,并实现通用的应用程序API
-
应用层
网络应用程序
LwIP的基本API
LwIP提供三种API
-
RAW API:无需操作系统就能使用的API,该接口把协议栈和应用程序放在同一个进程,使用状态机和回调函数进行控制,使用这一接口的应用程序无需连续操作
类似ESP32和STM32这样的SoC利用内置/外置网卡驱动(WiFi基带)就可以基于RAW API实现网络通信
-
NETCONN API:需要操作系统支持,将接收和处理放在同一线程的API,效率略低于RAW API,但软件兼容性更好一些
-
BSD API:基于UNIX的POSIX和“万物皆文件”思想的标准AP,方便应用程序移植,但是因为在嵌入式系统中效率较低且占用资源较多而较少使用。现在的RT-Thread能够提供兼容该API的软件移植包,但是需要跑在STM32H750这样的中高性能嵌入式设备上才能实现流畅运行
RAW API
该API的最大特点就是效率高但编程难度高,需要开发者熟悉函数回调机制
它的最基本单位是PCB(协议控制块,Protocol Control Block,注意不是Print Circuit Board!),在功能上类似socket
根据传输协议,PCB分为TCP PCB和UDP PCB两种
它的对应API如下所示
//创建一个新的TCP PCB
struct tcp_pcb* tcp_new(void)
{
return tcp_alloc(TCP_PRIO_NORMAL);
}
//绑定PCB到本地端口号和IP地址
err_t tcp_bind(struct tcp_pcb * pcb, struct ip_addr * ipaddr, u16_t port);
//使用IP_ADDR_ANY可以将PCB绑定到任何本地地址
//监听PCB
#define tcp_listen(pcb) tcp_listen_with_backlog(pcb,TCP_DEFAULT_LISTEN_BACKLOG)
struct tcp_pcb * tcp_listen_with_backlog(struct tcp_pcb *pcb, u8_t backlog);
//该函数会返回一个新的已处于监听状态的TCP PCB,原始的PCB则会被释放
//所以一定要像tcpb=tcp_listen(tcpb);这样使用这个函数
//连接服务器
err_t tcp_connect(struct tcp_pcb * pcb, struct ip_addr * ipaddr, u16_t port,
err_t (* connected)(void* arg, struct tcp_pcb * tpcb, err_t err));
/*
struct tcp_pcb * pcb表示需要设置的TCP PCB
struct ip_addr * ipaddr表示服务器的IP地址,可以使用以下方法设置IP:
1. 定义一个struct ip_addr * ipaddr结构体
2. 使用IP4_ADDR(&ipaddr,a,b,c,d)设置IP,其中a、b、c、d是IPv4地址的中间几个数字
比如192.168.1.2对应的就是a=192,b=168,c=1,d=2
u16_t port表示服务器端口号
err_t (* connected)(void* arg, struct tcp_pcb * tpcb, err_t err)表示连接成功时的回调函数,开发者应该在其中使用状态机来实现对连接状态的控制
*/
//设置有连接请求时的回调函数
void tcp_accept(struct tcp_pcb * pcb,
err_t (* accept)(void* arg, struct tcp_pcb * newpcb, err_t err))
{
pcb->accept = accept;
}
/* 其中的accept就表示回调函数,回调函数应该像下面这样 */
static err_t tcp_server_accept_handler(void* arg, struct tcp_pcb * newpcb, err_t err)
{
//设置回调函数优先级,当存在多个连接时非常重要,该函数必须被调用
tcp_setprio(pcb, TCP_PRIO_MIN);
//设置TCP数据接收回调函数,当有网络数据时,tcp_server_recv会被调用
tcp_recv(pcb,tcp_server_recv);
err = ERR_OK; //这里是函数的执行结果,一般采用if语句判断上述函数是否成功执行
return err;
}
//其中tcp_recv用于设置TCP数据接收回调函数
void tcp_recv(struct tcp_pcb * pcb,
err_t (* recv)(void * arg, struct tcp_pcb * tpcb, struct pbuf * p, err_t err))
{
pcb->recv = recv;
}
//回调函数多用于缓存网络数据,典型的TCP数据接收回调函数如下所示
static err_t tcp_server_recv(void * arg, struct tcp_pcb * tpcb, struct pbuf * p, err_t err)
{
//定义一个pbuf指针,指向传入的参数p,用于接收网络数据并缓存到本地
struct pbuf * p_temp = p;
//如果数据非空
if(p_temp != NULL)
{
tcp_recved(pcb, p_temp->tot_len);//读取数据
//如果数据非空,采用遍历链表的方式遍历数据(实际上底层就是个链表)
while(p_temp != NULL)
{
//把接收到的数据重新发送给客户端
tcp_write(pcb, p_temp->payload, p_temp->len, TCP_WRITE_FLAG_COPY);
//启动发送
tcp_output(pcb);
//获取下一个数据包
p_temp = p_temp->next;
}
}
else //数据为空则表示接收失败
{
tcl_close(pcb); //关闭连接
}
pbuf_free(p); //释放内存
err = ERR_OK;
return err;
}
RAW API的编程思路和Socket很相似,但是其中使用回调函数而不是顺序结构来对数据流进行控制
tcp_new
、tcp_bind
、tcp_listen
使用方法和socket的对应函数是基本一致的;但是tcp_accept
需要对应设置一个tcp_server_accept_handler
回调函数,并再通过tcp_recv
设置tcp_server_recv_handler
回调函数才能完成Socket中的accept和receive完成的功能,这是因为LwIP将所有任务放在同一个进程,没有操作系统通过事件集、信号量等方法在待接收和已接收FIFO中进行数据传递。
NETCONN API
在这个接口中,使用netconn作为连接结构,不区分TCP、UDP,方便应用程序使用统一的连接结构和编程函数
查阅代码可以了解netconn连接结构体实际上是对PCB的上层封装,将较为底层的PCB结构抽象成了一个“对象”
struct netconn{
/* netconn类型,可使用TCP、UDP、RAW */
enum netconn_type type;
/* netconn当前状态 */
enum netconn_state state;
/* LwIP内部协议控制块,本质上是在PCB的基础上进行封装 */
union {
struct ip_pcb * ip;
struct tcp_pcb * tcp;
struct udp_pcb * udp;
struct raw_pcb * raw;
} pcb;
/* netconn最后一个错误 */
err_t last_err;
#if !LWIP_NETCONN_SEM_PER_THREAD
/* 用于在内核上下文同步执行的功能 */
sys_sem_t op_completed;
#endif
/* mbox:接收包的mbox,直到他们被netconn应用程序线程获取(可以变得非常大) */
sys_mbox_t recvmbox;
#if LWIP_TCP
sys_mbox_t acceptmbox;
#endif /* LWIP_TCP */
/* 仅用于套接字层,通常不使用的封装 */
#if LWIP_SOCKET
int socket;
#endif /* LWIP_SOCKET */
#if LWIP_SO_SNDTIMEO
/* 超时等待发送数据,以毫秒为间隔(将数据以内部缓冲区的形式发送) */
s32_t send_timeout;
#endif /* LWIP_SO_SNDTIMEO */
#if LWIP_SO_RCVTIMEO
/* 超时等待接收新数据,以毫秒为间隔(或连接到侦听netconns的连接) */
int recv_timeout;
#endif /* LWIP_SO_RCVTIMEO */
#if LWIP_SO_RCVBUF
/* recvmbox中排队的最大字节数,如果未用于TCP应该为调整TCP_WND */
int recv_bufsize;
/* 当前在recvmbox中要接收的字节数,针对recv_bufsize测试以限制recvmbox上的字节;用于UDP和RAW,用于FIONREAD */
int recv_avail;
#endif /* LWIP_SO_RCVBUF */
#if LWIP_S_LINGER
/* 值小于0表示禁用延迟,值大于0表示延迟的秒数 */
s16_t linger;
#endif /* LWIP_S_LINGER */
u8_t flags;//更多netconn内部状态的标志
#if LWIP_TCP
/* TCP:当传递给netconn_write的数据不适合发送缓冲区时,暂时存储已发送的数量 */
size_t write_offset;
/* TCP:当传递给netconn_write的数据不适合发送缓冲区时,此时暂时存储消息,在连接和关闭期间也使用 */
struct api_msg * current_msg;
#endif /* LWIP_TCP */
netconn_callback callback;//通知此netconn事件的回调函数
}
用户可用的API如下:
-
netconn_new
#define netconn_new(t) netconn_new_with_proto_and_callback(t, 0, NULL) struct netconn * netconn_new_with_proto_and_callback(enum netconn_type t, u8_t proto, netconn_callback callback);
其中netconn_type是创建的连接类型,通常使用TCP或者UDP,其取值可以是枚举中的任何一个
NETCONN_INVALID = 0 NETCONN_TCP = 0x10 NETCONN_UDP = 0x20 NETCONN_UDPLITE = 0x21 NETCONN_UDPNOCHKSUM = 0x22 NETCONN_RAW = 0x40
proto则表示原始RAW IP pcb的IP,通常写0即可
netconn_cakkback为设置状态发生改变时的回调函数,通常直接填NULL即可
这个API返回一个netconn结构体
-
netconn_delete
err_t netconn_delete(struct netconn * conn)
该函数用于删除netconn结构体并释放内存
注意:当客户端断开连接后用户一定要调用该函数并释放netconn资源,否则可能会引起内存泄漏
-
netconn_bind
用于绑定netconn结构体的IP地址和端口号,使用方法和socket的bind类似
err_t netconn_bind(struct netconn * conn, ip_addr_t * addr, u16_t port)
-
netconn_listen
#define netconn_listen(conn) netconn_listen_with_backlog(conn,TCP_DEFAULT_LISTEN_BACKLOG) err_t netconn_listen_with_backlog(struct netconn * conn, u8_t backlog)
backlog表示连接队列的最大限制
该函数用于监听客户端连接
-
netconn_connect
该函数用于客户端向服务器发起连接
err_t netconn_connect(struct netconn * conn, const ip_addr_t * adr, u16_t port)
const ip_addr_t * adr表示服务器的IP地址,使用
IP4_ADDR
函数设置IP,该函数在上面的RAW API介绍过,不再赘述 -
netconn_accept
err_t netconn_accept(struct netconn * conn, struct netconn ** new_conn)
该函数将产生一个新的netconn结构体指针赋给new_conn参数,后续客户端的数据收发都应使用新的结构体指针
服务器使用这个函数来接受新的客户端连接请求
-
netconn_recv
用于从网络中接收数据
err_t netconn_recv(struct netconn * conn, struct netbuf ** new_buf)
其中new_buf是用来指向接收数据的网络缓存区指针,这个指针指向struct netbuf结构体指针
使用的时候应先初始化一个指针并将其放到new_buf参数的位置,使用这个函数以后,所有接收到的网络数据都会被放在这个指针中,只要使用这个指针(一般称为句柄)配合相关函数即可实现数据接收
-
netbuf_data
使用以下函数从netbuf结构体中获取指定长度的数据
err_t netbuf_data(struct netbuf ** buf, void ** dataptr, u16_t * len)
buf表示指定要获取数据的netbuf结构体句柄
dataptr表示获取数据后存放的缓存
len表示要获取的数据长度
-
netconn_write
#define netconn_write(conn, dataptr, size, apiflags) \ netconn_write_partly(conn, dataptr, size, apiflags, NULL) err_t netconn_write_partly(struct netconn * conn, const void * dataptr, size_t size, u8_t apiflags, size_t * bytes_written)
使用该函数来向网络发送数据
其中apiflags可选以下参数:
- NETCNN_COPY:数据将被复制到属于堆栈的内存中
- NETCONN_MORE:对于TCP连接,将在发送的最后一个数据段上设置PSH标志
- NETCONN_DONTBLOCK:仅在可以一次写入所有数据时才会写入数据
dataptr表示要发送的数据缓存区,size表示数据长度,可以用sizeof(dataptr)
bytes_writing表示指向接收写入字节数的位置的指针,通常置为NULL
-
netconn_close
该函数用于关闭连接,不再赘述
err_t netconn_close(struct netconn * conn)
BSD API
LwIP提供了一套基于open-read/write-close模型的UNIX标准API,实现了socket、bind、recv、send等API,但是由于BSD API接口需要占用过多资源,在嵌入式设备中很少使用
基本使用方式和Socket标准API一样,不再赘述
posted on 2021-11-11 14:00 redlightASl 阅读(401) 评论(0) 编辑 收藏 举报