学会Zynq(11)RAW API的TCP和UDP编程

RAW API
RAW API(有时称作native API)是一种事件驱动型的API,在没有操作系统的情况下使用。核心栈通过这个API完成不同协议间的交互。

使用lwIP栈的应用程序通过一组回调函数实现。当某些“事件”发生时,会lwIP核会调用这些回调函数,比如传入数据、传出数据、错误通知、连接关闭等。应用程序中的回调函数执行对这些事件的处理操作。

RAW API支持多种协议,下面介绍如何对TCP和UDP进行编程。在Xilinx平台中使用lwIP的RAW API,部分细节会有所不同,但大部分函数用法都一样。

TCP实现
1. 初始化
在使用任何TCP函数前,必须先调用**lwip_init()函数。此后必须每隔TCP_TMR_INTERVAL(通常取250ms)调用一次tcp_tmr()函数。某些版本的lwIP只需要将sys_check_timeouts()**函数添加到主循环中,它会处理栈中所有协议的定时器。Xilinx中还是需要通过配置处理器的定时器来调用tcp_tmr()。

**tcp_arg()**函数指定传给一个连接的所有回调函数的参数,原型接口如下:
void tcp_arg(struct tcp_pcb * pcb, void * arg)

“pcb”参数指定TCP连接的控制块;“arg”参数是指向用户设定的一些数据的指针。大多数情况下,用户使用这个参数来标识应用程序中的特定实例。

2. TCP连接步骤
一个TCP连接由一个协议控制块(Protocol Control Block,PCB)做标识。有两种建立连接的方法。被动连接(监听)方法,相当于作为服务端:

1. 调用pcb_new创建一个pcb。
2. (可选)调用tcp_arg将应用程序中特定的值于PCB关联在一起。
3 . 调用tcp_bind函数指定本地IP地址和端口。
4. 调用tcp_listen或tcp_listen_with_backlog,这些函数将释放作为参数的PCB,并返回一个更小的监听PCB,如“tcp_new = tcp_listen(tpcb);”。
5. 调用tcp_accept指定新连接到来时要调用的函数。

主动连接方法,相当于作为客户端:

1. 调用pcb_new创建一个pcb。
2. (可选)调用tcp_arg将应用程序中特定的值于PCB关联在一起。
3. (可选)调用tcp_bind函数指定本地IP地址和端口
4. 调用tcp_connect函数。

3. TCP连接函数
struct tcp_pcb * tcp_new(void)

该函数创建一个新的连接控制块PCB,连接的初始状态为“close”。如果没有可用的内存创建新的PCB,将会返回NULL。
err_t tcp_bind(struct tcp_pcb * pcb, struct ip_addr * ipaddr, u16_t port)

该函数将PCB与本地IP地址和端口号绑定在一起。IP地址可设置为IP_ADDR_ANY,连接绑定到所有本地IP地址。如果端口设置为0,函数会自动选择一个可用端口。绑定时连接必须处于“close”状态。绑定成功后会返回“ERR_OK”;如果连接试图绑定到被占用的端口,会返回“ERR_USE”。
struct tcp_pcb * tcp_listen(struct tcp_pcb * pcb)

该函数中的PCB参数用于指定要监听的连接,此时该连接必须处于“close”状态,并已经使用tcp_bind绑定到了本地端口。该函数设置本地端口来监听传入的连接。

tcp_listen函数会返回一个新的PCB,作为参数传递给函数的PCB会被释放。这是因为监听需要的内存更少,因此tcp_listen会为监听连接分配一个更小的内存。如果可用内存不够,则返回NULL,作为参数的PCB相关联的内存也不会被释放。

调用tcp_listen()之后必须调用tcp_accept(),否则此端口的传入连接会被中止。
struct tcp_pcb * tcp_listen_with_backlog(struct tcp_pcb * pcb, u8_t backlog)

该函数功能与tcp_listen相同,但将监听队列中未完成连接的数量限制为backlog参数的值。使用该函数需要在lwipop.h中设置TCP_LISTEN_BACKLOG=1。
void tcp_accept(struct tcp_pcb * pcb,
err_t (* accept)(void * arg, struct tcp_pcb * newpcb, err_t err))

该函数命令PCB开始监听传入的连接。当新连接到达本地端口时,PCB会调用设定的回调函数来完成新连接。

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会立即返回值,不会等待正确设置连接。SYN成功入列则返回ERR_OK;若没有可用内存用来SYN段的排队,则返回ERR_MEM。当连接建立时,会调用第四个参数所指定的回调函数。如果由于主机拒绝连接或没有应答,导致连接未能建立,会调用一个错误处理函数。

4. 发送TCP数据
在TCP连接上发送数据的步骤如下:

1. 调用tcp_sent()函数设置回调函数;
2. 调用tcp_sendbuf()函数查找可以发送的最大数据量;
3. 调用tcp_write()函数把数据排队;
4. 调用tcp_output()函数强制发送数据。

上述步骤是只是个大致流程,不同情况下用法会有所不同。

void tcp_sent(struct tcp_pcb * pcb,
err_t (* sent)(void * arg, struct tcp_pcb * tpcb, u16_t len))

tcp_sent指定当远程主机确实收到数据时,应该调用的回调函数。len参数为主机确认的字节数。

u16_t tcp_sndbuf(struct tcp_pcb * pcb)

tcp_sendbuf返回输出队列中可用空间的字节数。

err_t tcp_write(struct tcp_pcb * pcb, void * dataptr, u16_t len, u8_t apiflags)

tcp_write让参数dataptr指向的数据排队,数据长度为len。apiflags的取值包括两个bit位:TCP_WRITE_FLAG_COPY指示lwIP应该分配新内存并将数据分配到其中,未选此bit则不会分配新内存;TCP_WRITE_FLGA_MORE指示不在TCP段中设置push标志。

如果数据长度超过了当前发送缓冲区的大小,或者输出段队列长度大于lwipopt.h中为TCP_SND_QUEUELEN定义的上限,tcp_write()函数会失败并返回ERR_MEM。此时应用程序应该等待其它主机成功接收到排在前面的数据后,再重试。

err_t tcp_output(struct tcp_pcb * pcb)

tcp_output强制发送目前所有进入队列的数据。

5. 接收TCP数据
TCP数据接收是基于回调的,当新数据到达时,将调用应用程序指定的回调函数。TCP协议设定了一个窗口(window),该窗口告诉发送主机它可以在连接上发送多少数据。所有连接的窗口大小都是lwipopts.h中设置的TCP_WND值。当应用程序处理了传入的数据后,必须调用tcp_recved()函数,以指示TCP可以增加接收窗口。

void tcp_recv(struct tcp_pcb * pcb,
err_t (* recv)(void * arg, struct tcp_pcb * tpcb, struct pbuf * p, err_t err))

tcp_recv设置新数据到达时将调用的回调函数。如果没有错误发生且回调函数返回了ERR_OK,tcp_recv会释放占用的pbuf;否则不会释放pbuf,等待lwIP核做进一步处理。如果远程主机关闭了连接,将会调用带NULL pbuf的回调函数,告知应用程序连接已关闭。

void tcp_recved(struct tcp_pcb * pcb, u16_t len)

应用程序已经处理完数据并准备接收更多数据时,必须调用tcp_recved函数。其目的是在处理数据是有更大的窗口。len参数表示处理数据的长度。

6. 应用程序轮询(polling)
当连接处于空闲状态时(即没有发送或接收数据),lwIP将调用指定的回调函数重复轮询应用程序。这种机制可以用作看门狗计时器,以解决长时间处于空闲状态的连接;也可以作为一种等待内存变为可用状态的方法。比如当内存不可用导致tcp_write()的调用失败时,应用程序可以在连接空闲了一段时间后,使用轮询功能再次调用tcp_write()。

void tcp_poll(struct tcp_pcb * pcb,
err_t (* poll)(void * arg, struct tcp_pcb * tpcb), u8_t interval)

tcp_toll指定轮询应用程序时应调用的回调函数和轮询间隔。TCP有一个粗略的计时器,大概一秒钟发生两次信号,轮询间隔时间和此有关。比如设置为10,则表示应用程序每(10/2=)5秒轮询一次。

7. 关闭与中止连接
err_t tcp_close(struct tcp_pcb * pcb)

tcp_close用于关闭连接。如果没有可用内存来关闭连接时,tcp_close返回ERR_MEM。此时应用程序应该使用应答回调函数或轮询功能,等待并再次尝试关闭。成功关闭后返回ERR_OK。调用tcp_close后,TCP会释放PCB。但是在远程主机确认关闭连接之前,仍然可以在该连接上收到数据。

void tcp_abort(struct tcp_pcb * pcb)

tcp_abort向远程主机发送RST段(复位)来中止连接,PCB会被释放。

8. 其余函数
如果连接由于错误而中止,或者连接尝试失败、超时或重置,应用程序将通过err回调函数对此类事件发出警报。内存不足也会导致连接中止。要调用的回调函数通过tcp_err()函数设置。

void tcp_err(struct tcp_pcb * pcb, void (* err)(void * arg, err_t err))

错误回调函数不会将PCB作为参数,因为此时PCB可能已经被释放了。

tcp_nagle_enable ( struct tcp_pcb * aPcb );
tcp_nagle_disable ( struct tcp_pcb * aPcb );
tcp_nagle_disabled ( struct tcp_pcb * aPcb );

Nagle算法会自动连接许多小的消息,减少发送包的个数来增加网络效率。TCP/IP协议中无论发送多少数据,总需要在数据前面加上协议头;对方接收到数据也需要发送应答。Nagle算法尽可能发送大块数据,避免小数据块,从而充分利用网络带宽。

但有时我们又不需要Nagle算法。tcp_nagle_enable()函数用于开启nagle算法;tcp_nagle_disable()函数用于禁用nagle算法;tcp_nagle_disabled()用于检测,未启用nagle算法时返回ture。

RAW TCP示例序列图
RAW TCP主要是通过执行回调函数来实现的,它的操作往往与接收和处理单个消息紧密相关。因此如果熟悉底层的TCP协议对编程会有一定帮助,否则会不知道该在何时调用哪个函数。下表给出了远程客户机与本地lwIP服务器之间交互的序列图。

远程

注意,tcp_write()只对TCP数据进行排队,以便稍后传输,它实际上并没有开始传输。当。但是如果是在接收回调中使用tcp_write(),如上表中的示例,则不需要调用tcp_output()来传输要发送的数据。如果在接收回调中使用了tcp_output,它不会执行任何操作。

当接收回调函数返回时,lwIP栈会自动启动数据的发送,远程客户端的前一个数据包的应答将于第一个传出的数据段相结合。如果在其它地方调用tcp_write,则可能需要调用tcp_output来启动数据传输。

下面再给出一个lwIP作为客户端连接远程服务器的序列图:

上表中在连接建立前便建立了接收和发送的回调函数,该操作也可以在连接建立后进行。如果连接失败,客户端可以通过tcp_err()设置的回调函数得到失败的通知。

UDP实现

struct udp_pcb * udp_new(void)

udp_new()函数创建一个新的UDP PCB,用于UDP通信。PCB在绑定到本地地址或连接到远程地址之前都处于不活跃状态。

void udp_remove(struct udp_pcb * pcb)

udp_remove()函数移除并释放PCB。

err_t udp_bind(struct udp_pcb * pcb, struct ip_addr * ipaddr, u16_t port)

udp_bind()函数将PCB与本地地址绑定。IP地址参数ipaddr可以是“IP_ADDR_ANY”,表示监听任一本地IP地址。如果指定的端口port已被占用,会返回ERR_USE;否则返回ERR_OK。

err_t udp_connect(struct udp_pcb * pcb, struct ip_addr * ipaddr, u16_t port)

udp_connect()函数设置PCB的远程端。这个函数只是设置PCB的远程地址,不会产生任何网络流量。如果连接的端口不可用则返回ERR_USE;如何没有到目标的路由则返回ERR_RTE;连接成功则返回ERR_OK。

只有使用udp_send()函数时才需要进行连接。对于未连接的PCB,可以使用udp_sendto()函数将其发送到任何指定的远程地址。已连接的PCB只从连接的远程地址处接受数据;未连接的PCB可以从任意地址接受数据报。

void udp_disconnect(struct udp_pcb * pcb)

udp_disconnect()函数移除PCB连接的远程端。这个函数只会移除PCB的远程地址,不会产生任何网络流量。

err_t udp_send(struct udp_pcb * pcb, struct pbuf * p)

udp_send()函数将pbuf类型的变量p发送到远程地址集。pbuf不会被释放。

err_t udp_sendto(struct udp_pcb *pcb, struct pbuf *p,
struct ip_addr *dst_ip, u16_t dst_port);

udp_sendto()函数功能和udp_send相同,但是可以发送到任意指定的远程地址。

void udp_recv(struct udp_pcb * pcb,
void (* recv)(void * arg, struct udp_pcb * upcb,
struct pbuf * p,
struct ip_addr * addr,
u16_t port),
void * recv_arg)

udp_recv()函数设置特定连接上接收到数据报时应调用的回调函数。回调函数负责释放pbuf。

---------------------


文章来源:FPGADesigner的博客
*本文由作者授权转发,如需转载请联系作者本人

推荐阅读