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

相关文章

SpringCloud集成AlloyDB的示例代码

《SpringCloud集成AlloyDB的示例代码》AlloyDB是GoogleCloud提供的一种高度可扩展、强性能的关系型数据库服务,它兼容PostgreSQL,并提供了更快的查询性能... 目录1.AlloyDBjavascript是什么?AlloyDB 的工作原理2.搭建测试环境3.代码工程1.

Java调用Python代码的几种方法小结

《Java调用Python代码的几种方法小结》Python语言有丰富的系统管理、数据处理、统计类软件包,因此从java应用中调用Python代码的需求很常见、实用,本文介绍几种方法从java调用Pyt... 目录引言Java core使用ProcessBuilder使用Java脚本引擎总结引言python

Java中ArrayList的8种浅拷贝方式示例代码

《Java中ArrayList的8种浅拷贝方式示例代码》:本文主要介绍Java中ArrayList的8种浅拷贝方式的相关资料,讲解了Java中ArrayList的浅拷贝概念,并详细分享了八种实现浅... 目录引言什么是浅拷贝?ArrayList 浅拷贝的重要性方法一:使用构造函数方法二:使用 addAll(

Redis主从复制实现原理分析

《Redis主从复制实现原理分析》Redis主从复制通过Sync和CommandPropagate阶段实现数据同步,2.8版本后引入Psync指令,根据复制偏移量进行全量或部分同步,优化了数据传输效率... 目录Redis主DodMIK从复制实现原理实现原理Psync: 2.8版本后总结Redis主从复制实

JAVA利用顺序表实现“杨辉三角”的思路及代码示例

《JAVA利用顺序表实现“杨辉三角”的思路及代码示例》杨辉三角形是中国古代数学的杰出研究成果之一,是我国北宋数学家贾宪于1050年首先发现并使用的,:本文主要介绍JAVA利用顺序表实现杨辉三角的思... 目录一:“杨辉三角”题目链接二:题解代码:三:题解思路:总结一:“杨辉三角”题目链接题目链接:点击这里

SpringBoot使用注解集成Redis缓存的示例代码

《SpringBoot使用注解集成Redis缓存的示例代码》:本文主要介绍在SpringBoot中使用注解集成Redis缓存的步骤,包括添加依赖、创建相关配置类、需要缓存数据的类(Tes... 目录一、创建 Caching 配置类二、创建需要缓存数据的类三、测试方法Spring Boot 熟悉后,集成一个外

锐捷和腾达哪个好? 两个品牌路由器对比分析

《锐捷和腾达哪个好?两个品牌路由器对比分析》在选择路由器时,Tenda和锐捷都是备受关注的品牌,各自有独特的产品特点和市场定位,选择哪个品牌的路由器更合适,实际上取决于你的具体需求和使用场景,我们从... 在选购路由器时,锐捷和腾达都是市场上备受关注的品牌,但它们的定位和特点却有所不同。锐捷更偏向企业级和专

SpringBoot使用minio进行文件管理的流程步骤

《SpringBoot使用minio进行文件管理的流程步骤》MinIO是一个高性能的对象存储系统,兼容AmazonS3API,该软件设计用于处理非结构化数据,如图片、视频、日志文件以及备份数据等,本文... 目录一、拉取minio镜像二、创建配置文件和上传文件的目录三、启动容器四、浏览器登录 minio五、

轻松掌握python的dataclass让你的代码更简洁优雅

《轻松掌握python的dataclass让你的代码更简洁优雅》本文总结了几个我在使用Python的dataclass时常用的技巧,dataclass装饰器可以帮助我们简化数据类的定义过程,包括设置默... 目录1. 传统的类定义方式2. dataclass装饰器定义类2.1. 默认值2.2. 隐藏敏感信息

opencv实现像素统计的示例代码

《opencv实现像素统计的示例代码》本文介绍了OpenCV中统计图像像素信息的常用方法和函数,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一... 目录1. 统计像素值的基本信息2. 统计像素值的直方图3. 统计像素值的总和4. 统计非零像素的数量