使用 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
本节讲述 recv
回调函数。
recv
回调函数
在 TCP 控制块中,函数指针 recv
指向用户实现的函数,当接收到有效数据时,由协议栈调用此函数,通知用户处理接收到的数据。
函数指针 recv
的类型为 tcp_recv_fn
,该类型定义在 tcp.h 中:
/** Function prototype for tcp receive callback functions. Called when data has
* been received.
*
* @param arg Additional argument to pass to the callback function (@see tcp_arg())
* @param tpcb The connection pcb which received data
* @param p The received data (or NULL when the connection has been closed!)
* @param err An error code if there has been an error receiving
* Only return ERR_ABRT if you have called tcp_abort from within the
* callback function!
*/
typedef err_t (*tcp_recv_fn)(void *arg, struct tcp_pcb *tpcb,
struct pbuf *p, err_t err);
协议栈通过宏 TCP_EVENT_RECV(pcb,p,err,ret)
调用 pcb->recv
指向的函数。宏 TCP_EVENT_RECV
定义在 tcp_priv.h
中:
#define TCP_EVENT_RECV(pcb,p,err,ret) \
do { \
if((pcb)->recv != NULL) { \
(ret) = (pcb)->recv((pcb)->callback_arg,(pcb),(p),(err));\
} else { \
(ret) = tcp_recv_null(NULL, (pcb), (p), (err)); \
} \
} while (0)
以关键字 TCP_EVENT_RECV
搜索源码,可以搜索到 2 处使用:
TCP_EVENT_RECV(pcb, recv_data, ERR_OK, err);
TCP_EVENT_RECV(pcb, refused_data, ERR_OK, err);
1 由 tcp_input
函数调用
指针 recv_data
是一个 struct pbuf
类型的指针,定义在 tcp_in.c
文件中,是一个静态变量:
static struct pbuf *recv_data;
经过 tcp_process
函数处理后,如果接收到有效数据,则指针 recv_data
指向数据 pbuf
,此时协议栈通过宏 TCP_EVENT_RECV
调用用户编写的数据处理函数。
简化后的代码为:
void
tcp_input(struct pbuf *p, struct netif *inp)
{
// 经过一系列检测,没有错误
/* 在本地找到有效的控制块 pcb */
if (pcb != NULL) {
err = tcp_process(pcb);
/* A return value of ERR_ABRT means that tcp_abort() was called
and that the pcb has been freed. If so, we don't do anything. */
if (err != ERR_ABRT) {
if (recv_data != NULL) {
/* Notify application that data has been received. */
TCP_EVENT_RECV(pcb, recv_data, ERR_OK, err);
if (err == ERR_ABRT) {
goto aborted;
}
/* If the upper layer can't receive this data, store it */
if (err != ERR_OK) {
pcb->refused_data = recv_data;
LWIP_DEBUGF(TCP_INPUT_DEBUG, ("tcp_input: keep incoming packet, because pcb is \"full\"\n"));
}
}
/* Try to send something out. */
tcp_output(pcb); // <--- 注意这里调用了发送函数,所以 recv 回调函数就没必要再调用这个函数
}
}
}
从以上代码中可以看出:
- 回调函数有返回值,若发现异常,用户层可以主动调用
tcp_abort
函数终止连接,然后返回ERR_ABRT
错误码,协议栈会完成后续的操作:
/* Notify application that data has been received. */
TCP_EVENT_RECV(pcb, recv_data, ERR_OK, err);
if (err == ERR_ABRT) {
goto aborted;
}
- 如果正确的处理了数据,回调函数必须返回
ERR_OK
错误码,否则协议栈会认为用户没有接收这包数据,就会对它进行缓存:
/* If the upper layer can't receive this data, store it */
if (err != ERR_OK) {
pcb->refused_data = recv_data;
LWIP_DEBUGF(TCP_INPUT_DEBUG, ("tcp_input: keep incoming packet, because pcb is \"full\"\n"));
}
所以上层如果来不及处理数据,可以让协议栈暂存。这里暂存数据使用了指针 pcb->refused_data
,需要注意一下,因为接下来会再次看到它。
- 注意这里会调用 TCP 发送函数:
/* Try to send something out. */
tcp_output(pcb);
在 recv
回调函数中,处理完接收到的数据后,通常我们还会调用 tcp_write
函数回送数据。函数原型为:
/**
* @ingroup tcp_raw
* Write data for sending (but does not send it immediately).
*
* It waits in the expectation of more data being sent soon (as
* it can send them more efficiently by combining them together).
* To prompt the system to send data now, call tcp_output() after
* calling tcp_write().
*
* This function enqueues the data pointed to by the argument dataptr. The length of
* the data is passed as the len parameter. The apiflags can be one or more of:
* - TCP_WRITE_FLAG_COPY: indicates whether the new memory should be allocated
* for the data to be copied into. If this flag is not given, no new memory
* should be allocated and the data should only be referenced by pointer. This
* also means that the memory behind dataptr must not change until the data is
* ACKed by the remote host
* - TCP_WRITE_FLAG_MORE: indicates that more data follows. If this is omitted,
* the PSH flag is set in the last segment created by this call to tcp_write.
* If this flag is given, the PSH flag is not set.
*
* The tcp_write() function will fail and return ERR_MEM if the length
* of the data exceeds the current send buffer size or if the length of
* the queue of outgoing segment is larger than the upper limit defined
* in lwipopts.h. The number of bytes available in the output queue can
* be retrieved with the tcp_sndbuf() function.
*
* The proper way to use this function is to call the function with at
* most tcp_sndbuf() bytes of data. If the function returns ERR_MEM,
* the application should wait until some of the currently enqueued
* data has been successfully received by the other host and try again.
*
* @param pcb Protocol control block for the TCP connection to enqueue data for.
* @param arg Pointer to the data to be enqueued for sending.
* @param len Data length in bytes
* @param apiflags combination of following flags :
* - TCP_WRITE_FLAG_COPY (0x01) data will be copied into memory belonging to the stack
* - TCP_WRITE_FLAG_MORE (0x02) for TCP connection, PSH flag will not be set on last segment sent,
* @return ERR_OK if enqueued, another err_t on error
*/
err_t
tcp_write(struct tcp_pcb *pcb, const void *arg, u16_t len, u8_t apiflags)
通过注释可以得知,这个函数会尽可能把发送的数据组合在一起,然后一次性发送出去,因为这样更有效率。换句话说,调用这个函数并不会立即发送数据,如果希望立即发送数据,需要在调用 tcp_write
函数之后调用 tcp_output
函数。
而现在我们又知道了,在 tcp_input
函数中,调用 recv
回调函数后,协议栈会执行一次 tcp_output
函数,这就是我们在 recv
回调函数中调用 tcp_write
函数能够立即将数据发送出去的原因!
2 由 tcp_process_refused_data
函数调用
在上一节提到 “上层如果来不及处理数据,可以让协议栈暂存。这里暂存数据使用了指针 pcb->refused_data
”,而 tcp_process_refused_data
函数就是把暂存的数据重新提交给应用层处理。提交的方法是调用 recv
回调函数,简化后的代码为:
err_t
tcp_process_refused_data(struct tcp_pcb *pcb)
{
/* set pcb->refused_data to NULL in case the callback frees it and then
closes the pcb */
struct pbuf *refused_data = pcb->refused_data;
pcb->refused_data = NULL;
/* Notify again application with data previously received. */
TCP_EVENT_RECV(pcb, refused_data, ERR_OK, err);
if (err == ERR_ABRT) {
return ERR_ABRT;
} else if(err != ERR_OK){
/* data is still refused, pbuf is still valid (go on for ACK-only packets) */
pcb->refused_data = refused_data;
return ERR_INPROGRESS;
}
return ERR_OK;
}
协议栈会在两处调用 tcp_process_refused_data
函数。
2.1 在 tcp_input
函数中调用
void
tcp_input(struct pbuf *p, struct netif *inp)
{
// 经过一系列检测,没有错误
/* 在本地找到有效的控制块 pcb */
if (pcb != NULL) {
/* If there is data which was previously "refused" by upper layer */
if (pcb->refused_data != NULL) {
if ((tcp_process_refused_data(pcb) == ERR_ABRT) || // <--- 这里
((pcb->refused_data != NULL) && (tcplen > 0))) {
/* pcb has been aborted or refused data is still refused and the new segment contains data */
if (pcb->rcv_ann_wnd == 0) {
/* this is a zero-window probe, we respond to it with current RCV.NXT
and drop the data segment */
tcp_send_empty_ack(pcb);
}
goto aborted;
}
}
err = tcp_process(pcb);
/* A return value of ERR_ABRT means that tcp_abort() was called
and that the pcb has been freed. If so, we don't do anything. */
if (err != ERR_ABRT) {
if (recv_data != NULL) {
/* Notify application that data has been received. */
TCP_EVENT_RECV(pcb, recv_data, ERR_OK, err);
if (err == ERR_ABRT) {
goto aborted;
}
/* If the upper layer can't receive this data, store it */
if (err != ERR_OK) {
pcb->refused_data = recv_data;
}
}
/* Try to send something out. */
tcp_output(pcb);
}
}
}
通过以上代码可以知道:
- 在处理接收数据之前,先检查一下是否有上次暂存的数据,如果有则调用
tcp_process_refused_data
函数,将暂存数据上报给应用层处理。 - 无论上层有多少数据没有处理,协议栈只暂存最后一次接收且上层没有处理的数据:
/* If the upper layer can't receive this data, store it */
if (err != ERR_OK) {
pcb->refused_data = recv_data;
}
2.2 在 tcp_fasttmr
函数中调用
协议栈每隔 TCP_TMR_INTERVAL
(默认 250)毫秒调用一次 tcp_fasttmr
函数,在这个函数中会检查 TCP_PCB 是否有尚未给上层应用处理的暂存数据,如果有则调用 tcp_process_refused_data
函数,将暂存数据上报给应用层处理。简化后的代码为:
void
tcp_fasttmr(void)
{
++tcp_timer_ctr;
tcp_fasttmr_start:
pcb = tcp_active_pcbs;
while (pcb != NULL) {
if (pcb->last_timer != tcp_timer_ctr) {
next = pcb->next;
/* If there is data which was previously "refused" by upper layer */
if (pcb->refused_data != NULL) {
tcp_active_pcbs_changed = 0;
tcp_process_refused_data(pcb); // <--- 这里
if (tcp_active_pcbs_changed) {
/* application callback has changed the pcb list: restart the loop */
goto tcp_fasttmr_start;
}
}
pcb = next;
} else {
pcb = pcb->next;
}
}
}
3 recv
函数的复用行为
前面看到了错误回调函数、连接成功回调函数、接收到数据回调函数,后面还会看到发送成功回调函数等。那么我们合理推测,应该也有连接关闭回调函数。在连接关闭时,协议栈确实回调了一个函数,但这个函数也是 recv
回调函数!协议栈并没有提供单独的连接关闭回调函数,而是复用了 recv
回调函数。协议栈使用宏 TCP_EVENT_CLOSED
封装了这一过程,代码为:
#define TCP_EVENT_CLOSED(pcb,ret) \
do { \
if(((pcb)->recv != NULL)) { \
(ret) = (pcb)->recv((pcb)->callback_arg,(pcb),NULL,ERR_OK);\
} else { \
(ret) = ERR_OK; \
} \
} while (0)
注意调用 recv
函数时,第 3 个参数为 NULL
,这很重要。我们又知道,recv
的原型为:
typedef err_t (*tcp_recv_fn)(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err);
所以第三个参数是 struct pbuf
型指针。
也就是说,我们必须在 recv
回调函数中处理 pbuf
指针为 NULL
的特殊情况,这表示远端主动关闭了连接,这时我们应主动调用 tcp_close
函数,关闭本地连接。一个典型的 recv
回调函数框架为:
static err_t
app_recv(void *arg, struct tcp_pcb *pcb, struct pbuf *p, err_t err)
{
if (p == NULL) {
// 连接关闭前的处理,可选
tcp_close(pcb);
} else {
if (err != ERR_OK) {
// 目前还没有使用 ERR_OK 之外的回调参数,这里兼容以后的协议栈
pbuf_free(p);
return err;
}
// 更新窗口值,必须调用
tcp_recved(pcb,p->tot_len);
// 在这里处理接收到的数据
// 释放 pbuf,必须
pbuf_free(p);
}
return ERR_OK;
}
协议栈在 tci_input
函数中调用宏 TCP_EVENT_CLOSED
,简化后的代码为:
void
tcp_input(struct pbuf *p, struct netif *inp)
{
// 经过一系列检测,没有错误
/* 在本地找到有效的控制块 pcb */
if (pcb != NULL) {
err = tcp_process(pcb);
if (err != ERR_ABRT) {
if (recv_flags & TF_RESET) {
// 收到 RST 标志,回调 errf 函数
TCP_EVENT_ERR(pcb->state, pcb->errf, pcb->callback_arg, ERR_RST);
tcp_pcb_remove(&tcp_active_pcbs, pcb);
tcp_free(pcb);
} else {
if (recv_acked > 0) {
// 收到数据 ACK 应答,回调 sent 函数
TCP_EVENT_SENT(pcb, (u16_t)acked16, err);
if (err == ERR_ABRT) {
goto aborted;
}
recv_acked = 0;
}
if (recv_data != NULL) {
// 收到有效数据, 回调 recv 函数
TCP_EVENT_RECV(pcb, recv_data, ERR_OK, err);
if (err == ERR_ABRT) {
goto aborted;
}
}
if (recv_flags & TF_GOT_FIN) {
// 收到 FIN 标志,回调 recv 函数,远端关闭连接
TCP_EVENT_CLOSED(pcb, err); // <--- 这里
if (err == ERR_ABRT) {
goto aborted;
}
}
/* Try to send something out. */
tcp_output(pcb);
}
}
}
}
读后有收获,资助博主养娃 - 千金难买知识,但可以买好多奶粉 (〃‘▽’〃)