RAW/Callback API编程
1.1 IP RAW编程
在前面介绍IP协议时,IP层输入函数ip_input对于每个输入的数据包都会调用raw_input进行处理,这是IP层为应用程序直接获取IP数据包提供的一种机制,Socket编程中将这种机制称为原始套接字,而在LWIP内核中,我们可以把它称为原始协议控制块raw_pcb,对raw_pcb的描述如下:
// rt-thread\components\net\LWIP-1.4.1\src\include\LWIP\raw.h /** Function prototype for raw pcb receive callback functions. * @param arg user supplied argument (raw_pcb.recv_arg) * @param pcb the raw_pcb which received data * @param p the packet buffer that was received * @param addr the remote IP address from which the packet was received * @return 1 if the packet was 'eaten' (aka. deleted), * 0 if the packet lives on * If returning 1, the callback is responsible for freeing the pbuf * if it's not used any more. */ typedef u8_t (*raw_recv_fn)(void *arg, struct raw_pcb *pcb, struct pbuf *p, ip_addr_t *addr); struct raw_pcb { /* Common members of all PCB types */ IP_PCB; struct raw_pcb *next; u8_t protocol; /** receive callback function */ raw_recv_fn recv; /* user-supplied argument for the recv callback */ void *recv_arg; }; |
原始协议控制块raw_pcb如上所示,每一个控制块raw_pcb可定制一个特定协议类型的IP数据包,如ICMP包、TCP包、UDP包等。当IP层收到一个数据包后,如果该包首部中的IP地址和协议字段与某个raw_pcb吻合,则数据包会被递交给这个raw_pcb处理。raw_pcb对数据包的处理过程很简单,直接调用raw_pcb上注册的recv回调函数,用户根据自己的需要编写这个回调函数,从而完成对该IP包的特定处理。内核中可能同时存在多个raw_pcb,它们各自定制不同连接上的不同协议包,因此内核利用next字段将所有raw_pcb组织在一个名为raw_pcbs的链表上,方便对各个原始协议控制块进行遍历操作。
在IP层进行RAW编程的API函数见下表:
raw_pcb操作函数 | 函数功能描述 |
struct raw_pcb * raw_new(u8_t proto) | 创建一个raw_pcb并插入raw_pcbs链表首部,以proto作为协议类型初始化该控制块; |
void raw_remove(struct raw_pcb *pcb) | 从raw_pcbs链表中移除某raw_pcb并释放其内存空间; |
err_t raw_bind(struct raw_pcb *pcb,ip_addr_t *ipaddr) | 将本地IP地址绑定到raw_pcb上(设置IP_PCB中的local_ip); |
err_t raw_connect(struct raw_pcb *pcb,ip_addr_t *ipaddr) | 将对端IP地址绑定到raw_pcb上(设置IP_PCB中的remote_ip); |
void raw_recv(struct raw_pcb *pcb,raw_recv_fn recv, void *recv_arg) | 向raw_pcb中注册回调函数recv及其参数recv_arg; |
err_t raw_sendto(struct raw_pcb *pcb,struct pbuf *p, ip_addr_t *ipaddr) | 将一个raw IP数据包发送到目的IP地址对应的主机,raw IP数据包首部字段值由raw_pcb提供; |
err_t raw_send(struct raw_pcb *pcb,struct pbuf *p) | 实际调用raw_sendto; |
利用raw_pcb的结构体及其操作函数,我们可以注册一个ICMP协议的原始协议控制块,用来接收IP层的ping响应包,同时利用内核的定时机制,周期性地往对端IP地址构造并发送ping请求包,而在原始协议控制块的recv回调函数中接收并处理ping响应。按照上述原理发送ping请求的实现代码如下:
#define PING_DELAY 1000 #define PING_ID 0xAFAF #define PING_DATA_SIZE 32 /* ping variables */ static u16_t ping_seq_num; static u32_t ping_time; static struct raw_pcb *ping_pcb = NULL; static ip_addr_t ping_dst; void ping_init(void) { IP4_ADDR(&ping_dst, 192,168,1,103); ping_raw_init(); } static void ping_raw_init(void) { ping_pcb = raw_new(IP_PROTO_ICMP); raw_recv(ping_pcb, ping_recv, NULL); raw_bind(ping_pcb, IP_ADDR_ANY); sys_timeout(PING_DELAY, ping_timeout, ping_pcb); } static void ping_timeout(void *arg) { struct raw_pcb *pcb = (struct raw_pcb*)arg; ping_send(pcb, &ping_dst); sys_timeout(PING_DELAY, ping_timeout, pcb); } static void ping_send(struct raw_pcb *raw, ip_addr_t *addr) { struct pbuf *p; struct icmp_echo_hdr *iecho; size_t ping_size = sizeof(struct icmp_echo_hdr) + PING_DATA_SIZE; p = pbuf_alloc(PBUF_IP, (u16_t)ping_size, PBUF_RAM); if (!p) { return; } if ((p->len == p->tot_len) && (p->next == NULL)) { iecho = (struct icmp_echo_hdr *)p->payload; ping_prepare_echo(iecho, (u16_t)ping_size); raw_sendto(raw, p, addr); ping_time = sys_now(); LWIP_DEBUGF(PING_DEBUG, ("ping:[%"U32_F"] send ", ping_seq_num)); ip_addr_debug_print(PING_DEBUG, addr); LWIP_DEBUGF( PING_DEBUG, ("\n")); } pbuf_free(p); } /** Prepare a echo ICMP request */ static void ping_prepare_echo( struct icmp_echo_hdr *iecho, u16_t len) { size_t i; size_t data_len = len - sizeof(struct icmp_echo_hdr); ICMPH_TYPE_SET(iecho, ICMP_ECHO); ICMPH_CODE_SET(iecho, 0); iecho->chksum = 0; iecho->id = PING_ID; iecho->seqno = htons(++ping_seq_num); /* fill the additional data buffer with some data */ for(i = 0; i < data_len; i++) { ((char*)iecho)[sizeof(struct icmp_echo_hdr) + i] = (char)i; } iecho->chksum = inet_chksum(iecho, len); } /* Ping using the raw ip */ static u8_t ping_recv(void *arg, struct raw_pcb *pcb, struct pbuf *p, ip_addr_t *addr) { struct icmp_echo_hdr *iecho; //we can also check src ip here, but just egnore it if ((p->tot_len >= (PBUF_IP_HLEN + sizeof(struct icmp_echo_hdr)))) { iecho = (struct icmp_echo_hdr *)((u8_t*)p->payload + PBUF_IP_HLEN); if ((iecho->type == ICMP_ER) && (iecho->id == PING_ID) && (iecho->seqno == htons(ping_seq_num))) { LWIP_DEBUGF( PING_DEBUG, ("ping: recv ")); ip_addr_debug_print(PING_DEBUG, addr); LWIP_DEBUGF( PING_DEBUG, (" time=%"U32_F" ms\n", (sys_now()-ping_time)));
pbuf_free(p); return 1; /* eat the packet */ } } return 0; /* don't eat the packet */ } |
上面的代码主要功能有两个:一是通过注册到新建raw_pcb上的ping_recv函数,当接收到IP层的ping回送请求包时,通过串口打印ping响应信息;二是通过周期性函数ping_timeout不断调用ping_send向对端IP地址发送ping请求包(由函数ping_prepare_echo构造),并通过串口打印ping请求包发送信息。
1.2 UDP RAW编程
控制块操作函数构成了UDP编程的核心,用户程序使用UDP传输数据的关键在于使用内核提供控制块操作函数注册、管理控制块,同时最重要的是编写用户自定义报文处理函数。
使用UDP编程,基本就是调用控制块操作函数对UDP控制块进行操作,UDP相关的函数比较简单,它没有流量控制机制、没有确认机制,它完成的简单工作就是根据接收到的报文查找UDP控制块,然后调用注册的用户函数处理报文数据,如果用户注册的函数为空则相应的报文会被直接删除,如果查找不到对应的控制块时UDP会向源主机返回一个ICMP端口不可达差错报告报文。
1.新建控制块udp_new
任何想使用UDP服务的应用程序都必须拥有一个控制块,并把控制块绑定到相应的端口号上,在接收报文时,端口号将作为报文终点选择的唯一依据。新建控制块函数很简单,就是在内存池中为UDP控制块申请一个MEMP_UDP_PCB类型的内存空间,并初始化相关字段。如果MEMP_UDP_PCB类型的空间用完了则udp_new将返回NULL,MEMP_UDP_PCB的个数是用户可以配置的,用户应该根据实际连接情况合理配置其个数。
// rt-thread\components\net\LWIP-1.4.1\src\core\udp.c /** * Create a UDP PCB. * @return The UDP PCB which was created. NULL if the PCB data structure * could not be allocated. * @see udp_remove() */ struct udp_pcb *udp_new(void) { struct udp_pcb *pcb; pcb = (struct udp_pcb *)memp_malloc(MEMP_UDP_PCB); /* could allocate UDP PCB? */ if (pcb != NULL) { /* UDP Lite: by initializing to all zeroes, chksum_len is set to 0 * which means checksum is generated over the whole datagram per default * (recommended as default by RFC 3828). */ /* initialize PCB to all zeroes */ memset(pcb, 0, sizeof(struct udp_pcb)); pcb->ttl = UDP_TTL; } return pcb; } |
2.绑定控制块udp_bind
当作为服务器程序时,必须手动为控制块绑定一个熟知端口号,客户端程序能够使用这个熟知端口号与服务器进行通信;当作为客户端程序时,手动绑定端口号并不是必须的,在与服务器通信前UDP内核可以自动为控制块绑定一个短暂端口号。端口号绑定的本质就是设置控制块中的local_port与local_ip字段,同时涉及到对链表udp_pcbs的操作。若应用程序调用该函数时未指明端口号(port为0),则函数会在0xc000与0xffff之间寻找一个编号最低的未用端口给控制块。
// rt-thread\components\net\LWIP-1.4.1\src\core\udp.c /** * Bind an UDP PCB. * * @param pcb UDP PCB to be bound with a local address ipaddr and port. * @param ipaddr local IP address to bind with. Use IP_ADDR_ANY to * bind to all local interfaces. * @param port local UDP port to bind with. Use 0 to automatically bind * to a random port between UDP_LOCAL_PORT_RANGE_START and * UDP_LOCAL_PORT_RANGE_END. * * ipaddr & port are expected to be in the same byte order as in the pcb. * * @return LWIP error code. * - ERR_OK. Successful. No error occured. * - ERR_USE. The specified ipaddr and port are already bound to by * another UDP PCB. * @see udp_disconnect() */ err_t udp_bind(struct udp_pcb *pcb, ip_addr_t *ipaddr, u16_t port) { struct udp_pcb *ipcb; u8_t rebind; rebind = 0; /* Check for double bind and rebind of the same pcb */ for (ipcb = udp_pcbs; ipcb != NULL; ipcb = ipcb->next) { /* is this UDP PCB already on active list? */ if (pcb == ipcb) { /* pcb may occur at most once in active list */ LWIP_ASSERT("rebind == 0", rebind == 0); /* pcb already in list, just rebind */ rebind = 1; } /* port matches that of PCB in list and REUSEADDR not set -> reject */ else { if ((ipcb->local_port == port) && /* IP address matches, or one is IP_ADDR_ANY? */ (ip_addr_isany(&(ipcb->local_ip)) || ip_addr_isany(ipaddr) || ip_addr_cmp(&(ipcb->local_ip), ipaddr))) { /* other PCB already binds to this local IP and port */ return ERR_USE; } } } ip_addr_set(&pcb->local_ip, ipaddr);
/* no port specified? */ if (port == 0) { port = udp_new_port(); if (port == 0) { /* no more ports available in local range */ return ERR_USE; } } pcb->local_port = port; /* pcb not active yet? */ if (rebind == 0) { /* place the PCB on the active list if not already there */ pcb->next = udp_pcbs; udp_pcbs = pcb; } return ERR_OK; } |
3.建立连接udp_connect
与绑定控制块函数相对应,连接控制块函数完成控制块中remote_ip与remote_port的设置,只有绑定了本地IP地址和端口号以及远端IP地址和端口号的控制块才会处于UDP_FLAGS_CONNECTED连接状态。这里虽说建立连接,但由于UDP是面向无连接的,并没有任何握手数据被发送到对端。
// rt-thread\components\net\LWIP-1.4.1\src\core\udp.c /** * Connect an UDP PCB. * This will associate the UDP PCB with the remote address. * * @param pcb UDP PCB to be connected with remote address ipaddr and port. * @param ipaddr remote IP address to connect with. * @param port remote UDP port to connect with. * @return LWIP error code * * ipaddr & port are expected to be in the same byte order as in the pcb. * The udp pcb is bound to a random local port if not already bound. * @see udp_disconnect() */ err_t udp_connect(struct udp_pcb *pcb, ip_addr_t *ipaddr, u16_t port) { struct udp_pcb *ipcb; if (pcb->local_port == 0) { err_t err = udp_bind(pcb, &pcb->local_ip, pcb->local_port); if (err != ERR_OK) { return err; } } ip_addr_set(&pcb->remote_ip, ipaddr); pcb->remote_port = port; pcb->flags |= UDP_FLAGS_CONNECTED; /* Insert UDP PCB into the list of active UDP PCBs. */ for (ipcb = udp_pcbs; ipcb != NULL; ipcb = ipcb->next) { if (pcb == ipcb) { /* already on the list, just return */ return ERR_OK; } } /* PCB not yet on the list, add PCB now */ pcb->next = udp_pcbs; udp_pcbs = pcb; return ERR_OK; } |
4. 注册数据接受函数—udp_recv
该函数的本质是设置控制块中的recv字段和recv_arg字段,recv是用户注册到控制块中用于处理该UDP连接数据的用户函数,是用户程序和内核交互的重要桥梁。当内核收到这个连接上的数据后,会回调recv函数,并且将recv_arg作为回调的第一个参数传入。
// rt-thread\components\net\LWIP-1.4.1\src\core\udp.c /** * Set a receive callback for a UDP PCB * This callback will be called when receiving a datagram for the pcb. * * @param pcb the pcb for wich to set the recv callback * @param recv function pointer of the callback function * @param recv_arg additional argument to pass to the callback function */ void udp_recv(struct udp_pcb *pcb, udp_recv_fn recv, void *recv_arg) { /* remember recv() callback and user data */ pcb->recv = recv; pcb->recv_arg = recv_arg; } |
5. 发送数据udp_send/udp_sendto
该函数的本质是在控制块上将数据包发送出去,udp_send或udp_sendto都可以用来发送报文,它们二者之间的区别在于后者需要为函数指明数据发送的目的IP地址和端口号,而前者默认使用控制块中记录的目的IP地址和端口号。本质上,在内核中udp_send通过调用函数udp_sendto来实现其功能。待发送数据必须封装到数据包pbuf中,这是最重要也很容易出错的地方。这部分在后面介绍内核报文处理时再具体介绍其实现过程。
6. 断开连接udp_disconnect
该函数本质是清除控制块中remote_ip和remote_port字段,并将控制块置为非连接状态,这里没有任何握手数据发往对端。
// rt-thread\components\net\LWIP-1.4.1\src\core\udp.c /** * Disconnect a UDP PCB * * @param pcb the udp pcb to disconnect. */ void udp_disconnect(struct udp_pcb *pcb) { /* reset remote address association */ ip_addr_set_any(&pcb->remote_ip); pcb->remote_port = 0; /* mark PCB as unconnected */ pcb->flags &= ~UDP_FLAGS_CONNECTED; } |
7. 删除连接—udp_remove
该函数本质是将控制块从链表udp_pcbs中删除,并释放其占用的内存空间,至此这个控制块对应的连接在内核中彻底消失。
// rt-thread\components\net\LWIP-1.4.1\src\core\udp.c /** * Disconnect a UDP PCB * * @param pcb the udp pcb to disconnect. */ void udp_disconnect(struct udp_pcb *pcb) { /* reset remote address association */ ip_addr_set_any(&pcb->remote_ip); pcb->remote_port = 0; /* mark PCB as unconnected */ pcb->flags &= ~UDP_FLAGS_CONNECTED; } |
下面用一个简单的回送程序(将本地接收到的UDP数据原样返回给源主机)来看看UDP相关编程接口的应用,其中udp_remote_callback为用户自定义接收数据处理函数,udp_demo_init完成回送应用程序的初始化。
#define UDP_ECHO_PORT 7 void udp_demo_callback(void *arg, struct udp_pcb *upcb, struct pbuf *p, struct ip_addr *addr, u16_t port) { udp_sendto(upcb, p, addr, port); pbuf_free(p); } void udp_demo_init(void) { struct udp_pcb *upcb;
/* Create a new UDP control block */ upcb = udp_new();
/* Bind the upcb to any IP address and the UDP_PORT port*/ udp_bind(upcb, IP_ADDR_ANY, UDP_ECHO_PORT); /* Set a receive callback for the upcb */ udp_recv(upcb, udp_demo_callback, NULL); } |
上面这种应用程序编写方法叫做Raw/Callback API方法,在这种方式下的应用程序与协议栈内核处于同一个进程中,用户程序通过回调的方式被协议栈调用,以取得协议栈中的数据,基于回调机制的应用程序会使得整个代码的灵活性加大。另一方面,使用这种方式编程需要直接与内核交互,用户函数与协议栈内核存在相互制约关系,应用程序执行时内核一直处于等待状态,内核需要等用户函数返回一个处理结果后再继续运行。如果用户程序的计算量很大,执行时间很长,则协议栈代码就一直得不到执行,协议栈接收、处理新数据包效率会受到直接影响,甚至协议栈会因来不及处理到来的数据包而出现丢包的情况。
1.3 TCP RAW编程
TCP编程本质上也是对TCP控制块的操作,系统将为每一个连接分配一个控制块,TCP连接与控制块是一一对应的。控制块中有几个回调函数,用户编程的本质就是编写自己的回调函数,然后注册到TCP控制块中,当内核在控制块上检测到相应的事件后,会调用用户注册的回调函数。
前面介绍的两条TCP控制块状态转换路线是TCP编程的基础和重点,也是用户程序设计过程的难点。应用程序中的每一句API调用会对控制块产生什么影响、会将连接置于什么状态,用户对这些应该了然于胸,才能设计出稳定性好的程序。
使用Raw API编写高效可靠TCP程序的秘诀可以总结为一句话:立足控制块,认清状态转换图,编写回调函数。下面展示出TCP客户端与服务器程序设计时,两者的编写流程(包括相关的Raw API函数调用与状态转换)分别如下:
下面将根据上面的流程图,依次来梳理下各个API函数的功能。
1.新建控制块—tcp_new
函数原型:struct tcp_pcb *tcp_new(void)
该函数通过调用tcp_alloc函数为连接分配一个TCP控制块tcp_pcb。tcp_alloc首先为新的tcp_pcb分配MEMP_TCP_PCB类型的内存池空间,若空间不够则函数会释放处于TIME_WAIT状态的控制块或其它优先级更低的控制块(使用控制块的prio字段)为新控制块寻找空间。当内存空间分配成功后,函数会初始化tcp_pcb中各个字段的内容,将控制块置为CLOSED状态,并返回申请成功的控制块指针,以便应用程序根据指针对这个控制块操作。
2.绑定控制块—tcp_bind
函数原型:err_t tcp_bind(struct tcp_pcb *pcb, ip_addr_t *ipaddr, u16_t port)
对于服务器程序来说,当新建一个控制块后,需要在控制块上绑定本地IP地址及一个熟知端口号,以便于客户端的连接。该函数的本质是将两个参数的值赋给TCP控制块中的local_ip与local_port字段,但该函数需遍历前面说到的四条链表,以保证这个本地IP与本地Port没有被其他控制块使用。最后,tcp_bind还要将绑定完毕的控制块插入到tcp_bounds_pcbs首部。
3.控制块侦听—tcp_listen
函数原型:struct tcp_pcb *tcp_listen(struct tcp_pcb *pcb)
对于服务器程序来说,会主动调用函数tcp_listen使控制块进入侦听状态,以便接受客户端的连接。要把一个控制块置为LISTEN状态很简单,先将其从tcp_bounds_pcbs链表上取下来,将其state字段置为LISTEN,最后再将该PCB挂接到链表tcp_listen_pcbs上。但事实上,LWIP引入了一个tcp_pcb_listen结构专门用来描述LISTEN状态下的控制块,该结构与tcp_pcb结构相近,但去掉了其中在LISTEN阶段用不到的字段,可以节省内存空间的消耗。所以,函数tcp_listen是这样做的:先申请一个tcp_pcb_listen结构(从内存池MEMP_TCP_PCB_LISTEN中分配),然后将tcp_pcb中的有用字段拷贝进来,最后将tcp_pcb_listen结构挂接到链表tcp_listen_pcbs首部,同时在tcp_bounds_pcbs链表上删除对应的控制块。
进入 LISTEN 状态后,服务器就等待客户端发送来的 SYN 报文进行连接,当内核收到一个 SYN握手报文后,会遍历 tcp_listen_pcbs 链表,以找到和报文中目的 IP 地址、目的端口号匹配的控制块。若找到匹配的 LISTEN 状态控制块,则内核会新建一个 tcp_pcb 结构,并将 tcp_pcb_listen 结构中各个字段拷贝至其中,同时填写结构中的源端口号、源 IP 地址等字段,最后在 tcp_active_pcbs 链表中添加这个新 tcp_pcb 结构。这样,新 TCP 控制块就处在 tcp_active_pcbs 中了,此时这个 tcp_pcb 结构的 state 字段应该设置为 SYN_RCVD。注意 tcp_listen_pcbs 链表中的匹配 tcp_pcb_listen 结构将一直存在,它并不会被删除,以等待其他客户端的连接,这正是服务器需要实现的功能。
4.服务器accept函数注册—tcp_accept
函数原型:void tcp_accept(struct tcp_pcb *pcb, tcp_accept_fn accept)
服务器程序调用tcp_listen成功并返回一个LISTEN状态的控制块后,需要立即调用这个tcp_accept函数,向新控制块的accept指针字段注册一个用户自己编写的tcp_accept_fn类型函数。当服务器内核检测到一个连接建立成功后,会回调这个用户accept函数,用户应该在这个函数中完成对新连接的处理。tcp_accept函数只是把accept函数指针赋值给pcb->accept,让侦听的控制块记录下用户accept函数的地址,以方便回调。
5.accept回调函数类型—tcp_accept_fn
函数原型:err_t (*tcp_accept_fn)(void *arg, struct tcp_pcb *newpcb, err_t err)
用户应该自己编写一个tcp_accept_fn类型的函数,并使用tcp_accept注册到侦听控制块的accept字段。这个函数在服务器上有新连接建立时被内核调用,参数中的newpcb是新连接对应的控制块,用户的accept函数中可以对这个新的控制块进行后续处理,arg是用户自定义的一个指针参数,由tcp_arg注册。
6.参数注册函数—tcp_arg
函数原型:void tcp_arg(struct tcp_pcb *pcb, void *arg)
该函数应该在连接建立后立即调用,用来向控制块pcb的callback_arg字段注册一个参数arg,当内核调用该控制块上的任何一个回调函数时,arg将作为其中的一个参数传入。用户可以通过这种简单的方式将自己关心的数据传递到各个回调函数中,方便在回调函数中访问和处理。
7.控制块连接—tcp_connect
函数原型:err_t tcp_connect(struct tcp_pcb *pcb, ip_addr_t *ipaddr, u16_t port, tcp_connected_fn connected)
对于客户端程序来说,它需要执行主动打开操作,主动打开就是向服务器发送一个 SYN 握手报文段,这一点是通过调用函数 tcp_connect 来实现的。其中参数ipaddr表示服务器的IP地址,port表示服务器开放的熟知端口号,在发送连接请求前,内核会为pcb绑定一个短暂的端口号用于通信。客户端调用该函数后,对应控制块从链表 tcp_bound_pcbs 搬到tcp_active_pcbs,同时控制块状态设置为 SYN_SENT 状态,并初始化发送、接收相关的字段,最后调用函数 tcp_enqueue 为控制块组装一个握手报文,并调用函数 tcp_output 将报文发送出去。
如果此时服务器处于侦听状态,则经过三次握手后,服务器和客户端之间的连接就建立了,三次握手的细节由内核自动完成,用户程序无需关心。只需要在调用tcp_connect的时候,注册一个tcp_connected_fn类型的回调函数,当连接建立后,内核会回调这个注册的connected函数。与前面相对应,在服务器端,当新连接建立后被回调的函数是accept函数。
8.connected回调函数类型—tcp_connected_fn
函数原型:err_t (*tcp_connected_fn)(void *arg, struct tcp_pcb *tpcb, err_t err)
在客户端,当一个连接建立后,内核会回调用户注册的connected函数,参数中的tpcb为当前已经处于ESTABLISHED状态的控制块,arg为用户自定义的一个指针参数,由 tcp_arg注册。
9.数据发送—tcp_write
函数原型:err_t tcp_write(struct tcp_pcb *pcb, const void *dataptr, u16_t len, u8_t apiflags)
当一个连接建立起来后,双方都可以通过调用函数tcp_write向对方发送数据,在数据的接收方,若用户想处理接收到的数据,则必须向控制块的recv字段注册自定义处理函数,当内核收到关于某连接的数据后,对应控制块中注册的recv函数会被内核回调执行,这点和UDP中的回调执行完全相同。
在tcp_write函数中,pcb指向相应连接的控制块,dataptr指向待发送数据的起始地址,len表示待发送数据的长度,apiflags指明数据是否进行拷贝,以及报文段首部是否设置PSH标志等。这里与UDP不同的是,TCP的数据并不需要应用程序将其封装在pbuf中,因为这些数据需要在TCP内核中缓冲或重发,TCP有自己的一套数据封装机制来缓存这些数据,当TCP内核发送数据时,内核会自动将数据封装在pbuf中并递交给IP层。另外,tcp_write只负责为待发送数据构造TCP报文段,并将报文段挂载到控制块的unsent队列中,不负责数据的立即发送,只有在TCP定时器超时后,内核会统一将这些未发送的数据依次发送出去,如果想立即发送可以通过调用tcp_output函数实现。
10.连接sent函数注册—tcp_sent
函数原型:void tcp_sent(struct tcp_pcb *pcb, tcp_sent_fn sent)
该函数将一个tcp_sent_fn类型的函数注册到控制块pcb的sent字段,当内核发送的数据被对方确认后,内核会将TCP发送滑动窗口往后移动,同时回调sent函数告诉应用程序哪些数据正确地被对方接收了,此时用户可以将那些不使用的已发送数据删除,或者应用程序可以选择发送其他待发送的数据。
11.sent回调函数类型—tcp_sent_fn
函数原型:err_t (*tcp_sent_fn)(void *arg, struct tcp_pcb *tpcb, u16_t len)
当连接上有数据被发送出去后,用户注册的tcp_sent_fn类型的sent函数被调用,其中参数tpcb表示对应的连接控制块,len表示这次被成功发送出去的数据长度。在这个函数中,用户可以继续调用tcp_output在连接上发送更多的数据。
12.连接recv函数注册—tcp_recv
函数原型:void tcp_recv(struct tcp_pcb *pcb, tcp_recv_fn recv)
该函数是将一个tcp_recv_fn类型的函数注册到控制块pcb的recv字段,当内核在连接上接收到数据后,注册的recv函数被内核回调,用于处理连接上的数据。函数tcp_recv一般需要在连接建立后立即调用,以注册recv函数到内核中,因此对于客户端程序来说,tcp_recv一般在connected中调用,而对于服务器来说,tcp_recv则通常在accept函数中调用。
13.recv回调函数类型—tcp_recv_fn
函数原型:err_t (*tcp_recv_fn)(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err)
这是Raw API编程中最重要的函数,用户需要在这个函数中完成对接收数据的处理。当内核在一个连接上接收到数据后,会回调这个函数,其中参数tpcb对应接收数据的连接,而所接收到的数据被封装到了pbuf中,用户根据自己的应用完成pbuf的处理。需要注意的是,当内核检测到对方主动关闭连接时,也会回调recv函数,此时参数p为NULL,如上图中的步骤10所示,应用程序应及时处理这种连接断开的情况,比如完成数据处理后调用tcp_close来关闭本地到对端的连接。
14.更新接收窗口函数—tcp_recved
函数原型:void tcp_recved(struct tcp_pcb *pcb, u16_t len)
在recv函数中,当应用程序完成对数据的处理后,连接的任何一方需要调用tcp_recved函数告诉内核:我已经成功的处理完这个数据,你可以删除这个数据并继续接收其他数据了。在LWIP内核中,这个函数的本质是更新连接接收窗口的大小,以便向对方通告更大的接收窗口,使得对方可以继续传送数据。参数pcb表示对应的连接控制块,len表示应用程序已正常处理完成的数据长度,内核会将接收窗口增大len个字节。
15.关闭连接—tcp_close
函数原型:err_t tcp_close(struct tcp_pcb *pcb)
在连接建立后,任何一方都可以调用tcp_close来关闭连接,首先发起tcp_close调用的一方为主动关闭方,该函数调用后,主动关闭方会进入到FIN_WAIT_1状态。此时连接的另一方(被动关闭方)会接收到一个FIN报文,此时被动关闭方内核会调用recv函数,其中参数p为NULL,在被动关闭方的recv函数中,一般会调用tcp_close来关闭被动关闭方到主动关闭方的连接,完成断开连接的“四次握手”过程。
16.错误处理函数—tcp_err
函数原型:void tcp_err(struct tcp_pcb *pcb, tcp_err_fn err)
该函数的作用是向控制块pcb的errf字段注册一个tcp_err_fn类型的函数err,用户程序需要完成err程序的编写。这个err函数主要完成在连接异常结束时的一些处理,比如释放一些必须释放的资源。在内核调用该函数时,对应的连接已断开,且控制块已被删除,用户可以在该函数中选择重新建立控制块并重新发起连接,保证连接的有效性。
17.Errf回调函数类型—tcp_err_fn
函数原型:void (*tcp_err_fn)(void *arg, err_t err)
当连接被对方复位,或者内核内部定时器处理检测到连接上的任何错误时,控制块会被释放,同时用户注册的errf函数会被调用,参数arg为用户自定义的一个指针参数,由 tcp_arg注册。参数err的可能传入值为:ERR_RST表示连接被对端复位;ERR_ABRT表示内核检测到连接的异常,从而断开了连接。Errf函数为了保证应用程序的可靠性和连接的稳定性,必须对这个错误的信息做出正确的处理,在客户端中常见的做法是在该函数中重新建立控制块,并向服务器重新发起连接。
18.内核周期性事件—tcp_poll / tcp_poll_fn
函数原型:void tcp_poll(struct tcp_pcb *pcb, tcp_poll_fn poll, u8_t interval);
err_t (*tcp_poll_fn)(void *arg, struct tcp_pcb *tpcb)
该函数在连接控制块pcb的poll字段注册一个类型为tcp_poll_fn的函数poll,内核会周期性的调用控制块的poll函数,调用周期为interval * 0.5s(0.5s为TCP内核的定时器处理周期)。用户应用程序可以使用poll函数来完成一些周期性的事件,比如检测连接的状态,当连接异常时,断开连接并重新建立连接,从而保证连接的稳定性;另一方面,用户还可以在poll函数中周期性的向对端发送数据,而不必基于内核的发送/接收回调来发送数据。
以上就是TCP协议Raw API的所有接口函数,在实际使用中会分为客户端设计和服务器设计两部分,所以实际使用到的API函数并不多。由于Raw编程是基于各个内核事件的,要编出高效且稳定性好的程序,读者必须对内核的各个事件有详细的了解和全面的把握,明白申请情况下内核会回调哪个函数,只有这样才能保证应用程序的准确性。
下面将利用这些Raw API实现一个简单的HTTP服务器,HTTP服务器实现了一个最简单的功能:在本地打开端口80接受客户端的连接,当客户端连接成功后,接受客户端的html请求,并向客户端返回固定的html网页数据,然后主动断开到客户端的连接。该服务器的实现代码如下:
#define HTTP_PORT 80 const unsigned char htmldata[] = " \ <html> \ <head><title> A LWIP WebServer !!</title></head> \ <center><p>A WebServer Based on LWIP v1.4.1!</center>\ </html>"; /** *@code for a http server *@send HTTP data to any connected client, and then close connection */ static err_t http_recv(void *arg, struct tcp_pcb *pcb, struct pbuf *p, err_t err) { char *data = NULL; /* We perform here any necessary processing on the pbuf */ if (p != NULL) { /* We call this function to tell the LWIP that we have processed the data */ /* This lets the stack advertise a larger window, so more data can be received*/ tcp_recved(pcb, p->tot_len); data = p->payload; if(p->len >=3 && data[0] == 'G'&& data[1] == 'E'&& data[2] == 'T') { tcp_write(pcb, htmldata, sizeof(htmldata), 1); } else { printf("Request error\n"); } pbuf_free(p); tcp_close(pcb); } else if (err == ERR_OK) { /* When the pbuf is NULL and the err is ERR_OK, the remote end is closing the connection. */ /* We free the allocated memory and we close the connection */ return tcp_close(pcb); } return ERR_OK; } /** * @brief This function when the Telnet connection is established * @param arg user supplied argument * @param pcb the tcp_pcb which accepted the connection * @param err error value * @retval ERR_OK */ static err_t http_accept(void *arg, struct tcp_pcb *pcb, err_t err) { tcp_recv(pcb, http_recv); return ERR_OK; } /** * @brief Initialize the http application * @param None * @retval None */ static void http_server_init(void) { struct tcp_pcb *pcb = NULL;
/* Create a new TCP control block */ pcb = tcp_new(); /* Assign to the new pcb a local IP address and a port number */ /* Using IP_ADDR_ANY allow the pcb to be used by any local interface */ tcp_bind(pcb, IP_ADDR_ANY, HTTP_PORT); /* Set the connection to the LISTEN state */ pcb = tcp_listen(pcb); /* Specify the function to be called when a connection is established */ tcp_accept(pcb, http_accept); } |
下面再利用这些Raw API实现一个简单的客户端程序,这里客户端程序的重点放到了异常的处理上,在遇到错误或异常后客户端如何重新发起连接来保证连接的可用性。客户端程序实现的功能为:一直尝试连接服务器,并在连接建立后向服务器发送一个GREETING信息,告诉服务器“我上线了”,之后它会将服务器发向自己的任何数据返回给服务器,实现一个简单的ECHO功能,当服务器断开到客户端的连接后,客户端会再次握手服务器并建立连接。该客户端的实现代码如下:
/** *@code for a echo client *@client will connect to server first,then send anything received out to server *@client will never close connection actively */ static void echo_client_conn_err(void *arg, err_t err) { printf("connect error! closed by core!!\n"); printf("try to connect to server again!!\n"); echo_client_init(); } static err_t echo_client_recv(void *arg, struct tcp_pcb *pcb, struct pbuf *p, err_t err) { /* We perform here any necessary processing on the pbuf */ if (p != NULL) { /* We call this function to tell the LWIP that we have processed the data */ /* This lets the stack advertise a larger window, so more data can be received*/ tcp_recved(pcb, p->tot_len); tcp_write(pcb, p->payload, p->len, 1); pbuf_free(p); } else if (err == ERR_OK) { /* When the pbuf is NULL and the err is ERR_OK, the remote end is closing the connection. */ /* We free the allocated memory and we close the connection */ tcp_close(pcb); echo_client_init(); return ERR_OK; } return ERR_OK; } static err_t echo_client_sent(void *arg, struct tcp_pcb *pcb, u16_t len) { printf("echo client send data OK! sent len = [%d]\n", len); return ERR_OK; } static err_t echo_client_connected(void *arg, struct tcp_pcb *pcb, err_t err) { char GREETING[] = "Hi, I am a new Client!\n"; tcp_recv(pcb, echo_client_recv); tcp_sent(pcb, echo_client_sent); tcp_write(pcb, GREETING, sizeof(GREETING), 1); return ERR_OK; } static void echo_client_init(void) { struct tcp_pcb *pcb = NULL; struct ip_addr server_ip; /* Create a new TCP control block */ pcb = tcp_new(); IP4_ADDR(&server_ip, 192,168,1,11); tcp_connect(pcb, &server_ip, 21, echo_client_connected); tcp_err(pcb, echo_client_conn_err); } |
后面会介绍协议栈在有操作系统模拟层下运行,这时协议栈内核和用户程序就可以处在两个相互独立进程中进行,此时上面提到的制约现象将不再存在,协议栈进程接收到一个数据包后,可以将数据包通过邮箱的方式传递给用户进程,协议栈进程可以不被阻塞,仍然继续接收、处理下一个数据包。通常每个连接结构上都有缓冲队列,数据包会被协议栈挂接在每个结构的缓冲队列上供用户程序取用,这样就避免了数据包丢失的情况。
结语
本章为LWIP协议栈解析专栏的最后一章,本人后续会发布一篇实战文章《uboot移植LWIP实现FTP下载文件》,感兴趣的读者可移步阅读。感谢支持,你们的鼓励是我更新的动力!