基于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_newtcp_bindtcp_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如下:

  1. 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结构体

  2. netconn_delete

    err_t netconn_delete(struct netconn * conn)
    

    该函数用于删除netconn结构体并释放内存

    注意:当客户端断开连接后用户一定要调用该函数并释放netconn资源,否则可能会引起内存泄漏

  3. netconn_bind

    用于绑定netconn结构体的IP地址和端口号,使用方法和socket的bind类似

    err_t netconn_bind(struct netconn * conn, ip_addr_t * addr, u16_t port)
    
  4. 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表示连接队列的最大限制

    该函数用于监听客户端连接

  5. 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介绍过,不再赘述

  6. netconn_accept

    err_t netconn_accept(struct netconn * conn, struct netconn ** new_conn)
    

    该函数将产生一个新的netconn结构体指针赋给new_conn参数,后续客户端的数据收发都应使用新的结构体指针

    服务器使用这个函数来接受新的客户端连接请求

  7. netconn_recv

    用于从网络中接收数据

    err_t netconn_recv(struct netconn * conn, struct netbuf ** new_buf)
    

    其中new_buf是用来指向接收数据的网络缓存区指针,这个指针指向struct netbuf结构体指针

    使用的时候应先初始化一个指针并将其放到new_buf参数的位置,使用这个函数以后,所有接收到的网络数据都会被放在这个指针中,只要使用这个指针(一般称为句柄)配合相关函数即可实现数据接收

  8. netbuf_data

    使用以下函数从netbuf结构体中获取指定长度的数据

    err_t netbuf_data(struct netbuf ** buf, void ** dataptr, u16_t * len)
    

    buf表示指定要获取数据的netbuf结构体句柄

    dataptr表示获取数据后存放的缓存

    len表示要获取的数据长度

  9. 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

  10. 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编辑  收藏  举报