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

相关文章

使用Dify访问mysql数据库详细代码示例

《使用Dify访问mysql数据库详细代码示例》:本文主要介绍使用Dify访问mysql数据库的相关资料,并详细讲解了如何在本地搭建数据库访问服务,使用ngrok暴露到公网,并创建知识库、数据库访... 1、在本地搭建数据库访问的服务,并使用ngrok暴露到公网。#sql_tools.pyfrom

Java springBoot初步使用websocket的代码示例

《JavaspringBoot初步使用websocket的代码示例》:本文主要介绍JavaspringBoot初步使用websocket的相关资料,WebSocket是一种实现实时双向通信的协... 目录一、什么是websocket二、依赖坐标地址1.springBoot父级依赖2.springBoot依赖

讯飞webapi语音识别接口调用示例代码(python)

《讯飞webapi语音识别接口调用示例代码(python)》:本文主要介绍如何使用Python3调用讯飞WebAPI语音识别接口,重点解决了在处理语音识别结果时判断是否为最后一帧的问题,通过运行代... 目录前言一、环境二、引入库三、代码实例四、运行结果五、总结前言基于python3 讯飞webAPI语音

MyBatis-Plus中Service接口的lambdaUpdate用法及实例分析

《MyBatis-Plus中Service接口的lambdaUpdate用法及实例分析》本文将详细讲解MyBatis-Plus中的lambdaUpdate用法,并提供丰富的案例来帮助读者更好地理解和应... 目录深入探索MyBATis-Plus中Service接口的lambdaUpdate用法及示例案例背景

MyBatis-Plus中静态工具Db的多种用法及实例分析

《MyBatis-Plus中静态工具Db的多种用法及实例分析》本文将详细讲解MyBatis-Plus中静态工具Db的各种用法,并结合具体案例进行演示和说明,具有很好的参考价值,希望对大家有所帮助,如有... 目录MyBATis-Plus中静态工具Db的多种用法及实例案例背景使用静态工具Db进行数据库操作插入

什么是 Java 的 CyclicBarrier(代码示例)

《什么是Java的CyclicBarrier(代码示例)》CyclicBarrier是多线程协同的利器,适合需要多次同步的场景,本文通过代码示例讲解什么是Java的CyclicBarrier,感... 你的回答(口语化,面试场景)面试官:什么是 Java 的 CyclicBarrier?你:好的,我来举个例

SpringBoot接收JSON类型的参数方式

《SpringBoot接收JSON类型的参数方式》:本文主要介绍SpringBoot接收JSON类型的参数方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、jsON二、代码准备三、Apifox操作总结一、JSON在学习前端技术时,我们有讲到过JSON,而在

在VSCode中本地运行DeepSeek的流程步骤

《在VSCode中本地运行DeepSeek的流程步骤》本文详细介绍了如何在本地VSCode中安装和配置Ollama和CodeGPT,以使用DeepSeek进行AI编码辅助,无需依赖云服务,需要的朋友可... 目录步骤 1:在 VSCode 中安装 Ollama 和 CodeGPT安装Ollama下载Olla

基于Canvas的Html5多时区动态时钟实战代码

《基于Canvas的Html5多时区动态时钟实战代码》:本文主要介绍了如何使用Canvas在HTML5上实现一个多时区动态时钟的web展示,通过Canvas的API,可以绘制出6个不同城市的时钟,并且这些时钟可以动态转动,每个时钟上都会标注出对应的24小时制时间,详细内容请阅读本文,希望能对你有所帮助...

HTML5 data-*自定义数据属性的示例代码

《HTML5data-*自定义数据属性的示例代码》HTML5的自定义数据属性(data-*)提供了一种标准化的方法在HTML元素上存储额外信息,可以通过JavaScript访问、修改和在CSS中使用... 目录引言基本概念使用自定义数据属性1. 在 html 中定义2. 通过 JavaScript 访问3.