前言
本文对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 4
static 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 *pcb
for (pcb = udp_pcbs; pcb != NULL; pcb = pcb->next) {
//在udp_pcbs链表里找出pcb
}
pcb->recv(pcb->recv_arg, pcb, p, ip_current_src_addr(), src);
}