使用 lwIP 协议栈进行 TCP 裸机编程
,其本质就是编写协议栈指定的各种回调函数。将你的应用逻辑封装成函数,注册到协议栈,在适当的时候,由协议栈自动调用,所以称为回调。
注:除非特别说明,以下内容针对 lwIP 2.0.0 及以上版本。
向协议栈注册回调函数有专门的接口,如下所示:
tcp_err(pcb, errf); //注册 TCP 接到 RST 标志或发生错误回调函数 errf
tcp_connect(pcb, ipaddr, port, connected); //注册 TCP 建立连接成功回调函数 connecter
tcp_accept(pcb, accept); //注册 TCP 处于 LISTEN 状态时,监听到有新的连接接入
tcp_recv(pcb, recv); //注册 TCP 接收到数据回调函数 recv
tcp_sent(pcb, sent); //注册 TCP 发送数据成功回调函数 sent
tcp_poll(pcb, poll, interval); //注册 TCP 周期性执行回调函数 poll
本节讲述 accept
函数。
accept
回调函数
在 TCP 控制块中,函数指针 accept
指向用户实现的函数,当监听到有新的连接接入时,由协议栈调用此函数,通知用户接受了新的连接或者通知用户内存不足。
函数指针 accept
的类型为 tcp_accept_fn
,该类型定义在 tcp.h 中:
/** Function prototype for tcp accept callback functions. Called when a new
* connection can be accepted on a listening pcb.
*
* @param arg Additional argument to pass to the callback function (@see tcp_arg())
* @param newpcb The new connection pcb
* @param err An error code if there has been an error accepting.
* Only return ERR_ABRT if you have called tcp_abort from within the
* callback function!
*/
typedef err_t (*tcp_accept_fn)(void *arg, struct tcp_pcb *newpcb, err_t err);
协议栈通过宏 TCP_EVENT_ACCEPT(lpcb,pcb,arg,err,ret)
调用 lpcb->accept
指向的函数。宏 TCP_EVENT_ACCEPT
定义在 tcp_priv.h
中:
#define TCP_EVENT_ACCEPT(lpcb,pcb,arg,err,ret) \
do { \
if((lpcb)->accept != NULL) \
(ret) = (lpcb)->accept((arg),(pcb),(err)); \
else (ret) = ERR_ARG; \
} while (0)
以关键字 TCP_EVENT_ACCEPT
搜索源码,可以搜索到 2 处使用:
TCP_EVENT_ACCEPT(pcb, NULL, pcb->callback_arg, ERR_MEM, err);
TCP_EVENT_ACCEPT(pcb->listener, pcb, pcb->callback_arg, ERR_OK, err);
1 由 tcp_listen_input
函数调用
处于 LISTEN
状态的 TCP 控制块 ,如果收到客户端发送的 SYN
同步标志,表示一个客户端在请求建立连接了。
lwIP 会为这个新连接申请一个 TCP_PCB
,这一过程在 tcp_listen_input
函数中完成的。然而 TCP_PCB
的个数是有限的,如果申请失败,则会调用错误码为 ERR_MEM
的 accept
回调函数,向用户报告内存分配失败。简化后的代码为:
static void
tcp_listen_input(struct tcp_pcb_listen *pcb)
{
// 通过一系列检查 没有错误
npcb = tcp_alloc(pcb->prio); // 申请新的 TCP_PCB
if (npcb == NULL) { // 内存错误处理
LWIP_DEBUGF(TCP_DEBUG, ("tcp_listen_input: could not allocate PCB\n"));
TCP_EVENT_ACCEPT(pcb, NULL, pcb->callback_arg, ERR_MEM, err);
return;
}
// 申请成功,初始化新申请的pcb
npcb->state = SYN_RCVD;
// 发送 ACK|SYN 标志
return;
}
这里需要注意,申请 TCP_PCB
失败的处理方法,lwIP 2.1.x
版本与 lwIP 1.4.1
不同。
再看看 lwIP 1.4.1 的 tcp_listen_input
函数代码(经简化):
static err_t
tcp_listen_input(struct tcp_pcb_listen *pcb)
{
// 通过一系列检查 没有错误
npcb = tcp_alloc(pcb->prio); // 申请新的 TCP_PCB
if (npcb == NULL) { // 内存错误处理
LWIP_DEBUGF(TCP_DEBUG, ("tcp_listen_input: could not allocate PCB\n"));
return ERR_MEM;
}
// 申请成功,初始化新申请的pcb
// 发送 ACK|SYN 标志
return ERR_OK;
}
可以看到, lwIP 1.4.1
版本 tcp_listen_input
函数具有返回值,如果申请 TCP_PCB 失败,则返回 ERR_MEM
错误码。而 lwIP 2.1.x
版本 tcp_listen_input
函数不具有返回值(返回类型为 void
),其次,lwIP 2.1.x
版本处理内存错误是通过调用 accept
回调函数来实现的。宏展开代码(简化后)如下所示,注意第二个参数为 NULL
,错误码为 ERR_MEM
:
if(pcb->accept != NULL)
pcb->accept(pcb->callback_arg, NULL, ERR_MEM);
这个功能最早是由 Simon Goldschmidt
在 2016-03-23 提交的,提交记录为:
tcp: call accept-callback with ERR_MEM when allocating a pcb fails on
passive open to inform the application about this error
ATTENTION: applications have to handle NULL pcb in accept callback!
tcp:在被动打开分配 pcb 失败时,使用 ERR_MEM 参数调用 accept 回调函数,以通知应用程序有关此错误
注意:应用程序必须在 accept 回调中处理 pcb 句柄为NULL
的情况!
这就告诉我们一个重要的信息:lwIP 2.1.x 版本的 accept
回调函数编写方式与 lwIP 1.4.1 版本不同。lwIP 2.1.x 版本的 accept
回调函数 必须 在 accept 回调中处理 pcb 句柄为 NULL
的情况!!举个例子。
lwIP 1.4.1 版本的 accept 回调函数可以这么写:
/* 客户端连接时, 回调此函数 */
static err_t telnet_accept(void *arg, struct tcp_pcb *pcb, err_t err)
{
char * p_link_info = "已连接到Telnet!\r\n";
tcp_recv(pcb,telnet_recv);
tcp_err(pcb,NULL);
pcb->so_options |= SOF_KEEPALIVE; //增加保活机制
tcp_write(pcb, p_link_info, strlen(p_link_info), TCP_WRITE_FLAG_COPY);
return ERR_OK;
}
而 lwIP 2.1.x 版本的accept 回调函数需要这么写:
/* 客户端连接时, 回调此函数 */
static err_t telnet_accept(void *arg, struct tcp_pcb *pcb, err_t err)
{
char * p_link_info = "已连接到Telnet!\r\n";
if(pcb == NULL)
{
if(err == ERR_MEM)
// 处理 TCP 连接个数不足,可选
return ERR_OK;
}
tcp_recv(pcb,telnet_recv);
tcp_err(pcb,NULL);
pcb->so_options |= SOF_KEEPALIVE; //增加保活机制
tcp_write(pcb, p_link_info, strlen(p_link_info), TCP_WRITE_FLAG_COPY);
return ERR_OK;
}
这里对 pcb
句柄是否为 NULL
做了处理,如果检测到 NULL
,accpet 回调函数需要提前退出!。
2 由 tcp_process
函数调用
处于 SYN_RCVD
状态的 TCP 控制块,如果接收的正确的 ACK
标志,则调用错误码为 ERR_OK
的 accept
回调函数,向用户报告接受了新的连接。简化后的代码为:
static err_t
tcp_process(struct tcp_pcb *pcb)
{
switch (pcb->state) {
case SYN_RCVD:
if (flags & TCP_ACK) {
/* expected ACK number? */
if (TCP_SEQ_BETWEEN(ackno, pcb->lastack + 1, pcb->snd_nxt)) {
pcb->state = ESTABLISHED;
/* Call the accept function. */
TCP_EVENT_ACCEPT(pcb->listener, pcb, pcb->callback_arg, ERR_OK, err);
if (err != ERR_OK) {
/* If the accept function returns with an error, we abort the connection. */
if (err != ERR_ABRT) {
tcp_abort(pcb);
}
return ERR_ABRT;
}
tcp_receive(pcb);
}
}
break;
}
return ERR_OK;
}
读后有收获,资助博主养娃 - 千金难买知识,但可以买好多奶粉 (〃‘▽’〃)