当前位置: 首页 > news >正文

【LwIP源码学习6】UDP部分源码分析

前言

本文对lwip协议栈里UDP部分的源码进行分析。将源码中最关键部分提取出来,梳理UDP部分的设计框架。

应用层例子

首先举一个应用层使用UDP的简单例子,如下:

//第一步:申请套接字
sock = socket(AF_INET, SOCK_DGRAM, 0);
//第二步:绑定套接字与网卡信息
bind(sock, (struct sockaddr *)&udp_addr, sizeof(struct sockaddr)
while(1)
{
/*第三步:接收数据*/recv_data_len=recvfrom(sock,recv_data,RECV_DATA,0,(struct sockaddr*)&seraddr,&addrlen);/*第四步:发送数据*/sendto(sock,recv_data,recv_data_len,0,(struct sockaddr*)&seraddr,addrlen);
}

而这些函数的原型如下:
sockets.h文件

#define socket(domain,type,protocol)              lwip_socket(domain,type,protocol)
#define bind(s,name,namelen)                      lwip_bind(s,name,namelen)
#define recvfrom(s,mem,len,flags,from,fromlen)    lwip_recvfrom(s,mem,len,flags,from,fromlen)
#define sendto(s,dataptr,size,flags,to,tolen)     lwip_sendto(s,dataptr,size,flags,to,tolen)

申请套接字

申请套接字使用lwip_socket函数,代码如下,首先调用netconn_new_with_proto_and_callback函数申请一个netconn结构体,然后调用alloc_socket函数在全局变量sockets[NUM_SOCKETS]中找出一个合适的socket分配给用户,可以看到最多可以申请4个套接字。
socket.c文件

#define NUM_SOCKETS 4static struct lwip_sock sockets[NUM_SOCKETS];unsigned int lwip_socket(int domain, int type, int protocol)
{int i;struct netconn *conn;conn = netconn_new_with_proto_and_callback(1, 0, NULL);i = alloc_socket(conn, 0);conn->socket = i;return conn->socket
}static int
alloc_socket(struct netconn *newconn, int accepted)
{int i;for (i = 0; i < NUM_SOCKETS; i++){if (!sockets[i].conn){sockets[i].conn = newconn;return i;}}return -1;
}

以下是netconn_new_with_proto_and_callback函数,首先调用netconn_alloc创建一个netconn,然后调用pcb_new做进一步处理,这里的api_msg用于协调用户线程与内核线程的通信。
api_lib.c文件

struct netconn * 
netconn_new_with_proto_and_callback(enum netconn_type t, char proto, netconn_callback callback)
{struct netconn *conn;struct api_msg msg;conn = netconn_alloc(t, callback);msg.conn = conn;pcb_new(&msg);return conn;
}

上面调用的两个函数都在api_msg.c文件里,如下,
netconn_alloc函数用于申请内存,然后创建一个消息队列赋值给conn->recvmbox,然后赋值一下回调函数。
pcb_new函数首先调用udp_new函数创建一个udp_pcb,然后调用udp_recv函数将recv_udp函数与新创建的udp_pcb进行绑定,让recv_udp函数作为这个udp_pcb接收到网口数据时的回调函数。
recv_udp函数里将接收到的数据放入conn->recvmbox指向的消息队列中。所以lwip里UDP协议接收数据的过程是:内核接收到数据后放入消息队列里,应用层从消息队列里取出数据做进一步处理

api_msg.c文件

struct netconn *
netconn_alloc(enum netconn_type t, netconn_callback callback)
{conn = (struct netconn *)memp_malloc(MEMP_NETCONN);sys_mbox_new(&conn->recvmbox, size);conn->callback     = callback;return conn;
}static void
pcb_new(struct api_msg *msg)
{msg->conn->pcb.udp = udp_new();udp_recv(msg->conn->pcb.udp, recv_udp, msg->conn);}static void
recv_udp(void *arg, struct udp_pcb *pcb, struct pbuf *p,const int *addr, char port)
{sys_mbox_trypost(&conn->recvmbox, buf);
}

上面使用到的两个函数在udp.c文件里,如下。
udp_new函数主要就是为新的udp_pcb申请内存。
udp_recv主要设置接收数据的回调函数,也就是api_msg.c文件里的recv_udp函数

udp.c文件

struct udp_pcb *
udp_new(void)
{struct udp_pcb *pcb;pcb = (struct udp_pcb *)malloc(sizeof(udp_pcb));return pcb;
}void
udp_recv(struct udp_pcb *pcb, udp_recv_fn recv, void *recv_arg)
{pcb->recv = recv;pcb->recv_arg = recv_arg;  
}

绑定套接字与网卡信息

绑定套接字用到lwip_bind函数,如下,主要调用netconn_bind函数来绑定本地IP地址和端口号。

socket.c文件

int
lwip_bind(int s, const struct sockaddr *name, socklen_t namelen)
{struct lwip_sock *sock;sock = get_socket(s);err = netconn_bind(sock->conn, &local_addr, local_port);return 0;
}

lwip_bind函数调用的netconn_bind函数如下,
api_lib.c文件

err_t
netconn_bind(struct netconn *conn, const ip_addr_t *addr, u16_t port)
{err = netconn_apimsg(lwip_netconn_do_bind, &API_MSG_VAR_REF(msg));return err;
}void
lwip_netconn_do_bind(void *m)
{struct api_msg *msg = (struct api_msg *)m;switch (msg->conn->type)) {case NETCONN_UDP:err = udp_bind(msg->conn->pcb.udp, API_EXPR_REF(msg->msg.bc.ipaddr), msg->msg.bc.port);break;default:err = ERR_VAL;break;}
}

其中的

netconn_apimsg(lwip_netconn_do_bind, &API_MSG_VAR_REF(msg));

就相当于

lwip_netconn_do_bind(msg);

只不过添加了调用前申请锁,调用后释放锁,这么做是为了防止应用层线程调用这个函数修改协议栈信息时,跟协议栈线程访问冲突。
lwip_netconn_do_bind函数调用的udp_bind函数如下,主要就是绑定IP地址、端口号,将新的udp_pcb放到udp_pcbs链表中。
udp.c文件

err_t
udp_bind(struct udp_pcb *pcb, const ip_addr_t *ipaddr, u16_t port)
{ip_addr_set_ipaddr(&pcb->local_ip, ipaddr);pcb->local_port = port;//将新的pcb添加到udp_pcbs链表中pcb->next = udp_pcbs;udp_pcbs = pcb;return 0;
}

接收数据

接收数据使用lwip_recvfrom函数,其中判断到套接字协议类型为UDP时,会调用lwip_recvfrom_udp_raw做进一步处理。
lwip_recvfrom_udp_raw函数首先调用netconn_recv_data函数将接收到的UDP数据取出,然后调用pbuf_copy_partial函数将取出的数据放在msg->msg_iov[0].iov_base指向的内存,这块内存是应用层传入的。
socket.c文件

size_t lwip_recvfrom(int s, void *mem, size_t len, int flags,struct sockaddr *from, int *fromlen)
{struct lwip_sock *sock;sock = get_socket(s);if (sock->conn->type == NETCONN_TCP){}else{err = lwip_recvfrom_udp_raw(sock, flags, &msg, &datagram_len, s);}
}static err_t
lwip_recvfrom_udp_raw(struct lwip_sock *sock, int flags, struct msghdr *msg, u16_t *datagram_len, int dbg_s)
{struct netbuf *buf;netconn_recv_data(conn, &buf, apiflags);//将buf里的数据拷贝到msg->msg_iov[0].iov_base,这里iov_base就是应用层传入的存放数据的位置pbuf_copy_partial(buf->p, (u8_t *)msg->msg_iov[0].iov_base, copylen, copied);}

lwip_recvfrom_udp_raw函数里取数据用到的netconn_recv_data函数如下,其中主要是从conn->recvmbox指向的消息队列中取出UDP数据。
api_lib.c文件

static err_t
netconn_recv_data(struct netconn *conn, void **new_buf, u8_t apiflags)
{sys_arch_mbox_fetch(&conn->recvmbox, &buf, 0);API_EVENT(conn, NETCONN_EVT_RCVMINUS, len); //这里再调用conn里的回调函数return ERR_OK;
}

发送数据

发送数据主要用到lwip_sendto函数,如下。
socket.c文件

ssize_t
lwip_sendto(int s, const void *data, size_t size, int flags,const struct sockaddr *to, socklen_t tolen)
{err = netconn_send(sock->conn, &buf);return (err == ERR_OK ? short_size : -1);
}

lwip_sendto函数里调用的netconn_send函数如下,与之前相同,其中的netconn_apimsg(lwip_netconn_do_send, &API_MSG_VAR_REF(msg))相当于
lwip_netconn_do_send(msg),这么封装是为了避免应用层线程与lwip内核线程访问冲突。lwip_netconn_do_send函数主要调用了udp_sendto函数。
api_lib.c文件

err_t
netconn_send(struct netconn *conn, struct netbuf *buf)
{err = netconn_apimsg(lwip_netconn_do_send, &API_MSG_VAR_REF(msg));return err;
}void
lwip_netconn_do_send(void *m)
{err = udp_sendto(msg->conn->pcb.udp, msg->msg.b->p, &msg->msg.b->addr, msg->msg.b->port);
}

lwip_netconn_do_send函数调用的udp_sendto函数如下,其中一层一层的调用,最后调用的是ip4_output_if_src函数,将数据从UDP层传给IP层。
udp.c文件

err_t
udp_sendto(struct udp_pcb *pcb, struct pbuf *p,const ip_addr_t *dst_ip, u16_t dst_port)
{return udp_sendto_if(pcb, p, dst_ip, dst_port, netif); 
)err_t
udp_sendto_if(struct udp_pcb *pcb, struct pbuf *p,const ip_addr_t *dst_ip, u16_t dst_port, struct netif *netif)
{return udp_sendto_if_src(pcb, p, dst_ip, dst_port, netif, src_ip);
}err_t
udp_sendto_if_src(struct udp_pcb *pcb, struct pbuf *p,const ip_addr_t *dst_ip, u16_t dst_port, struct netif *netif, const ip_addr_t *src_ip)
{err = ip4_output_if_src(q, src_ip, dst_ip, ttl, pcb->tos, ip_proto, netif);return err;
}

lwip内核接收UDP数据处理过程

lwip里有一个tcpip_thread线程,如下,他会一直检测tcpip_mbox消息队列中是否有消息,如果有,就说明网卡接收到了数据,则调用tcpip_thread_handle_msg函数做进一步处理,tcpip_thread_handle_msg函数会对消息做一个分类,UDP消息类型是TCPIP_MSG_INPKT,所以会执行msg->msg.inp.input_fn(msg->msg.inp.p, msg->msg.inp.netif),而msg->msg.inp.input_fn函数在创建TCPIP_MSG_INPKT类型消息时被赋值为ethernet_input函数。
tcpip.c文件

static void
tcpip_thread(void *arg)
{while (1){TCPIP_MBOX_FETCH(&tcpip_mbox, (void **)&msg);if (msg == NULL) {continue;}tcpip_thread_handle_msg(msg);}
}static void
tcpip_thread_handle_msg(struct tcpip_msg *msg)
{switch (msg->type) {case TCPIP_MSG_INPKT:msg->msg.inp.input_fn(msg->msg.inp.p, msg->msg.inp.netif);break;default:break;}
}

以下就是msg->msg.inp.input_fn函数的原型ethernet_input函数,这里会判断接收到数据的类型,UDP协议数据就会调用ip4_input函数做进一步处理。
ethernet.c文件

err_t
ethernet_input(struct pbuf *p, struct netif *netif)
{struct eth_hdr *ethhdr;u16_t type;
......//获取网卡接受数据的以太网首部ethhdr = (struct eth_hdr *)p->payload;
......//在以太网首部中获取本包数据的类型type = ethhdr->type;
......switch (type) {......case PP_HTONS(ETHTYPE_IP):  //如果本包数据是IP数据报......    ip4_input(p, netif);...break;case PP_HTONS(ETHTYPE_ARP): //如果本包数据是ARP数据报......etharp_input(p, netif);...break;......default:...}return ERR_OK;
}

以下是ethernet_input函数处理UDP数据时调用的ip4_input函数,其中会调用udp_input函数做进一步处理。
ip4.c文件

err_t
ip4_input(struct pbuf *p, struct netif *inp)
{switch (IPH_PROTO(iphdr)) {case IP_PROTO_UDP:udp_input(p, inp);break;default:break;}
}

以下是ip4_input函数里调用的udp_input函数,其中调用了pcb->recv函数,从之前的分析可知,pcb->recv函数就是api_msg.c文件里的recv_udp函数,也就是将lwip内核接收到的UDP消息放到conn->recvmbox指向的消息队列中。
udp.c文件

void
udp_input(struct pbuf *p, struct netif *inp)
{struct udp_pcb *pcbfor (pcb = udp_pcbs; pcb != NULL; pcb = pcb->next) {//在udp_pcbs链表里找出pcb}pcb->recv(pcb->recv_arg, pcb, p, ip_current_src_addr(), src);
}

相关文章:

  • 研修室智慧化升级实践:线上预约+智能门锁的融合方案
  • 高级数据结构:线段树
  • Problem B: 面向对象综合题2
  • LLM框架
  • 【Diffusion】在华为云ModelArts上运行MindSpore扩散模型教程
  • 基于SpringBoot的抽奖系统测试报告
  • 11、参数化三维产品设计组件 - /设计与仿真组件/parametric-3d-product-design
  • Linux-TCP套接字编程简易实践:实现EchoServer与远程命令执行及自定义协议(反)序列化
  • 基于物联网的智能家居监控系统设计和实现(源码+论文+部署讲解等)
  • OpenWrt开发第8篇:树莓派开发板做无线接入点
  • 计算机网络笔记(二十一)——4.3IP层转发分组的过程
  • 小土堆pytorch--torchvision中的数据集的使用dataloader的使用
  • 在python中,为什么要引入事件循环这个概念?
  • 第二十三节:图像金字塔- 图像金字塔应用 (图像融合)
  • 封装和分用(网络原理)
  • 【常用算法:排序篇】4.高效堆排序:线性建堆法与蚂蚁问题的降维打击
  • Kafka的基本概念和Dokcer中部署Kafka
  • B 端电商数据接口开发:1688 商品详情页实时数据抓取技术解析
  • 组合模式(Composite Pattern)详解
  • Docker拉取ubuntu22.04镜像使用ROS2 humble及仿真工具可视化进行导航
  • 深圳中院回应“退休夫妻月入1.2万负债1.2亿”:其自述因经营不善负债
  • 马上评丨岂能为流量拿自己的生命开玩笑
  • 警方通报“网约车司机偷拍女乘客”:已被行政拘留
  • 成都锦江区一在建工地起火,致2人遇难1人受伤
  • 佩斯科夫:俄会考虑30天停火提议,但试图对俄施压无用
  • 梅花奖在上海|朱洁静:穿越了人生暴风雨,舞台是最好良药