LwIP代码收发报流程分析(2)数据包的接收

2024-04-21 03:44

本文主要是介绍LwIP代码收发报流程分析(2)数据包的接收,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

LwIP代码收发报流程分析(2)数据包的接收

上一篇博客,LwIP代码收发报流程分析(1)数据包的发送 我们一起分析了 LwIP 是如何将 ICMP echo 也就是 Ping 报文发送出去的过程,在这篇博客中我们将一起分析 LwIP 是如何接收网络数据包的,为后面我们将其移植到 CH32 MCU 上做做预习。

这次还是选 LwIP 官方代码中的 ping 例程作为代码分析的对象,代码位置在 LWIP\contrib-2.1.0\apps\ping\ping.c 文件中。

由于本博客主要是分析 LwIP 的接收数据包流程的,相关的代码也只选取围绕这一主旨的,其他的一些方面的细节,只要在流程清晰的情况下再找到代码自己看看一般就能明白。或者大家也可以论可以留言~ 咱们一起讨论、学习。

网络数据包的接收

上一篇博客中说到发包使用 ping_send 函数,那么与之对应的就是 ping_recv 函数了。

static void
ping_recv(int s)
{char buf[64];int len;struct sockaddr_storage from;int fromlen = sizeof(from);/* 可以看出是使用 lwip_recvfrom 来进行报文的接收 */while((len = lwip_recvfrom(s, buf, sizeof(buf), 0, (struct sockaddr*)&from, (socklen_t*)&fromlen)) > 0) {...}
...
}

在下面的 lwip_recvfrom 函数中可以看出里面实际上是数据结构的转换,数据是调用了另外一个函数来获得的。

ssize_t
lwip_recvfrom(int s, void *mem, size_t len, int flags,struct sockaddr *from, socklen_t *fromlen)
{
...sock = get_socket(s);/* 组合为 msghdr 结构,用于传递给接收函数当参数*/u16_t datagram_len = 0;struct iovec vec;struct msghdr msg;err_t err;vec.iov_base = mem;vec.iov_len = len;msg.msg_control = NULL;msg.msg_controllen = 0;msg.msg_flags = 0;msg.msg_iov = &vec;msg.msg_iovlen = 1;msg.msg_name = from;msg.msg_namelen = (fromlen ? *fromlen : 0);/* 网络数据包的来源实际上是通过这个函数 */err = lwip_recvfrom_udp_raw(sock, flags, &msg, &datagram_len, s);...}
}

可以看出实际上数据包的接收是 lwip_recvfrom_udp_raw 函数通过 msghdr 结构进行接收的。
这个函数从名字就可以看出来是用来接收像 udp 和 raw 这样的无连接协议的数据包使用的。

static err_t
lwip_recvfrom_udp_raw(struct lwip_sock *sock, int flags, struct msghdr *msg, u16_t *datagram_len, int dbg_s)
{.../*这里可以看出 flag 参数的目的就是对 socket 是否阻塞进行设置用的 */if (flags & MSG_DONTWAIT) {apiflags = NETCONN_DONTBLOCK;} else {apiflags = 0;}/* 这里看到只有把上一次的接收到的数据全部取走才会开启新的一次数据包的接收,*  这也就是为什么要在接收的时候使用循环接收的原因。*/buf = sock->lastdata.netbuf;if (buf == NULL) {err = netconn_recv_udp_raw_netbuf_flags(sock->conn, &buf, apiflags);sock->lastdata.netbuf = buf;}
.../* 将接收到的 buf 转换为 iovec 结构  */for (i = 0; (i < msg->msg_iovlen) && (copied < buflen); i++) {u16_t len_left = (u16_t)(buflen - copied);if (msg->msg_iov[i].iov_len > len_left) {copylen = len_left;} else {copylen = (u16_t)msg->msg_iov[i].iov_len;}pbuf_copy_partial(buf->p, (u8_t *)msg->msg_iov[i].iov_base, copylen, copied);copied = (u16_t)(copied + copylen);}
...
}

netconn_recv_udp_raw_netbuf_flags 这个函数可以理解为,什么都没有做,就直接调用了 netconn_recv_data 函数。

err_t
netconn_recv_udp_raw_netbuf_flags(struct netconn *conn, struct netbuf **new_buf, u8_t apiflags)
{LWIP_ERROR("netconn_recv_udp_raw_netbuf: invalid conn", (conn != NULL) &&NETCONNTYPE_GROUP(netconn_type(conn)) != NETCONN_TCP, return ERR_ARG;);return netconn_recv_data(conn, (void **)new_buf, apiflags);
}

netconn_recv_data 该函数也处理 TCP ,但是目前分析的点不在 如何实现的 TCP 连接,而是网络数据包的接收流程,所以我在这里将此部分代码进行忽略。

static err_t
netconn_recv_data(struct netconn *conn, void **new_buf, u8_t apiflags)
{
.../*这里判断 socket 是不是非阻塞且无连接*/if (netconn_is_nonblocking(conn) || (apiflags & NETCONN_DONTBLOCK) ||(conn->flags & NETCONN_FLAG_MBOXCLOSED) || (conn->pending_err != ERR_OK)) {sys_arch_mbox_tryfetch(&conn->recvmbox, &buf) == SYS_MBOX_EMPTY) }/* 这里是处理 udp 和 raw 协议的位置 */
#if (LWIP_UDP || LWIP_RAW){len = netbuf_len((struct netbuf *)buf);}/* 此处会执行回调函数用于通知接收事件以及其数据长度 */
API_EVENT(conn, NETCONN_EVT_RCVMINUS, len);*new_buf = buf;return ERR_OK;
}

接下来分析 sys_arch_mbox_tryfetch 函数,该函数用于获取到实际的网络数据包。而且该函数还是一个根据不同 OS 而使用不同设计的一个函数。 这里以 freeRTOS 举例:

u32_t
sys_arch_mbox_tryfetch(sys_mbox_t *mbox, void **msg)
{void *msg_dummy;if (!msg) {msg = &msg_dummy;}ret = xQueueReceive(mbox->mbx, &(*msg), 0);...
}

OK~ 我们看到了 xQueueReceive 函数,该函数在 FreeRTOS 中的作用是一种线程间通讯的一种方式,即 消息队列。
看到这里我们也明白了,一定是在 LwIP 的设计中有一个线程是专门负责接收网络数据,然后上层再通过不同的消息队列拿到那个专门负责接收线程的数据。

既然有消息队列的收端,就一定有对应消息队列的发端。 在与 sys_arch_mbox_tryfetch 函数同一个文件下还有一个 sys_mbox_trypost 函数。

err_t
sys_mbox_trypost(sys_mbox_t *mbox, void *msg)
{BaseType_t ret;ret = xQueueSendToBack(mbox->mbx, &msg, 0);...
}

现在就可以从 sys_mbox_trypost 函数倒推出函数调用逻辑。也就是说,往下的倒推过程就是将数据通过消息队列发送的过程。
通过搜索就很容能够找到 recv_raw 函数调用了 sys_mbox_trypost 函数。

static u8_t
recv_raw(void *arg, struct raw_pcb *pcb, struct pbuf *p,const ip_addr_t *addr)
{...conn = (struct netconn *)arg;/* 把整个包进行拷贝到新的位置 */q = pbuf_clone(PBUF_RAW, PBUF_RAM, p);u16_t len;buf = (struct netbuf *)memp_malloc(MEMP_NETBUF);buf->p = q;buf->ptr = q;ip_addr_copy(buf->addr, *ip_current_src_addr());buf->port = pcb->protocol;len = q->tot_len;/* 将数据包放入消息队列中 */sys_mbox_trypost(&conn->recvmbox, buf);...
}

到了 recv_raw 函数这里,其参数 *p 已经是接收到的数据包了,所以还要往上一路找过去。

pcb_new 函数中有这样一句话 raw_recv(msg->conn->pcb.raw, recv_raw, msg->conn);。 这里 recv_raw 就是作为回调函数传入的。

void
raw_recv(struct raw_pcb *pcb, raw_recv_fn recv, void *recv_arg)
{/* */pcb->recv = recv;pcb->recv_arg = recv_arg;
}

上面的函数为 pcb 的 recv 成员指定了 recv_raw 这个成员函数。接下来就看一下哪里调用了这个 recv 函数。

raw_input_state_t
raw_input(struct pbuf *p, struct netif *inp)
{...proto = IPH_PROTO((struct ip_hdr *)p->payload);while (pcb != NULL) {if ((pcb->protocol == proto) && raw_input_local_match(pcb, broadcast) &&(((pcb->flags & RAW_FLAGS_CONNECTED) == 0) ||ip_addr_eq(&pcb->remote_ip, ip_current_src_addr()))) {ret = RAW_INPUT_DELIVERED;/* 你看这里不就调用了吗~  */eaten = pcb->recv(pcb->recv_arg, pcb, p, ip_current_src_addr());...prev = pcb;pcb = pcb->next;}return ret;
}

后面的函数内容很多,先 focous 在调用关系上。 下面也就是 ip_input 或者 ethernet_input函数调用了 ip4_input 函数。

ip4_input(struct pbuf *p, struct netif *inp)
ip_input & ethernet_input 函数。

而这个函数的区别就是,是不是在 flags 上置位了 NETIF_FLAG_ETHARPNETIF_FLAG_ETHERNET 这两个宏,里面的逻辑可能会有所区别,但是这并不是当前我们感兴趣的内容。

err_t
tcpip_input(struct pbuf *p, struct netif *inp)
{
#if LWIP_ETHERNETif (inp->flags & (NETIF_FLAG_ETHARP | NETIF_FLAG_ETHERNET)) {return tcpip_inpkt(p, inp, ethernet_input);} else
#endif /* LWIP_ETHERNET */return tcpip_inpkt(p, inp, ip_input);
}

tcpip_inpkt 函数只是设置了 ethernet_input & ip_input 作为回调函数,等待调用。

err_t
tcpip_inpkt(struct pbuf *p, struct netif *inp, netif_input_fn input_fn)
{/*分配 msg 结构并填充其内容*/msg = (struct tcpip_msg *)memp_malloc(MEMP_TCPIP_MSG_INPKT);msg->type = TCPIP_MSG_INPKT;msg->msg.inp.p = p;msg->msg.inp.netif = inp;/* 设置接收数据包的函数 */msg->msg.inp.input_fn = input_fn;if (sys_mbox_trypost(&tcpip_mbox, msg) != ERR_OK) {memp_free(MEMP_TCPIP_MSG_INPKT, msg);return ERR_MEM;}return ERR_OK;
#endif /* LWIP_TCPIP_CORE_LOCKING_INPUT */
}

继续看下去 tcpip_input 又是在 netif_init 函数中通过下面的代码进行调用的。
netif_add(&loop_netif, LOOPIF_ADDRINIT NULL, netif_loopif_init, tcpip_input);

而在 netif_add 函数中设置了这个用于处理将数据包处理并送入消息队列中的函数。

    /*netif_add 函数很长,这里就留下这三行*/netif->state = state;netif->num = netif_num;netif->input = input;

继续看一下, netif_init 函数又是在 lwip_init 处进行调用的。

lwip_init 函数就是在最初的 LwIP 的初始化中进行的。

好的,到这里我们已经知道了在初始化的时候就会注册各个结构中用于处理接收数据的函数,并将数据送入消息队列等待处理。
tcpip_init 函数的注释中可以发现,LwIP 初始化应根据是否使用了 OS 的情况下,对应不同的初始化函数。在使用 OS 的情况下 LwIP 的初始化应该使用 tcpip_init 函数,lwip_init 函数也在这个函数中得到了调用。

/*** @ingroup lwip_os* Initialize this module:* - initialize all sub modules* - start the tcpip_thread** @param initfunc a function to call when tcpip_thread is running and finished initializing* @param arg argument to pass to initfunc*/
void
tcpip_init(tcpip_init_done_fn initfunc, void *arg)
{lwip_init();/* 这里面的这个函数以及其参数,会在 tcpip_thread 线程中执行,也就是在初始化完成之后进行。 */tcpip_init_done = initfunc;tcpip_init_done_arg = arg;/* 这里创建的就是要创建消息队列用于传输网络数据包 */sys_mbox_new(&tcpip_mbox, TCPIP_MBOX_SIZE);sys_thread_new(TCPIP_THREAD_NAME, tcpip_thread, NULL, TCPIP_THREAD_STACKSIZE, TCPIP_THREAD_PRIO);
}

tcpip_thread 函数

static void
tcpip_thread(void *arg)
{/* 这里就能出来,将 tcpip_init 函数传递进来的函数以及参数进行执行了。 */if (tcpip_init_done != NULL) {tcpip_init_done(tcpip_init_done_arg);}while (1) {                      LWIP_TCPIP_THREAD_ALIVE();/* 这里面适用于接收消息队列中的数据包并且可以判断超时,后面再分析。*/TCPIP_MBOX_FETCH(&tcpip_mbox, (void **)&msg);tcpip_thread_handle_msg(msg);}
}

tcpip_thread_handle_msg 函数会根据入 tcpip_msg 数据格式的类型判断如何进行处理,其中 TCPIP_MSG_INPKT 类型 会调用 msg->msg.inp.input_fn 函数进行处理。这个函数就是在前面介绍过的 tcpip_inpkt 函数中指定的接收函数。


static void
tcpip_thread_handle_msg(struct tcpip_msg *msg)
{switch (msg->type) {
...case TCPIP_MSG_INPKT:LWIP_DEBUGF(TCPIP_DEBUG, ("tcpip_thread: PACKET %p\n", (void *)msg));if (msg->msg.inp.input_fn(msg->msg.inp.p, msg->msg.inp.netif) != ERR_OK) {pbuf_free(msg->msg.inp.p);}memp_free(MEMP_TCPIP_MSG_INPKT, msg);break;case TCPIP_MSG_CALLBACK:LWIP_DEBUGF(TCPIP_DEBUG, ("tcpip_thread: CALLBACK %p\n", (void *)msg));msg->msg.cb.function(msg->msg.cb.ctx);memp_free(MEMP_TCPIP_MSG_API, msg);break;case TCPIP_MSG_CALLBACK_STATIC:LWIP_DEBUGF(TCPIP_DEBUG, ("tcpip_thread: CALLBACK_STATIC %p\n", (void *)msg));msg->msg.cb.function(msg->msg.cb.ctx);break;default:LWIP_DEBUGF(TCPIP_DEBUG, ("tcpip_thread: invalid message: %d\n", msg->type));LWIP_ASSERT("tcpip_thread: invalid message", 0);break;}...
}

OK~ 目前调用的流程是没问题了,但是大家是否注意到在 tcpip_thread_handle_msg 函数调用的时候,实际上 msg 结构中已经存在了数据包。
这个数据包正是上面正确调用了接收相关的函数才正确放入消息队列中的。

那么下一步要解决的就是网卡如何将数据包传递到 msg 结构中的。

上一篇博客也介绍了,在 LwIP 例子中包含了一个网卡接口的程序 contrib\examples\ethernetif\ethernetif.c

里面的 ethernetif_init 函数用于完成网卡硬件和其他一些的初始化工作,而且上一篇博客也介绍了ethernetif.c 文件中的 low_level_output 函数用于实际网卡发送网络数据包,而 low_level_input 函数则就是用来接收网络数据包的网卡驱动部分。 该函数在 ethernetif_input 函数中被调用,并且注释中也对该函数进行了比较详细的说明。
下面的代码中我保留了LwIP代码中原始的注释。

/*** This function should be called when a packet is ready to be read* from the interface. It uses the function low_level_input() that* should handle the actual reception of bytes from the network* interface. Then the type of the received packet is determined and* the appropriate input function is called.** @param netif the lwip network interface structure for this ethernetif*/
static void
ethernetif_input(struct netif *netif)
{struct ethernetif *ethernetif;struct eth_hdr *ethhdr;struct pbuf *p;ethernetif = netif->state;/* move received packet into a new pbuf */p = low_level_input(netif);/* if no packet could be read, silently ignore this */if (p != NULL) {/* pass all packets to ethernet_input, which decides what packets it supports */if (netif->input(p, netif) != ERR_OK) {LWIP_DEBUGF(NETIF_DEBUG, ("ethernetif_input: IP input error\n"));pbuf_free(p);p = NULL;}}
}

上面的代码实际是调用内部的 low_level_input 函数完成了实际的网络数据包的接收工作,然后调用 netif->input 函数对网络数据包进行接收并处理。

这里的 netif->input 函数就是在前面介绍过的 netif_add 函数中进行指定的。下面再把设置 netif->input 函数这段代码拿过来看看。

#if NO_SYSnetif_add(&loop_netif, LOOPIF_ADDRINIT NULL, netif_loopif_init, ip_input);
#else  /* NO_SYS */netif_add(&loop_netif, LOOPIF_ADDRINIT NULL, netif_loopif_init, tcpip_input);

到这里对 LwIP 的接收流程是不是已经有了一个整体的概念呢? 我再用图更清晰的表示一下。

在这里插入图片描述


请添加图片描述

这篇关于LwIP代码收发报流程分析(2)数据包的接收的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/922082

相关文章

Spring Boot 3.4.3 基于 Spring WebFlux 实现 SSE 功能(代码示例)

《SpringBoot3.4.3基于SpringWebFlux实现SSE功能(代码示例)》SpringBoot3.4.3结合SpringWebFlux实现SSE功能,为实时数据推送提供... 目录1. SSE 简介1.1 什么是 SSE?1.2 SSE 的优点1.3 适用场景2. Spring WebFlu

java之Objects.nonNull用法代码解读

《java之Objects.nonNull用法代码解读》:本文主要介绍java之Objects.nonNull用法代码,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐... 目录Java之Objects.nonwww.chinasem.cnNull用法代码Objects.nonN

Spring事务中@Transactional注解不生效的原因分析与解决

《Spring事务中@Transactional注解不生效的原因分析与解决》在Spring框架中,@Transactional注解是管理数据库事务的核心方式,本文将深入分析事务自调用的底层原理,解释为... 目录1. 引言2. 事务自调用问题重现2.1 示例代码2.2 问题现象3. 为什么事务自调用会失效3

SpringBoot实现MD5加盐算法的示例代码

《SpringBoot实现MD5加盐算法的示例代码》加盐算法是一种用于增强密码安全性的技术,本文主要介绍了SpringBoot实现MD5加盐算法的示例代码,文中通过示例代码介绍的非常详细,对大家的学习... 目录一、什么是加盐算法二、如何实现加盐算法2.1 加盐算法代码实现2.2 注册页面中进行密码加盐2.

python+opencv处理颜色之将目标颜色转换实例代码

《python+opencv处理颜色之将目标颜色转换实例代码》OpenCV是一个的跨平台计算机视觉库,可以运行在Linux、Windows和MacOS操作系统上,:本文主要介绍python+ope... 目录下面是代码+ 效果 + 解释转HSV: 关于颜色总是要转HSV的掩膜再标注总结 目标:将红色的部分滤

找不到Anaconda prompt终端的原因分析及解决方案

《找不到Anacondaprompt终端的原因分析及解决方案》因为anaconda还没有初始化,在安装anaconda的过程中,有一行是否要添加anaconda到菜单目录中,由于没有勾选,导致没有菜... 目录问题原因问http://www.chinasem.cn题解决安装了 Anaconda 却找不到 An

Spring定时任务只执行一次的原因分析与解决方案

《Spring定时任务只执行一次的原因分析与解决方案》在使用Spring的@Scheduled定时任务时,你是否遇到过任务只执行一次,后续不再触发的情况?这种情况可能由多种原因导致,如未启用调度、线程... 目录1. 问题背景2. Spring定时任务的基本用法3. 为什么定时任务只执行一次?3.1 未启用

在C#中调用Python代码的两种实现方式

《在C#中调用Python代码的两种实现方式》:本文主要介绍在C#中调用Python代码的两种实现方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录C#调用python代码的方式1. 使用 Python.NET2. 使用外部进程调用 Python 脚本总结C#调

Python实现自动化接收与处理手机验证码

《Python实现自动化接收与处理手机验证码》在移动互联网时代,短信验证码已成为身份验证、账号注册等环节的重要安全手段,本文将介绍如何利用Python实现验证码的自动接收,识别与转发,需要的可以参考下... 目录引言一、准备工作1.1 硬件与软件需求1.2 环境配置二、核心功能实现2.1 短信监听与获取2.

Java时间轮调度算法的代码实现

《Java时间轮调度算法的代码实现》时间轮是一种高效的定时调度算法,主要用于管理延时任务或周期性任务,它通过一个环形数组(时间轮)和指针来实现,将大量定时任务分摊到固定的时间槽中,极大地降低了时间复杂... 目录1、简述2、时间轮的原理3. 时间轮的实现步骤3.1 定义时间槽3.2 定义时间轮3.3 使用时