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

news2024/7/6 20:03:54

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

从以上代码中可以看出:

  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 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);
    }
  } 
}

通过以上代码可以知道:

  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.coloradmin.cn/o/1304776.html

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

相关文章

射频功率放大器的参数有哪些

射频功率放大器是射频通信系统中重要的组件&#xff0c;用于将输入的射频信号放大到需要的功率水平。在设计和选择射频功率放大器时&#xff0c;需要考虑多种参数。下面西安安泰将详细介绍射频功率放大器的常见参数。 1、P1dB功率压缩点 当放大器的输入功率比较低时&#xff0c…

如何通过控制台排查定位EasyCore?

过去当数据库出现了问题&#xff0c;我们只能通过日志去查看问题&#xff0c;可能是数据库有了重启、主节点发生了漂移或查询语句太复杂&#xff0c;这一整套逻辑走下来只能winsen, karel。 我们经常说数据库负载过高&#xff0c;请求很多&#xff0c;但我们却不能直接告诉哪个…

MyBatis-Plus - 论自定义 BaseMapper 方法『逻辑删』失效解决方案

问题描述 在上一篇我们讲过 MyBatis-Plus - 论 1 个实体类被 N 个DAO 类绑定&#xff0c;导致 MP 特性&#xff08;逻辑删&#xff09;失效的解决方案-CSDN博客 所以在这个基础上&#xff0c;我们可以很好定位到源码的分析位置。 但是今天这个问题就更奇怪了&#xff0c;已经…

springMVC 学习总结(三) 拦截器及统一异常处理

一.拦截器 1.拦截器与过滤器的区别 拦截器 Interceptor 和 过滤器 Filter类似&#xff0c;主要用于拦截用户请求并作出一定处理操作&#xff0c; 但两则也有不同之处&#xff0c;如过滤器只在Servlet前后起作用&#xff0c;是Servlet规范定义的&#xff0c;是Servlt容器才支…

基于ssm的珠宝首饰交易平台开发论文

摘 要 随着科学技术的飞速发展&#xff0c;各行各业都在努力与现代先进技术接轨&#xff0c;通过科技手段提高自身的优势&#xff1b;对于珠宝首饰交易平台当然也不能排除在外&#xff0c;随着网络技术的不断成熟&#xff0c;带动了珠宝首饰交易平台&#xff0c;它彻底改变了过…

从头到尾的数据之旅

目录 引言 链表介绍 单向链表的接口实现 结构 创建节点 头插 尾插 头删 尾删 打印 节点查找 节点前插入 节点删除 内存释放 总结 引言 在前面的学习中&#xff0c;我们深入了解了顺序表&#xff0c;通过其增删查改的操作&#xff0c;我们发现了顺序表在某些情况…

移液器吸头材质选择——PFA吸头在半导体化工行业的应用

PFA吸头是一种高性能移液器配件&#xff0c;这种材料具有优异的耐化学品、耐热和电绝缘性能&#xff0c;使得PFA吸头在应用中表现出色。那么它有哪些特点呢&#xff1f; 首先&#xff0c;PFA吸头具有卓越的耐化学腐蚀性能。无论是酸性溶液、碱性溶液还是有机溶剂&#xff0c;P…

【pytest】单元测试文件的写法

前言 可怜的宾馆&#xff0c;可怜得像被12月的冷雨淋湿的一条三只腿的黑狗。——《舞舞舞》 \;\\\;\\\; 目录 前言test_1或s_test格式非测试文件pytest.fixture()装饰器pytestselenium test_1或s_test格式 要么 test_前缀 在前&#xff0c;要么 _test后缀 在后&#xff01; …

版本控制:让你的代码有迹可循

&#x1f90d; 前端开发工程师&#xff08;主业&#xff09;、技术博主&#xff08;副业&#xff09;、已过CET6 &#x1f368; 阿珊和她的猫_CSDN个人主页 &#x1f560; 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 &#x1f35a; 蓝桥云课签约作者、已在蓝桥云…

人工智能在智慧工地中的应用场景

随着料技的不断发展&#xff0c;人工智能在各个领域都有着广泛的应用。智慧工地作为人工智能在建筑行业的应用领域之一&#xff0c;通过引入人工智能技术&#xff0c;可以提高工地的管理效率、降低事故发生概率、提升工人的工作效率&#xff0c;从而实现智能化、自动化的工地管…

IPEmotion 2023 R3 现支持新款数据记录仪IPE833

新发布的IPEmotion 2023 R3增加了多种新功能&#xff0c;其中最重要的新功能包括支持最新的数据记录仪IPE833和用于XCP测量的报文转信号功能。此外&#xff0c;它还增加了一项用于提高记录仪安全性的新功能。 — 创新一览 — ■ 支持新款数据记录仪IPE833 • 四路CAN FD接口&…

24款CBR600RR复活,CBR1000R电控下放,有望引进?

最近本田在欧洲市场亮相了停产已经6年的24款本田CBR600RR&#xff0c;传说中的F5复活了&#xff01;24款CBR采用了全新的外观设计&#xff0c;可以看到前面也加上了流行的定风翼&#xff0c;不过设计是娇小一点的&#xff0c;另外本田的CBR600RR也是唯一在售的采用尾排设计的仿…

18、Web攻防——ASP安全MDB下载植入IIS短文件名写权限解析

文章目录 一、MDB默认下载1.1 搭建IIS网站1.2 搭建网站会出现的一些问题1.2 攻击思路 二、ASP后门植入连接三、IIS短文件名探针——安全漏洞四、IIS6.0文件解析漏洞五、IIS6.0任意文件上传漏洞 一、MDB默认下载 web攻防常见开发语言&#xff1a;ASP、ASPX、PHP、JAVA、Python、…

基于ssm四六级报名与成绩查询系统论文

摘 要 互联网发展至今&#xff0c;无论是其理论还是技术都已经成熟&#xff0c;而且它广泛参与在社会中的方方面面。它让信息都可以通过网络传播&#xff0c;搭配信息管理工具可以很好地为人们提供服务。针对四六级报名信息管理混乱&#xff0c;出错率高&#xff0c;信息安全性…

一体化超声波气象站科普解说

随着科技的不断发展&#xff0c;气象监测设备也在逐步升级。一体化超声波气象站作为新型气象监测设备&#xff0c;以其优势和预报能力&#xff0c;成为了气象监测领域的新宠。 一、一体化超声波气象站的特点 WX-CSQX12 一体化超声波气象站是一种集成了多种气象监测设备的新型…

蓝桥杯--数的拆分

蓝桥杯省赛 数的拆分&#xff0c;应该是一道数论的题目 连接&#xff1a;数的拆分 对有所有的样例&#xff0c;应该用long long 来表示。 n的范围是1e18次方&#xff0c;暴力绝对是行不通的&#xff0c;不能用暴力来解决。 这是一道数学的题目&#xff0c;需要对题目进行分…

链表TOP难度——排序链表

https://leetcode.cn/problems/sort-list/?envTypestudy-plan-v2&envIdtop-interview-150 采用分治思想解决这题&#xff0c;每次合并长度为1\2\4…的链表&#xff0c;合并思想和合并有序链表一致&#xff0c;单独写成一个函数即可。 合并思路如下&#xff1a; while(分治…

赛900定妆照亮相3个色,配Stylema卡钳,风琴排气,后减震有黑科技?

钱江的赛900工信部早就给大家报道过&#xff0c;使用的是奥古斯塔的921发动机&#xff0c;之前有说叫赛1000&#xff0c;又说叫赛921&#xff0c;现在名字终于是定了&#xff0c;叫赛900RR&#xff0c;那么目前钱江赛系列的产品线就清晰了&#xff1a;赛1000、赛900、赛800、赛…

Java - JVM内存模型及GC(垃圾回收)机制

JVM内存模型 JVM堆内存划分&#xff08;JDK1.8以前&#xff09; JVM堆内存划分&#xff08;JDK1.8之后&#xff09; 主要变化在于&#xff1a; java8没有了永久代&#xff08;虚拟内存&#xff09;&#xff0c;替换为了元空间&#xff08;本地内存&#xff09;。常量池&#…

GoEasy使用手册

GoEasy官网 登录 - GoEasy 即时通讯聊天案例 GoEasy - GoEasy (gitee.com) 注意事项 接口使用人数上限为15&#xff0c;超出之后会请求超时返回408状态码&#xff0c;可以新建一个应用用来更换common Key 创建应用 ​ 添加应用名称&#xff0c;其余默认&#xff0c;点击…