lwIP 细节之四:recv 回调函数是何时调用的

使用 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 calledand 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 回调函数就没必要再调用这个函数}} 
}

从以上代码中可以看出:

  1. 回调函数有返回值,若发现异常,用户层可以主动调用 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;
}
  1. 如果正确的处理了数据,回调函数必须返回 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 ,需要注意一下,因为接下来会再次看到它。

  1. 注意这里会调用 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 thencloses 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.NXTand 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 calledand 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);}} 
}

通过以上代码可以知道:

  1. 在处理接收数据之前,先检查一下是否有上次暂存的数据,如果有则调用 tcp_process_refused_data 函数,将暂存数据上报给应用层处理。
  2. 无论上层有多少数据没有处理,协议栈只暂存最后一次接收且上层没有处理的数据:
/* 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);}}} 
}






读后有收获,资助博主养娃 - 千金难买知识,但可以买好多奶粉 (〃‘▽’〃)
千金难买知识,但可以买好多奶粉

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/218276.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

常用whl文件地址整理

文章目录 一、Deep Graph Library&#xff08;DGL&#xff09;二、torch torchvision torchaudio三、numpy四、pandas可留言其他whl文件地址&#xff0c;不定期更新 一、Deep Graph Library&#xff08;DGL&#xff09; DGL是一个专门用于深度学习图形的Python包, 一款面向图神…

代码随想录算法训练营第50天| 123.买卖股票的最佳时机III 188.买卖股票的最佳时机IV

JAVA代码编写 123.买卖股票的最佳时机III 给定一个数组&#xff0c;它的第 i 个元素是一支给定的股票在第 i 天的价格。 设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。 **注意&#xff1a;**你不能同时参与多笔交易&#xff08;你必须在再次购买前出…

HDPE硅芯管材具有优异的耐腐蚀性、耐磨损性和耐老化性

HDPE硅芯管材作为一种优质的管道材料&#xff0c;具有许多突出的性能。其中&#xff0c;其优异的耐腐蚀性、耐磨损性和耐老化性是其主要特点之一。 首先&#xff0c;HDPE硅芯管材具有出色的耐腐蚀性。它的高密度聚乙烯&#xff08;HDPE&#xff09;材料具有良好的耐腐蚀性能&a…

2023快速上手新红利项目:短剧分销推广CPS

短剧分销推广CPS是一个新红利项目&#xff0c;对于新手小白来说也可以快速上手。 以下是一些建议&#xff0c;帮助新手小白更好地进行短剧分销推广CPS&#xff1a; 学习基础知识&#xff1a;了解短剧的基本概念、制作流程和推广方式。了解短剧的市场需求和受众群体&#xff0c…

STM32F030C8读取CS1237采集模拟

STM32F030C8读取CS1237采集模拟 Chapter1 【问题解决记录】STM32F030C8读取CS1237采集模拟问题描述原因分析&#xff1a;解决方案&#xff1a; Chapter2 CS1237 STM32控制程序以及原理图需要注意事项 Chapter1 【问题解决记录】STM32F030C8读取CS1237采集模拟 原文链接&#x…

【技术分享】常见VLAN部署方式

VLAN部署方式&#xff1a; 第一种End-to-End VLAN&#xff08;端到端VLAN&#xff09; 全局部署的VLAN&#xff0c;VLAN信息可以扩展到整个网络&#xff08;换句话说就是每台交换机上VLAN信息一致&#xff09; 将用户分组到与物理位置无关的VLAN中&#xff1b;如果用户在园区…

第7章:深度剖析知识图谱中的知识推理:方法与应用探究

&#x1f497;&#x1f497;&#x1f497;欢迎来到我的博客&#xff0c;你将找到有关如何使用技术解决问题的文章&#xff0c;也会找到某个技术的学习路线。无论你是何种职业&#xff0c;我都希望我的博客对你有所帮助。最后不要忘记订阅我的博客以获取最新文章&#xff0c;也欢…

《算法通关村——回溯模板如何解决回溯问题》

《算法通关村——回溯模板如何解决回溯问题》 93. 复原 IP 地址 有效 IP 地址 正好由四个整数&#xff08;每个整数位于 0 到 255 之间组成&#xff0c;且不能含有前导 0&#xff09;&#xff0c;整数之间用 . 分隔。 例如&#xff1a;"0.1.2.201" 和 "192.1…

【活动回顾】ABeam News | 兰州大学外国语学院回访ABeam 旗下德硕管理咨询(上海),持续推进远景合作

访企拓岗深入调研 持续推进远景合作 继11月上旬ABeam旗下艾宾信息技术开发&#xff08;西安&#xff09;团队一行拜访兰州大学并举行隆重的校企签约仪式后&#xff0c;近日兰州大学一行领导也如约莅临德硕管理咨询&#xff08;上海&#xff09;有限公司开展拓岗调研。 深化…

线上业务优化之案例实战

本文是我从业多年开发生涯中针对线上业务的处理经验总结而来&#xff0c;这些业务或多或少相信大家都遇到过&#xff0c;因此在这里分享给大家&#xff0c;大家也可以看看是不是遇到过类似场景。本文大纲如下&#xff0c; 后台上传文件 线上后台项目有一个消息推送的功能&#…

实物+3D动画展示离心式过滤器的工作原理 #雨水收集#雨水过滤

产品规格型号 规格型号&#xff1a;LLLXGL-100、LLLXGL-150、LLLXGL-200、LLLXGL-300

第一届古剑山ctf-pwn全部题解

1. choice 附件&#xff1a; https://github.com/chounana/ctf/blob/main/2023%E7%AC%AC%E4%B8%80%E5%B1%8A%E5%8F%A4%E5%89%91%E5%B1%B1pwn/choice.zip 漏洞代码&#xff1a; 漏洞成因&#xff1a; byte_804A04C输入的长度可以覆盖nbytes的值&#xff0c;导致后面输入时存…

RFID复习内容整理

第一章 日常生活中的RFID技术 身份证&#xff08;高频&#xff09; typeB13.56MHz 一卡通&#xff08;高频&#xff09; ISO/IEC 14443 typeA 图书馆门禁停车场门票ETC 微波段、超高频 服装快销品牌 物联网定义 最初的定义 将各种信息传感设备&#xff0c;如射频识别(RFID)…

会JSX没什么了不起,你了解过 StyleX 么?

近日&#xff0c;Meta开源了一款CSS-in-JS库 —— StyleX。看命名方式&#xff0c;Style - X是不是有点像JS - X&#xff0c;他们有关系么&#xff1f;当然有。 JSX是一种用JS描述HTML的语法规范&#xff0c;广泛应用于前端框架中&#xff08;比如React、SolidJS...&#xff0…

公众号怎么提高2个限制

一般可以申请多少个公众号&#xff1f;许多用户在申请公众号时可能会遇到“公众号显示主体已达上限”的问题。这是因为在2018年11月16日对公众号申请数量进行了调整&#xff0c;具体调整如下&#xff1a;1、个人主体申请公众号数量上限从2个调整为1个。2、企业主体申请公众号数…

【LeetCode:2697. 字典序最小回文串 | 双指针 + 贪心】

&#x1f680; 算法题 &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&#xff0c;…

静态HTTP应用:理解其工作原理与优势

随着互联网的普及和发展&#xff0c;Web应用已经成为人们日常生活和工作中不可或缺的一部分。而静态HTTP应用作为Web应用的一种重要形式&#xff0c;也越来越受到开发者的青睐。本文将带你了解静态HTTP应用的工作原理和优势&#xff0c;让你更好地理解这种应用形式。 一、静态…

binlog+mysqldump恢复数据(误删数据库或者表)

表删除恢复 1、准备数据 首先准备数据库环境&#xff0c;测试数据库为speech1&#xff0c;如下&#xff1a; 为test数据表添加3条记录&#xff0c;如下&#xff1a;三行为新加的记录&#xff0c;添加后将test表删除。 2、恢复数据 查看binlog日志状态 SHOW MASTER STATUS…

多线程案例-定时器(附完整代码)

定时器是什么 定时器是软件开发中的一个重要组件.类似于一个"闹钟".达到一个设定的时间之后,就执行某个指定好的代码. 定时器是一种实际开发中非常常用的组件. 比如网络通信种,如果对方500ms内没有返回数据,则断开尝试重连. 比如一个Map,希望里面的某个key在3s之后过…

uniapp+vite+ts+express踩坑总结

1 关于引入express包报 import express from "express"; ^^^^^^ SyntaxError: Cannot use import statement outside a module的问题。 解决方案&#xff1a; 在package.json中添加type&#xff1a;“module”选项 2 Response is a type and must be imported …