四、LwIP的API的实现
LwIP的API的实现主要有两部分组成:一部分驻留在用户进程中,一部分驻留在TCP/IP协议栈进程中。这两个部分间通过操作系统模拟层提供的进程通信机制(IPC)进行通信,从而完成用户进程与协议栈间的通信,IPC包括共享内存、消息传递和信号量。通常尽可能多的工作在用户进程内的API部分实现,例如运算量及时间开销大的工作;TCP/IP协议栈进程中的API部分只完成少量的工作,主要是完成进程间的通讯工作。两部分API之间通过共享内存传递数据,对于共享内存区的描述是采用和pbuf类似的结构来实现。综上,可以用简单的一段话来描述这种API实现的机制:API 函数库中处理网络连接的函数驻留在 TCP/IP 进程中。位于应用程序进程中的API函数使用邮箱这种通讯协议向驻留在TCP/IP 进程中的API函数传递消息。这个消息包括需要协议栈执行的操作类型及相关参数。驻留在 TCP/IP 进程中的API函数执行这个操作并通过消息传递向应用程序返回操作结果。
很早以前描述过结构pbuf,它是协议栈内部用来描述数据包的一种方式。这里介绍数据结构netbuf,它是API用来描述数据的一种方式。netbuf是基于pbuf来实现的,其结构如下所示,很明显,它就是包含了几个典型结构的指针。
struct netbuf {
struct pbuf *p, *ptr;
struct ip_addr *addr;
u16_t port;
};
注意,这里的netbuf只是相当于一个数据头,而真正保存数据的还是字段p指向的pbuf链表。字段ptr也是指向该netbuf的pbuf链表,但与p的区别在于:p一直指向pbuf链表中的第一个pbuf结构,而ptr则不然,它可能指向链表中的其他位置,源文档里面把它描述为fragment pionter,与该指针调整密切相关的函数是netbuf_next和netbuf_first。还有两个字段addr和port分别表示发出netbuf数据端的IP地址和端口号,这两个字段实际用处似乎不大,API定义了宏netbuf_fromaddr和netbuf_fromport分别用于返回某个netbuf结构中这两个字段的值。
与netbuf相关的处理函数很多,在源文档15.2节中对每个函数也有了详细的说明,这里说说比较重要的几个。netbuf_new用于分配一个新的netbuf结构,注意这里只是一个头部结构,而真正需要的存储数据区域是在函数netbuf_alloc中分配的,同理函数netbuf_delete用于删除一个netbuf结构,同时函数pbuf_free会被调用,用以删除数据区域的空间。以上这几个函数使用的简单例子如下:
struct netbuf *buf; // 申明指针
buf = netbuf_new(); // 申请新的netbuf
netbuf_alloc(buf, 200); // 为netbuf分配200 字节的空间
。。。。 // 使用buf做相关事情
netbuf_delete(buf); // 删除buf
讲过了API的内存管理,再来讲具体的API函数。与前面的对应,API函数由两部分组成,分别在文件api_lib.c和api_msg.c中。前者包括了用户程序直接调用的API接口函数,后者包括了与协议栈进程通信的API函数,用户程序不可直接调用。这两部分API函数之间通过邮箱传递的消息进行通信。这里又要涉及到两个重要的数据结构:API应用接口部分提供给上层应用以描述一个网络连接的数据结构netconn,以及描述两部分API函数间传递的消息结构的api_msg。
应用程序要使用API函数建立一个连接连接,首先应该建立一个netconn的结构来描述这个连接的各种属性。netconn结构如下,其中去掉了不必要的编译选项和注释。
struct netconn {
enum netconn_type type; // 连接的类型,包括TCP, UDP等
enum netconn_state state; // 连接的状态
union { // 共用体,内核用来描述某个连接
struct ip_pcb *ip;
struct tcp_pcb *tcp;
struct udp_pcb *udp;
struct raw_pcb *raw;
} pcb;
err_t err; // 该连接最近一次发生错误的编码
sys_sem_t op_completed; // 用于两部分API间同步的信号量
sys_mbox_t recvmbox; // 接收数据的邮箱
sys_mbox_t acceptmbox; // 服务器用来接受外部连接的邮箱
int socket; // 该字段只在socket实现中使用
u16_t recv_avail; //
struct api_msg_msg *write_msg; // 对数据不能正常处理时,保存信息
int write_offset; // 同上,表示已经处理数据的多少
netconn_callback callback; // 回调函数,在发生与该netconn相关的事件时可以调用
};
write_msg是api_msg_msg类型的指针,该结构后续讲到;netconn_callback是API定义的一种函数指针类型,其定义为:
typedef void (* netconn_callback)(struct netconn *, enum netconn_evt, u16_t len);
关键字typedef的功能是定义新的类型。这里它用于定义一种netconn_callback的类型,这种类型为指向某种函数的指针,且这种函数有三个类型的输入参数,返回类型为void。在这定义以后就可以像使用int,char一样使用netconn_callback,也即它也表示一种数据类型了。所以上面的callback字段表示一个函数指针,当把某个函数名赋给该字段后,该字段就记录了这个函数的起始地址。
netconn的其他字段后面用到时再详解。接下来看看两部分API函数间传递的消息结构的api_msg:
struct api_msg {
void (* function)(struct api_msg_msg *msg); // 函数指针
struct api_msg_msg msg; // 函数执行时需要的参数
}
字段function是一个函数指针,通常当消息被构造时,该字段被填充为api_msg.c中的某个与协议栈接口的API函数,然后该消息被投递到协议栈进程中进行处理,协议栈进程解析出并执行该函数,这样应用程序与协议栈之间的通信就完成了。字段msg中包含了这个函数执行时需要的所有参数,api_msg_msg是个枚举类型,所以对于不同的function,msg有着不同的结构。api_msg_msg结构如下所示:
struct api_msg_msg {
struct netconn *conn; // 与消息相关的某个连接
union {
struct netbuf *b; // 函数do_send的参数
struct { // 函数do_newconn的参数
u8_t proto;
} n;
struct { // 函数do_bind和do_connect的参数
struct ip_addr *ipaddr;
u16_t port;
} bc;
struct { // 函数do_getaddr的参数
struct ip_addr *ipaddr;
u16_t *port;
u8_t local;
} ad;
struct { // 函数do_write的参数
const void *dataptr;
int len;
u8_t apiflags;
} w;
struct { // 函数do_recv的参数
u16_t len;
} r;
} msg;
};
这个结构体只包含了两个字段:描述连接信息的conn和枚举类型msg。在api_msg_msg中保存conn字段是必须的,因为conn结构中包含了与该连接相关的信箱和信号量等信息,协议栈进程要用这些信息来完成与应用进程间的同步与通信。枚举类型msg的各个成员与调用它的函数密切相关。因此在构建一个消息时,API应首先填充消息的function字段,并针对特定的function种类往api_msg_msg的枚举字段写入参数,函数function被协议栈解析执行时,直接按照自己已定的格式取参数。这意味着,对于构造消息的API函数和解析消息的function函数,它们对参数个数、结构的认识是预先约定的,不能有其他变数。这一点体现出LwIP呆板僵硬的一面。
评论
查看更多