通过源码理解IGMP v1的实现(基于linux1.2.13)

2024-03-27 21:18

本文主要是介绍通过源码理解IGMP v1的实现(基于linux1.2.13),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

IGMP是组成员管理协议,我们知道一般的通信是单播的,虽然主机发出的单播报文,局域网中的每个主机都会收到,但是默认情况下,主机只会处理目的ip是自己的报文。如果我想让多个主机都可以处理我发出的报文怎么办呢?这就是IGMP做的事情。他定义了组的概念,我们可以使用多播的方式,给一个组发送报文,属于这个组的主机都可以处理这个报文。下面我们看看多播是怎么实现的。首先我们看一下网络架构。

ip地址中给多播预留了一段范围的ip。IGMP的一个多播组其实就是一个多播ip。主机记录了本主机加入的多播组信息。组播路由记录了局域网中所有多播组的信息和转发信息。IGMP的实现主要分为下面几个方面。

1 加入、离开多播组

多播是和进程(或者说socket)相关的。我们可以通过以下代码加入一个多播组。

setsockopt(fd,IPPROTO_IP,IP_ADD_MEMBERSHIP,&mreq, // device对应的ip和加入多播组的ipsizeof(mreq));

mreq的结构体定义如下

struct ip_mreq 
{struct in_addr imr_multiaddr;	/* IP multicast address of group */struct in_addr imr_interface;	/* local IP address of interface */
};

我们看一下setsockopt的实现(只列出相关部分代码)

	case IP_ADD_MEMBERSHIP: {struct ip_mreq mreq;static struct options optmem;unsigned long route_src;struct rtable *rt;struct device *dev=NULL;err=verify_area(VERIFY_READ, optval, sizeof(mreq));memcpy_fromfs(&mreq,optval,sizeof(mreq));// 没有设置device则根据多播组ip选择一个deviceif(mreq.imr_interface.s_addr==INADDR_ANY) {if((rt=ip_rt_route(mreq.imr_multiaddr.s_addr,&optmem, &route_src))!=NULL){dev=rt->rt_dev;rt->rt_use--;}}else{// 根据device ip找到,找到对应的devicefor(dev = dev_base; dev; dev = dev->next){// 在工作状态、支持多播,ip一样if((dev->flags&IFF_UP)&&(dev->flags&IFF_MULTICAST)&&(dev->pa_addr==mreq.imr_interface.s_addr))break;}}// 加入多播组return ip_mc_join_group(sk,dev,mreq.imr_multiaddr.s_addr);}

拿到加入的多播组ip和device后,调用ip_mc_join_group,在socket结构体中,有一个字段维护了该socket加入的多播组信息。

int ip_mc_join_group(struct sock *sk , struct device *dev, unsigned long addr)
{int unused= -1;int i;// 还没有加入过多播组if(sk->ip_mc_list==NULL){if((sk->ip_mc_list=(struct ip_mc_socklist *)kmalloc(sizeof(*sk->ip_mc_list), GFP_KERNEL))==NULL)return -ENOMEM;memset(sk->ip_mc_list,'\0',sizeof(*sk->ip_mc_list));}// 遍历加入的多播组队列,判断是否已经加入过for(i=0;i<IP_MAX_MEMBERSHIPS;i++){if(sk->ip_mc_list->multiaddr[i]==addr && sk->ip_mc_list->multidev[i]==dev)return -EADDRINUSE;if(sk->ip_mc_list->multidev[i]==NULL)unused=i;}// 到这说明没有加入过当前设置的多播组,则记录并且加入if(unused==-1)return -ENOBUFS;sk->ip_mc_list->multiaddr[unused]=addr;sk->ip_mc_list->multidev[unused]=dev;// addr为多播组ipip_mc_inc_group(dev,addr);return 0;
}

ip_mc_join_group函数的主要逻辑是把socket想加入的多播组信息记录到socket的ip_mc_list字段中(如果还没有加入过该多播组的话)。接着调ip_mc_inc_group往下走。device层维护了主机中使用了该device的多播组信息。

static void ip_mc_inc_group(struct device *dev, unsigned long addr)
{struct ip_mc_list *i;// 遍历该设置维护的多播组队列,判断是否已经有socket加入过该多播组,是则引用数加一for(i=dev->ip_mc_list;i!=NULL;i=i->next){if(i->multiaddr==addr){i->users++;return;}}// 到这说明,还没有socket加入过当前多播组,则记录并加入i=(struct ip_mc_list *)kmalloc(sizeof(*i), GFP_KERNEL);if(!i)return;i->users=1;i->interface=dev;i->multiaddr=addr;i->next=dev->ip_mc_list;// 通过igmp通知其他方igmp_group_added(i);dev->ip_mc_list=i;
}

ip_mc_inc_group函数的主要逻辑是判断socket想要加入的多播组是不是已经存在于当前device中,如果不是则新增一个节点。继续调用igmp_group_added

static void igmp_group_added(struct ip_mc_list *im)
{// 初始化定时器igmp_init_timer(im);// 发送一个igmp数据包,同步多播组信息(socket加入了一个新的多播组)igmp_send_report(im->interface, im->multiaddr, IGMP_HOST_MEMBERSHIP_REPORT);// 转换多播组ip到多播mac地址,并记录到device中ip_mc_filter_add(im->interface, im->multiaddr);
}

我们看看igmp_send_report和ip_mc_filter_add的具体逻辑。

static void igmp_send_report(struct device *dev, unsigned long address, int type)
{// 申请一个skb表示一个数据包struct sk_buff *skb=alloc_skb(MAX_IGMP_SIZE, GFP_ATOMIC);int tmp;struct igmphdr *igh;// 构建ip头,ip协议头的源ip是INADDR_ANY,即随机选择一个本机的,目的ip为多播组ip(address)tmp=ip_build_header(skb, INADDR_ANY, address, &dev, IPPROTO_IGMP, NULL,skb->mem_len, 0, 1);// data表示所有的数据部分,tmp表示ip头大小,所以igh就是ip协议的数据部分,即igmp报文的内容igh=(struct igmphdr *)(skb->data+tmp);skb->len=tmp+sizeof(*igh);igh->csum=0;igh->unused=0;igh->type=type;igh->group=address;igh->csum=ip_compute_csum((void *)igh,sizeof(*igh));// 调用ip层发送出去ip_queue_xmit(NULL,dev,skb,1);
}

igmp_send_report其实就是构造一个igmp协议数据包,然后发送出去,igmp的协议格式如下

struct igmphdr
{// 类型unsigned char type;unsigned char unused;// 校验和unsigned short csum;// igmp的数据部分,比如加入多播组的时候,group表示多播组ipunsigned long group;
};

接着我们看ip_mc_filter_add

void ip_mc_filter_add(struct device *dev, unsigned long addr)
{char buf[6];// 把多播组ip转成mac多播地址addr=ntohl(addr);buf[0]=0x01;buf[1]=0x00;buf[2]=0x5e;buf[5]=addr&0xFF;addr>>=8;buf[4]=addr&0xFF;addr>>=8;buf[3]=addr&0x7F;dev_mc_add(dev,buf,ETH_ALEN,0);
}

我们知道ip地址是32位,mac地址是48位,但是IANA规定,ipv4组播MAC地址的高24位是0x01005E,第25位是0,低23位是ipv4组播地址的低23位。而多播的ip地址高四位固定是1110。另外低23位被映射到mac多播地址的23位,所以多播ip地址中,有5位是可以随机组合的。这就意味着,每32个多播ip地址,映射到一个mac地址。这会带来一些问题,假设主机x加入了多播组a,主机y加入了多播组b,而a和b对应的mac多播地址是一样的。当主机z给多播组a发送一个数据包的时候,这时候主机x和y的网卡都会处理该数据包,并上报到上层,但是多播组a对应的mac多播地址和多播组b是一样的。我们拿到一个多播组ip的时候,可以计算出他的多播mac地址,但是反过来就不行,因为一个多播mac地址对应了32个多播ip地址。那主机x和y怎么判断是不是发给自己的数据包?因为device维护了一个本device上的多播ip列表,操作系统根据收到的数据包中的ip目的地址和device的多播ip列表对比。如果在列表中,则说明是发给自己的。我们看看具体的实现(来自ip层收到ip数据包时的处理逻辑)。

// 是目的ip是多播ip,并且不是IGMP_ALL_HOSTS,IGMP_ALL_HOSTS是所有多播组的所有主机都可以处理的
if(brd==IS_MULTICAST && iph->daddr!=IGMP_ALL_HOSTS && !(dev->flags&IFF_LOOPBACK)){struct ip_mc_list *ip_mc=dev->ip_mc_list;do{// 找不到,丢包if(ip_mc==NULL){	kfree_skb(skb, FREE_WRITE);return 0;}// 目的ip在该设置的多播ip列表中,处理该数据包if(ip_mc->multiaddr==iph->daddr)break;ip_mc=ip_mc->next;}while(1);}

最后我们看看dev_mc_add。device中维护了当前的mac多播地址列表,他会把这个列表信息同步到网卡中,使得网卡可以处理该列表中多播mac地址的数据包。

void dev_mc_add(struct device *dev, void *addr, int alen, int newonly)
{struct dev_mc_list *dmi;// device维护的多播mac地址列表for(dmi=dev->mc_list;dmi!=NULL;dmi=dmi->next){// 已存在,则引用计数加一if(memcmp(dmi->dmi_addr,addr,dmi->dmi_addrlen)==0 && dmi->dmi_addrlen==alen){if(!newonly)dmi->dmi_users++;return;}}// 不存在则新增一个项到device列表中dmi=(struct dev_mc_list *)kmalloc(sizeof(*dmi),GFP_KERNEL);memcpy(dmi->dmi_addr, addr, alen);dmi->dmi_addrlen=alen;dmi->next=dev->mc_list;dmi->dmi_users=1;dev->mc_list=dmi;dev->mc_count++;// 通知网卡需要处理该多播mac地址dev_mc_upload(dev);
}

网卡的工作模式有几种,分别是正常模式(只接收发给自己的数据包)、混杂模式(接收所有数据包)、多播模式(接收一般数据包和多播数据包)。网卡默认是只处理发给自己的数据包,所以当我们加入一个多播组的时候,我们需要告诉网卡,当收到该多播组的数据包时,需要处理,而不是忽略。dev_mc_upload函数就是通知网卡。

void dev_mc_upload(struct device *dev)
{struct dev_mc_list *dmi;char *data, *tmp;// 不工作了if(!(dev->flags&IFF_UP))return;// 当前是混杂模式,则不需要设置多播了,因为网卡会处理所有收到的数据,不管是不是发给自己的if(dev->flags&IFF_PROMISC){dev->set_multicast_list(dev, -1, NULL);return;}// 多播地址个数,为0,则设置网卡工作模式为正常模式,因为不需要处理多播了if(dev->mc_count==0){dev->set_multicast_list(dev,0,NULL);return;}data=kmalloc(dev->mc_count*dev->addr_len, GFP_KERNEL);// 复制所有的多播mac地址信息for(tmp = data, dmi=dev->mc_list;dmi!=NULL;dmi=dmi->next){memcpy(tmp,dmi->dmi_addr, dmi->dmi_addrlen);tmp+=dev->addr_len;}// 告诉网卡dev->set_multicast_list(dev,dev->mc_count,data);kfree(data);
}

最后我们看一下set_multicast_list

static void
set_multicast_list(struct device *dev, int num_addrs, void *addrs)
{int ioaddr = dev->base_addr;// 多播模式if (num_addrs > 0) {outb(RX_MULT, RX_CMD);inb(RX_STATUS);		/* Clear status. */} else if (num_addrs < 0) { // 混杂模式outb(RX_PROM, RX_CMD);inb(RX_STATUS);} else { // 正常模式outb(RX_NORM, RX_CMD);inb(RX_STATUS);}
}

set_multicast_list就是设置网卡工作模式的函数。至此,我们就成功加入了一个多播组。离开一个多播组也是类似的过程。

2 维护多播组信息

加入多播组后,我们可以主动退出多播组,但是如果追主机挂了,就无法主动退出了,所以多播路由也会定期向所有多播组的所有主机发送探测报文,
2.1 监听来自多播路由的探测报文

void ip_mc_allhost(struct device *dev)
{struct ip_mc_list *i;for(i=dev->ip_mc_list;i!=NULL;i=i->next)if(i->multiaddr==IGMP_ALL_HOSTS)return;i=(struct ip_mc_list *)kmalloc(sizeof(*i), GFP_KERNEL);if(!i)return;i->users=1;i->interface=dev;i->multiaddr=IGMP_ALL_HOSTS;i->next=dev->ip_mc_list;dev->ip_mc_list=i;ip_mc_filter_add(i->interface, i->multiaddr);
}

设备启动的时候,操作系统会设置网卡监听目的ip是224.0.0.1的报文,使得可以处理目的ip是224.0.0.1的多播消息。该类型的报文是多播路由用于查询局域网当前多播组情况的,比如查询哪些多播组已经没有成员了,如果没有成员则删除路由信息。
2.2 处理某设备的IGMP报文

int igmp_rcv(struct sk_buff *skb, struct device *dev, struct options *opt,unsigned long daddr, unsigned short len, unsigned long saddr, int redo,struct inet_protocol *protocol)
{// igmp报头struct igmphdr *igh=(struct igmphdr *)skb->h.raw;// 该数据包是发给所有多播主机的,用于查询本多播组中是否还有成员if(igh->type==IGMP_HOST_MEMBERSHIP_QUERY && daddr==IGMP_ALL_HOSTS)igmp_heard_query(dev);// 该数据包是其他成员对多播路由查询报文的回复,同多播组的主机也会收到if(igh->type==IGMP_HOST_MEMBERSHIP_REPORT && daddr==igh->group)igmp_heard_report(dev,igh->group);kfree_skb(skb, FREE_READ);return 0;
}

IGMP v1只处理两种报文,分别是组成员查询报文(查询组是否有成员),其他成员回复多播路由的报告报文。组成员查询报文由多播路由发出,所有的多播组中的所有主机都可以收到。组成员查询报文的ip协议头的目的地址是224.0.0.1(IGMP_ALL_HOSTS),代表所有的组播主机都可以处理该报文。我们看一下这两种报文的具体实现。

static void igmp_heard_query(struct device *dev)
{struct ip_mc_list *im;for(im=dev->ip_mc_list;im!=NULL;im=im->next)// IGMP_ALL_HOSTS表示所有组播主机if(!im->tm_running && im->multiaddr!=IGMP_ALL_HOSTS)igmp_start_timer(im);
}

该函数用于处理组播路由的查询报文,dev->ip_mc_list是该设备对应的所有多播组信息,这里针对该设备中的每一个多播组,开启对应的定时器,超时后会发送回复报文给多播路由。我们看一下开启定时器的逻辑。

// 开启一个定时器
static void igmp_start_timer(struct ip_mc_list *im)
{int tv;if(im->tm_running)return;tv=random()%(10*HZ);		/* Pick a number any number 8) */im->timer.expires=tv;im->tm_running=1;add_timer(&im->timer);
}

随机选择一个超时时间,然后插入系统维护的定时器队列。为什么使用定时器,而不是立即回复呢?因为多播路由只需要知道某个多播组是否至少还有一个成员,如果有的话就保存该多播组信息,否则就删除路由项。如果某多播组在局域网中有多个成员,那么多个成员都会处理该报文,如果都立即响应,则会引起过多没有必要的流量,因为组播路由只需要收到一个响应就行。我们看看超时时的逻辑。

static void igmp_init_timer(struct ip_mc_list *im)
{im->tm_running=0;init_timer(&im->timer);im->timer.data=(unsigned long)im;im->timer.function=&igmp_timer_expire;
}static void igmp_timer_expire(unsigned long data)
{struct ip_mc_list *im=(struct ip_mc_list *)data;igmp_stop_timer(im);igmp_send_report(im->interface, im->multiaddr, IGMP_HOST_MEMBERSHIP_REPORT);
}

我们看到,超时后会执行igmp_send_report发送一个类型是IGMP_HOST_MEMBERSHIP_REPORT的IGMP、目的ip是多播组ip的报文,说明该多播组还有成员。该报文不仅会发送给多播路由,还会发给同多播组的所有主机。其他主机也是类似的逻辑,即开启一个定时器。所以最快到期的主机会先发送回复报文给多播路由和同多播组的成员,我们看一下其他同多播组的主机收到该类报文时的处理逻辑

// 成员报告报文并且多播组是当前设置关联的多播组
if(igh->type==IGMP_HOST_MEMBERSHIP_REPORT && daddr==igh->group)igmp_heard_report(dev,igh->group);

当一个多播组的其他成员针对多播路由的查询报文作了响应,因为该响应报文的目的ip是多播组ip,所以该多播组的其他成员也能收到该报文。当某个主机收到该类型的报文的时候,就知道同多播组的其他成员已经回复了多播路由了,我们就不需要回复了。

/*收到其他组成员,对于多播路由查询报文的回复,则自己就不用回复了,因为多播路由知道该组还有成员,不会删除路由信息,减少网络流量
*/
static void igmp_heard_report(struct device *dev, unsigned long address)
{struct ip_mc_list *im;for(im=dev->ip_mc_list;im!=NULL;im=im->next)if(im->multiaddr==address)igmp_stop_timer(im);
}

我们看到,这里会删除定时器。即不会作为响应了。
2.3 其他
socket关闭, 退出他之前加入过的多播

void ip_mc_drop_socket(struct sock *sk)
{int i;if(sk->ip_mc_list==NULL)return;for(i=0;i<IP_MAX_MEMBERSHIPS;i++){if(sk->ip_mc_list->multidev[i]){ip_mc_dec_group(sk->ip_mc_list->multidev[i], sk->ip_mc_list->multiaddr[i]);sk->ip_mc_list->multidev[i]=NULL;}}kfree_s(sk->ip_mc_list,sizeof(*sk->ip_mc_list));sk->ip_mc_list=NULL;
}

设备停止工作了,删除对应的多播信息

void ip_mc_drop_device(struct device *dev)
{struct ip_mc_list *i;struct ip_mc_list *j;for(i=dev->ip_mc_list;i!=NULL;i=j){j=i->next;kfree_s(i,sizeof(*i));}dev->ip_mc_list=NULL;
}

以上是IGMP v1版本的实现,在后续v2 v3版本了又增加了很多功能,比如离开组报文(linux1.2.13已经实现了),针对离开报文中的多播组,增加特定组查询报文,用于查询某个组中是否还有成员,另外还有路由选举,当局域网中有多个多播路由,多播路由之间通过协议选举出ip最小的路由为查询路由,定时给多播组发送探测报文。然后成为查询器的多播路由,会定期给其他多播路由同步心跳。否则其他多播路由会在定时器超时时认为当前查询路由已经挂了,重新选举。

这篇关于通过源码理解IGMP v1的实现(基于linux1.2.13)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



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

相关文章

基于 HTML5 Canvas 实现图片旋转与下载功能(完整代码展示)

《基于HTML5Canvas实现图片旋转与下载功能(完整代码展示)》本文将深入剖析一段基于HTML5Canvas的代码,该代码实现了图片的旋转(90度和180度)以及旋转后图片的下载... 目录一、引言二、html 结构分析三、css 样式分析四、JavaScript 功能实现一、引言在 Web 开发中,

SpringBoot中使用Flux实现流式返回的方法小结

《SpringBoot中使用Flux实现流式返回的方法小结》文章介绍流式返回(StreamingResponse)在SpringBoot中通过Flux实现,优势包括提升用户体验、降低内存消耗、支持长连... 目录背景流式返回的核心概念与优势1. 提升用户体验2. 降低内存消耗3. 支持长连接与实时通信在Sp

Conda虚拟环境的复制和迁移的四种方法实现

《Conda虚拟环境的复制和迁移的四种方法实现》本文主要介绍了Conda虚拟环境的复制和迁移的四种方法实现,包括requirements.txt,environment.yml,conda-pack,... 目录在本机复制Conda虚拟环境相同操作系统之间复制环境方法一:requirements.txt方法

Spring Boot 实现 IP 限流的原理、实践与利弊解析

《SpringBoot实现IP限流的原理、实践与利弊解析》在SpringBoot中实现IP限流是一种简单而有效的方式来保障系统的稳定性和可用性,本文给大家介绍SpringBoot实现IP限... 目录一、引言二、IP 限流原理2.1 令牌桶算法2.2 漏桶算法三、使用场景3.1 防止恶意攻击3.2 控制资源

springboot下载接口限速功能实现

《springboot下载接口限速功能实现》通过Redis统计并发数动态调整每个用户带宽,核心逻辑为每秒读取并发送限定数据量,防止单用户占用过多资源,确保整体下载均衡且高效,本文给大家介绍spring... 目录 一、整体目标 二、涉及的主要类/方法✅ 三、核心流程图解(简化) 四、关键代码详解1️⃣ 设置

Nginx 配置跨域的实现及常见问题解决

《Nginx配置跨域的实现及常见问题解决》本文主要介绍了Nginx配置跨域的实现及常见问题解决,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来... 目录1. 跨域1.1 同源策略1.2 跨域资源共享(CORS)2. Nginx 配置跨域的场景2.1

Python中提取文件名扩展名的多种方法实现

《Python中提取文件名扩展名的多种方法实现》在Python编程中,经常会遇到需要从文件名中提取扩展名的场景,Python提供了多种方法来实现这一功能,不同方法适用于不同的场景和需求,包括os.pa... 目录技术背景实现步骤方法一:使用os.path.splitext方法二:使用pathlib模块方法三

CSS实现元素撑满剩余空间的五种方法

《CSS实现元素撑满剩余空间的五种方法》在日常开发中,我们经常需要让某个元素占据容器的剩余空间,本文将介绍5种不同的方法来实现这个需求,并分析各种方法的优缺点,感兴趣的朋友一起看看吧... css实现元素撑满剩余空间的5种方法 在日常开发中,我们经常需要让某个元素占据容器的剩余空间。这是一个常见的布局需求

HTML5 getUserMedia API网页录音实现指南示例小结

《HTML5getUserMediaAPI网页录音实现指南示例小结》本教程将指导你如何利用这一API,结合WebAudioAPI,实现网页录音功能,从获取音频流到处理和保存录音,整个过程将逐步... 目录1. html5 getUserMedia API简介1.1 API概念与历史1.2 功能与优势1.3

Java实现删除文件中的指定内容

《Java实现删除文件中的指定内容》在日常开发中,经常需要对文本文件进行批量处理,其中,删除文件中指定内容是最常见的需求之一,下面我们就来看看如何使用java实现删除文件中的指定内容吧... 目录1. 项目背景详细介绍2. 项目需求详细介绍2.1 功能需求2.2 非功能需求3. 相关技术详细介绍3.1 Ja