使用 NETCONN 接口编程
NETCONN API 使用了操作系统的 IPC 机制, 对网络连接进行了抽象,用户可以像操作文件一样操作网络连接(打开/关闭、读/写数据)。 但是 NETCONN API 并不如操作文件的 API 那样简单易用。举个例子,调用 f_read 函数读文件时,读到的数据会被放在一个用户指定的数组中,用户操作起来很方便,而 NETCONN API 的读数据 API,就没有那么人性化了。 用户获得的不是一个数组,而是一个特殊的数据结构 netbuf,用户如果想使用好它,就需要对内核的 pbuf 和 netbuf 结构体有所了解。
netbuf 结构体:
LwIP 为了更好描述应用线程发送与接收的数据,并且为了更好管理这些数据的缓冲区,LwIP 定义了一个 netbuf 结构体,它是基于 pbuf 上更高一层的封装,记录了主机的 IP 地址与端口号。
struct netbuf
{
struct pbuf *p, *ptr; (1)
ip_addr_t addr; (2)
u16_t port; (3)
};
netbuf 相关函数说明:
netbuf 是 LwIP 描述用户数据很重要的一个结构体,因为 LwIP 是不可能让我们直接操作 pbuf 的,因为分层的思想,应用数据必然是由用户操作的, 因此 LwIP 会提供很多函数接口让用户对 netbuf 进行操作,无论是 UDP 报文还是 TCP 报文段,其本质都是数据,要发送出去的数据都会封装在 netbuf 中,然后通过邮箱发送给内核线程(tcpip_thread 线程),然后经过内核的一系列处理,放入发送队列中,然后调用底层网卡发送函数进行发送,反之,应用线程接收到数据,也是通过 netbuf 进行管理。
netconn 结构体:
在 LwIP 中,如 TCP 连接, UDP 通信,都是需要提供一个编程接口给用户使用的,那么为了描述这样子的一个接口, LwIP 抽象出来一个 nettonn 结构体,它能描述一个连接,供应用程序使用,同时内核的 NETCONN API 接口也对各种连接操作函数进行了统一的封装,这样子,用户程序可以很方便使 netconn 和编程函数,我们暂且将 netconn 称之为连接结体。一个连接结构体中包含的成员变量很多,如描述连接的类型,连接的状态(主要是在TCP 连接中使用),对应的控制块(如 UDP 控制块、 TCP 控制块等等),还有对应线程的消息邮箱以及一些记录的信息。
struct netconn
{
/** netconn 类型 */
enum netconn_type type;
/** 当前 netconn 状态 */
enum netconn_state state;
/** LwIP 的控制块指针,如 TCP 控制块、 UDP 控制块 */
union
{
struct ip_pcb *ip;
struct tcp_pcb *tcp;
struct udp_pcb *udp;
struct raw_pcb *raw;
} pcb;
err_t pending_err;/** 这个 netconn 最后一个异步未报告的错误 */
sys_sem_t op_completed; //信号量
/** 消息邮箱,存储接收的数据,直到它们被提取 */
sys_mbox_t recvmbox;
/** 用于 TCP 服务器上的请求连接缓冲区 */
sys_mbox_t acceptmbox;
/** socket 描述符,用于 Socket API */
#if LWIP_SOCKET
int socket;
#endif /* LWIP_SOCKET */
/** 标志 */
u8_t flags;
#if LWIP_TCP
/** 当调用 netconn_write()函数发送的数据不适合发送缓冲区时,
数据会暂时存储在 current_msg 中,等待数据合适的时候进行发送 */
struct api_msg *current_msg;
#endif /* LWIP_TCP */
/** 连接相关的回调函数 */
netconn_callback callback;
};
netconn 函数接口说明:
netconn_new()
函数 netconn_new ()本质上是一个宏定义,它用来创建一个新的连接结构, 连接结构的类型可以选择为 TCP 或 UDP 等,参数 type 描述了连接的类型,可以为 NETCONN_TCP或 NETCONN_UDP 等, 在这个函数被调用时,会初始化相关的字段,而并不会创建连接。
netconn_bind()
netconn_bind()函数用于将一个 IP 地址及端口号与 netconn 连接结构进行绑定,如果作为服务器端,这一步操作是必然需要的,作为客户端,不需要这一步,系统会自动分配端口号,使用默认网卡发送数据。同样的, 该函数会调用 netconn_apimsg()函数构造一个 API 消息,并且请求内核执行 lwip_netconn_do_bind()函数, 然后通过 netconn 连接结构的信号量进行同步,事实上内核线程的处理也是通过函数调用 xxx_bind(xxx_bing 可以是 udp_bing、 tcp_bing、 raw_bing,具体是哪个函数内核是根据 netconn 的类型决定的) 完成相应控制块的绑定工作。
netconn_connect()
netconn_connect()函数是用于连接服务器的函数,它一般在客户端中调用,将服务器端的 IP 地址和端口号与本地的 netconn 连接结构绑定,当 TCP 协议使用该函数的时候就是进行握手的过程,调用的应用线程将阻塞至握手完成;而对于 UDP 协议来说,调用该函数只是设置 UDP 控制块的目标 IP 地址与目标端口号,其实这个函数也是通过调用netconn_apimsg()函数构造一个 API 消息,并且请求内核执行 lwip_netconn_do_connect()函数, 然后通过 netconn 连接结构的信号量进行同步,在 lwip_netconn_do_connect()函数中,根据 netconn 的类型不同, 调用对应的 xxx_connect()函数进行对应的处理,如果是 TCP 连接,将调用 tcp_connect();如果是 UDP 协议,将调用 udp_connect();如果是RAW,将调用 raw_connect()函数处理。
netconn_recv()
它可以接收一个 UDP 或者 TCP的数据包,从 recvmbox 邮箱中获取pbuf数据包,如果该邮箱中没有数据包,那么线程调用这个函数将会进入阻塞状态以等待消息的到来, 如果在等待 TCP 连接上的数据时,远端主机终止连接,将返回一个终止连接的错误代码(ERR_CLSD),应用程序可以根据错误的类型进行不一样的处理。对应 TCP 连接, netconn_recv()函数将调用 netconn_recv_data_tcp()函数去获取 TCP 连接上的数据,在获取数据的过程中,调用 netconn_recv_data()函数从 recvmbox 邮箱获取pbuf, 然后通过 netconn_tcp_recvd_msg()->netconn_apimsg()函数构造一个 API 消息投递给系统邮箱, 请求内核执行 lwip_netconn_do_recv()函数, 该函数将调用 tcp_recved()函数去更新 TCP 接收窗口,同时netconn_recv()函数将完成 pbuf 数据包封装在 netbuf 中,返回个应用程序; 而对于 UDP 协议、 RAW 连接,将简单多了,将直接调用netconn_recv_data()函数获取数据,完成 pbuf 封装在 netbuf 中,返回给应用程序。
netconn_send()
该函数会调用 netconn_apimsg()函数构造一个 API 消息,并且请求内核执行 lwip_netconn_do_send()函数, 这个函数会通过消息得到目标 IP 地址与端口号以及 pbuf 数据报等信息, 然后调用 raw_send()/udp_send()等函数发送数据,最后通过 netconn 连接结构的信号量进行同步。
netconn_write()
用于处于稳定连接状态的 TCP 协议发送数据,这个函数的功能是把 dataptr 指针指向的数据放在属于 conn 连接的 TCP 连接的发送队列中, size 参数指定了数据的长度, apiflags 参数有以下几种:
/* 没有标志位(默认标志位) */
#define NETCONN_NOFLAG 0x00
/* 不拷贝数据到内核线程 */
#define NETCONN_NOCOPY 0x00
/* 拷贝数据到内核线程 */
#define NETCONN_COPY 0x01
/* 尽快递交给上层应用 */
#define NETCONN_MORE 0x02
/* 当内核缓冲区满时,不会被阻塞,而是直接返回 */
#define NETCONN_DONTBLOCK 0x04
/* 不自动更新接收窗口,需要调用 netconn_tcp_recvd()函数完成 */
#define NETCONN_NOAUTORCVD 0x08
/* 上层已经收到数据,将 FIN 保留在队列中直到再次调用 */