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

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/FPGADesigner/article/details/88689126

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_listentcp_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 消息 lwIP栈操作 lwIP服务器操作 简述
- - - <= tcp_new() 创建TCP PCB
- - - <= tcp_bind() 绑定端口号
- - - <= tcp_listen_with_backlog() 创建监听端点(分配新的PCB)
- - - <= tcp_accept() 设置accept回调
- - - <= tcp_arg() 设置回调函数参数
connect => - - - 客户端连接服务器
- SYN => - - 远程栈发送SYN
- - 分配新PCB - lwIP“挂起”会话
- <= SYN/ACK - - SYN/ACK响应
<= (连接返回) - - - 远程栈通知客户端连接成功
- ACK => - - 远程栈发送完成“三次握手”的应答
- - (调用 accept 回调函数) => - lwIP通知应用程序有新会话
- - - <= tcp_accepted() 服务器接受连接,减少挂起会话的计数
- - - <= tcp_arg() 设置新的回调参数
- - - <= tcp_recv() 服务器设置接收回调
- - - <= tcp_err() 服务器设置错误/中止的回调函数
- - - <= tcp_sent() 服务器设置发送回调
- - - <= (服务器从accept回调函数返回OK状态)
- - (把PCB标记为活跃) - -
已建立连接(任何一方都可以发送数据)
send => TCP数据=> - - 客户端发送请求数据
- - (lwIP调用服务器的接收回调函数) => - -
- - - <= tcp_write(response_data, len) 服务器向客户机写入回复的数据
- - (lwIP对TCP段进行排队) - -
- - - <=tcp_write(response_data2, len2) 服务器写入更多数据
- - (lwIP对TCP段进行排队) - 段与段之间可以合并
- - - <= tcp_recved() 服务器通知lwIP 使用更大的窗口
- - - <= (服务器从接收回调函数返回OK状态) -
- <= TCP 数据 (lwIP查找要发送的队列段) - lwIP向客户端发送数据段,包括接收客户端数据的应答

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

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

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

远程服务器 TCP 消息 lwIP栈操作 lwIP客户端操作 简述
- - - <= tcp_new() 创建TCP PCB
- - - <= [tcp_bind()] 绑定端口号
- - - <= tcp_arg() 创建监听端点(分配新的PCB)
- - - <= tcp_err()
- - - <= tcp_recv() 设置接收回调函数
- - - <= tcp_sent() 设置发送回调函数
- - - <= tcp_connect() 连接并提供连接回调函数
- <= SYN <= (lwIP生成SYN) - lwIP生成到远程服务器的SYN包
- SYN/ACK => - - 远程服务器栈发送SYN/ACK
- - (lwIP调用连接回调) => - 从lwIP的角度看,会话已经建立,回调返回时会生成三次TCP握手的应答
建立连接(任何一方都可以发送数据)
- - - <=tcp_write(request_data, len) 客户端向服务器写请求数据
- - (lwIP对TCP段排队) - -
- - - <=tcp_write(request_data2, len2) 客户端向服务器写更多数据
- - (lwIP对TCP段排队) - 段与段之间可以合并
- - - <= tcp_output() 客户端向lwIP发送信号以生成输出包
- - - <= (客户端从连接回调函数中返回) -
- <= TCP 数据 - - lwIP生成一个或多个数据包
send => TCP数据 => - - 服务器发送应答数据
- - (调用客户端接收回调)=> - 做相应处理

上表中在连接建立前便建立了接收和发送的回调函数,该操作也可以在连接建立后进行。如果连接失败,客户端可以通过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。

猜你喜欢

转载自blog.csdn.net/FPGADesigner/article/details/88689126