lwIP RAW TCP/IP接口
作者: Adam Dunkels, Leon Woestenberg, Christiaan Simons
lwIP为使用TCP/IP协议通信的应用程序编程提供了两种接口接口(APIs):
* 低层次的称之为"core" / "callback" 或者 "raw" API
* 高层次的称之为"sequential" API
lwIP "sequential" API为使用TCP/IP协议栈编程提供符合常规的、通用的途径,它与BSD socket API非常相似。程序的执行过程同样是基于"open-read-write-close"模型的。从本质上讲,TCP/IP协议栈的通信过程是事件驱动的,因此,TCP/IP的代码和用户应用程序的代码必须在不同的线程里面。
**以下内容讨论"RAW" API**
RAW IP允许应用程序和TCP/IP代码紧密结合,程序的执行是基于在TCP/IP内核中被调用的回调函数事件驱动的。TCP/IP内核和应用程序可以运行在同一线程。lwIP "sequential" API接口会消耗大量的CPU资源,它并不适用于小型嵌入式系统,因为它必须运行在多线程环境中。
RAW API不仅执行速度快,而且消耗的内存资源更少。它的缺点是应用程序编写相对较难并不易理解。尽管如此,这种方式仍是资源较少的嵌入式系统的首选方法。
不同的应用程序中我们可以同时使用这两种APIs,实际上"sequential" API就是由RAW API封装后得到的。
什么是回调函数?
RAW API是基于回调函数所驱动的。每一个回调函数实际上只是一个普通的C函数,这个函数在TCP/IP内核中被调用。每一个回调函数都作为一个参数传递给当前TCP或UDP连接。而且,为了能够保存程序的特定状态,可以向回调函数传递一个指定的状态,并且这个指定的状态是独立于TCP/IP协议栈的。
--应用程序设置状态函数
- void tcp_arg(struct tcp_pcb *pcb, void *arg)
指定传给所有回调函数的特定状态参数。参数"pcb"指当前TCP连接控制块,"arg"指传递给回调函数的参数。
--TCP连接函数
这些函数用于建立连接,它们与"sequential" API以及BSD socket API非常相似。使用tcp_new()函数建立一个新的TCP标识符(也就是协议控制块-PCB)。这个PCB可以用来监听一个外来的连接(译注:作为服务器)也可以连接到另一个主机(译注:作为客户端)。
- struct tcp_pcb *tcp_new(void)
创建一个新的连接标识符(PCB)。如果没有有效的存储空间创建这个新的pcb,返回NULL。
译注:这个函数创建一个TCP协议控制块,但并不把它放到任何TCP PCB列表,直到使用tcp_bind()函数绑定。Tcp_new()函数会调用tcp_alloc函数来动态申请一块内存并初始化它,之后将这块内存的首地址返回给tcp_new()函数,如果动态内存不成功的话返回NULL。
- err_t tcp_bind(struct tcp_pcb *pcb, struct ip_addr *ipaddr,
u16_t port)
给pcb绑定一个本地IP地址和端口号。如果参数"ipaddr"为IP_ADDR_ANY,则为这个pcb绑定任意本地IP地址。
译注:这个函数的大部分代码用于检验给出的IP地址和端口号是否合适,如果合适则将给出的IP地址和端口号赋给当前PCB,更新已绑定tcp_pcb列表并返回ERR_OK.如果给出的参数不合适,则返回ERR_USE(表示端口已被使用)。
参数ipaddr如果为IP_ADDR_ANY,表示绑定到任意本地地址,那么IP_ADDR_ANY是什么呢?在lwip-1.3.0/src/include/ipv4/lwip/ip_addr.h中定义了:
#define IP_ADDR_ANY ((struct ip_addr *)&ip_addr_any)
ip_addr_any是一个ip_addr型变量,在lwip-1.3.0/src/core/ipv4/ip_addr.c中有如下声明:
#define IP_ADDR_ANY_VALUE 0x00000000UL
const struct ip_addr ip_addr_any = { IP_ADDR_ANY_VALUE };
所以, IP_ADDR_ANY是等于0x00000000UL的. 在IP地址上规定 0.0.0.0为广播地址,也就是任意地址的意思。
- struct tcp_pcb *tcp_listen(struct tcp_pcb *pcb)
指定一个PCB进入监听状态。当一个远端连接访问时,函数 tcp_accept()指定的回调函数将被调用。在调用这个函数之前一定要使用tcp_bind()函数绑定一个本地IP和端口号。
tcp_listen() 函数返回一个新的连接标识符,原始的pcb会被释放,这是为了节省内存,使之更适合小内存系统。
如果监听连接的内存无效,tcp_listen()函数返回NULL,如果这样的话,传入的PCB参数将不会被释放。
这个函数从原理上看也比较简单,首先是做一些必要的检查,判断原始pcb是否已经处于连接状态,如果没有则申请一块tcp_pcb类型的内存,将原始的必要的pcb内容复制到新的pcb中,设置新的pcb状态为LISTEN,释放原始的pcb,并将新pcb连接放入已监听队列。
- struct tcp_pcb *tcp_listen_with_backlog(struct tcp_pcb *pcb, u8_t backlog)
这个函数和tcp_listen()函数相同,只是限制了TCP监听队列连接个数,这个个数由backlog参数指定。为了使用它,你必须在你的lwipopt.h中设置TCP_LISTEN_BACKLOG=1。
- void tcp_accepted(struct tcp_pcb *pcb)
通知lwIP一个传入的连接已经被接受。通常这个函数在“accept()”函数的回调函数中被调用。这允许lwIP处理自身内部的任务。比如,允许更多传入的连接进入监听队列。
- void tcp_accept(struct tcp_pcb *pcb,
err_t (* accept)(void *arg, struct tcp_pcb *newpcb,
err_t err))
指定应在侦听连接上的一个新的连接到达时调用的回调函数。
- 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));
设置打开连接的pcb连接到远程主机并发送初始的SYN段。
函数tcp_connect() 会立即返回;它并不等待这个连接是否被正确设置。相反的,当连接正确建立后它将调用第四个参数("connected"参数)指定的函数。如果这个连接不能正确的建立,可能是主机拒绝这个连接或者主机没有响应,"connected"函数将被调用并设置一个相应的参数。
当入队的SYN段内存不可用时,tcp_connect()函数能返回ERR_MEM,表示连接没有正确建立。如果SYN成功入队,tcp_connect()函数返回ERR_OK。
---TCP数据发送函数
lwIP会调用tcp_write()函数来发送队列中的数据。当数据成功的发送到远程主机,会调用一个指定的回调函数来通知应用程序。
- err_t tcp_write(struct tcp_pcb *pcb, void *dataptr, u16_t len,
u8_t copy)
参数"dataptr"指向数据队列;参数"len"传递数据的长度;参数"copy"的值为0或者1,表明是否需要申请新的内存用于数据的拷贝。如果这个参数为0,则不需要申请新的内存,此时数据只能使用指针来引用。
如果数据长度超过当前发送缓存字节数或者要发送的段队列长度超过lwipopts.h中定义的上限值,tcp_write()函数执行失败并返回ERR_MEN。可以使用tcp_sndbuf()函数来返回输出队列有效的字节数。
使用这个函数的正确方法是根据tcp_sndbuf() 函数返回的字节数来发送数据。如果函数返回ERR_MEM,应用程序应该等待直到当前队列数据成功的被远程主机收到然后尝试重新发送一次。
- void tcp_sent(struct tcp_pcb *pcb,
err_t (* sent)(void *arg, struct tcp_pcb *tpcb,
u16_t len))
当远程主机成功接收(也就是应答信号)到数据时,该函数指定的回调函数被调用。传送给回调函数的"len"参数给出了上一次已经被确认的发送的最大字节数。
--TCP数据接收函数
TCP数据接收是基于回调函数的---当一个新的数据接收到时,应用程序指定的回调函数被调用。当应用程序接收到数据后,它必须调用tcp_recved()函数来指示接收数据的大小。
- void tcp_recv(struct tcp_pcb *pcb,
err_t (* recv)(void *arg, struct tcp_pcb *tpcb,
struct pbuf *p, err_t err))
当接收到数据时,本函数设置的回调函数将被调用。如果传递给回调函数一个NULL pbuf则说明远程主机关闭了这个连接。如果函数正常运行并且回调函数返回ERR_OK,则必须释放这个pbuf,如果其它情况,必须保存这个pbuf,这样才能让lwIP内核保存它以供应用程序检查并恢复错误。
- void tcp_recved(struct tcp_pcb *pcb, u16_t len)
当应用程序接收到数据后必须调用这个函数。参数"len"表明接收到的数据的长度。
--- 应用程序轮询函数
当一个连接空的时候(也就是说,既没有数据接收也没有数据发送),lwIP会通过调用一个指定的回调函数来重复轮询应用程序。这可以用作一个看门狗定时器,用来终止空闲时间太长的连接;或者用作等待内存有效的一种方法。举例来说,如果调用tcp_write()函数时因为内存无效而失败,应用程序可以使用轮询功能在连接空闲的时候再次调用tcp_write()。
- void tcp_poll(struct tcp_pcb *pcb, u8_t interval,
err_t (* poll)(void *arg, struct tcp_pcb *tpcb))
指定轮询间隔和应用程序轮询时调用的回调函数。这个间隔是以TCP粗粒度定时器为单位的,即500毫秒一次。如果参数"interval"的值为10,则意味着每5秒轮询一次应用程序。
---关闭和终止连接函数
- err_t tcp_close(struct tcp_pcb *pcb)
关闭连接。如果关闭的连接内存无效,函数返回ERR_MEM,如果是这样的话,应用程序应该等待并通过使用acknowledgment回调函数或者轮询功能重新关闭连接。如果连接关闭成功,函数返回WRR_OK。
TCP内核调用tcp_close()后,参数"pcb"指定的连接被解除。
- void tcp_abort(struct tcp_pcb *pcb)
通过向远程主机发送一个RST(