- 論壇徽章:
- 0
|
組播通信
摘要:
本文可做為TCP/IP組播技術(shù)的入門材料,文中介紹了組播通信的概念及原理,以及用于組播應用編程的Linux API的詳細資料。為了使讀者更加完整的了解Linux 組播的整體概念,文中對實現(xiàn)該技術(shù)的核心函數(shù)也做了介紹。在文章的最后給出了一個簡單的C語言套接字編程例子,說明如何創(chuàng)建組播應用程序。
一、導言
在網(wǎng)絡中,主機間可以用三種不同的地址進行通信:
單播地址(unicast):即在子網(wǎng)中主機的唯一地址(接口)。如IP地址:192.168.100.9或MAC地址:80:C0:F6:A0:4A:B1。
廣播地址:這種類型的地址用來向子網(wǎng)內(nèi)的所有主機(接口)發(fā)送數(shù)據(jù)。如廣播IP地址是192.168.100.255,MAC廣播地址:FF:FF:FF:FF:FF。
組播地址:通過該地址向子網(wǎng)內(nèi)的多個主機即主機群(接口)發(fā)送數(shù)據(jù)。
如果只是向子網(wǎng)內(nèi)的部分主機發(fā)送報文,組播地址就很有用處了;在需要向多個主機發(fā)送多媒體信息(如實時音頻、視頻)的情況下,考慮到其所需的帶寬,分別向每一客戶端主機發(fā)送數(shù)據(jù)并不是個好辦法,如果發(fā)送主機與某些接收端的客戶主機不在子網(wǎng)之內(nèi),采用廣播方式也不是一個好的解決方案。
二、組播地址
大家知道,IP地址空間被劃分為A、B、C三類。第四類即D類地址被保留用做組播地址。在第四版的IP
協(xié)議
(IPv4)中,從224.0.0.0到239.255.255.255間的所有IP地址都屬于D類地址。
組播地址中最重要的是第24位到27位間的這四位,對應到十進制是224到239,其它28位保留用做組播的組標識,如下圖所示:
圖1 組播地址示意圖
IPv4的組播地址在網(wǎng)絡層要轉(zhuǎn)換成網(wǎng)絡物理地址。對一個單播的網(wǎng)絡地址,通過ARP
協(xié)議
可以獲取與IP地址對應的物理地址。但在組播方式下ARP
協(xié)議
無法完成類似功能,必須得用其它的方法獲取物理地址。在下面列出的RFC文檔中提出了完成這個轉(zhuǎn)換過程的方法:
RFC1112:Multicast IPv4 to Ethernet physical address correspondence
RFC1390:Correspondence to FDDI
RFC1469:Correspondence to Token-Ring networks
在最大的以太網(wǎng)地址范圍內(nèi),轉(zhuǎn)換過程是這樣的:將以太網(wǎng)地址的前24位最固定為01:00:5E,這幾位是重要的標志位。緊接著的一位固定為0,其它23位用IPv4組播地址中的低23位來填充。該轉(zhuǎn)換過程如下圖所示:
圖2 地址轉(zhuǎn)換示意圖
例如,組播地址為224.0.0.5其以太網(wǎng)物理地址為01:00:5E:00:00:05。
還有一些特殊的IPv4組播地址:
224.0.0.1:標識子網(wǎng)中的所有主機。同一個子網(wǎng)中具有組播功能的主機都是這個組的成員。
224.0.0.2:該地址用來標識網(wǎng)絡中每個具有組播功有的
路由
器。
224.0.0.0----224.0.0.255范圍內(nèi)的地址被分配給了低層次的協(xié)議。向這些范圍內(nèi)的地址發(fā)送數(shù)據(jù)包,有組播功能的路由器將不會為其提供路由。
239.0.0.0----239.255.255.255間的地址分配用做管理用途。這些地址被分配給局部的每一個組織,但不可以分配到組織外部,組織內(nèi)的路由器不向在組織外的地址提供路由。
除了上面列出的部分組播地址外,還有許多的組播地址。在最新版本的RFC文檔“Assinged Numbers”中有完整的介紹。
下面的表中列出了全部的組播地址空間,同時還列出了相應的地址段的常用名稱及其TTL(IP包的存活時間)。在IPv4組播方式下,TTL有雙重意義:正如大家所知的,TTL原本用來控制數(shù)據(jù)包在網(wǎng)絡中的存活時間,防止由于路由器配置錯誤導致出現(xiàn)數(shù)據(jù)包傳播的死循環(huán);在組播方式下,它還代表了數(shù)據(jù)包的活動范圍,如:數(shù)據(jù)包在網(wǎng)絡中能夠傳送多遠?這樣就可以基于數(shù)據(jù)包的分類來定義其傳送范圍。
范圍 TTL 地址區(qū)間 描述
節(jié)點(Node) 0 只能向本機發(fā)送的數(shù)據(jù)包,不能向網(wǎng)絡中的其它接口傳送
鏈路(Link) 1 224.0.0.0-224.0.0.255 只能在發(fā)送主機所在的一個子網(wǎng)內(nèi)的傳送,不會通過路由器轉(zhuǎn)發(fā)。
部門 32 239.255.0.0-239.255.255.255 只在整個組織下的一個部門內(nèi)(Department) 傳送
組織 64 239.192.0.0--239.195.255.255 在整個組織內(nèi)傳送(Organization)
全局(Global)255 224.0.1.0--238.255.255.255 沒有限制,可全局范圍內(nèi)傳送
三、組播的工作過程
在局域網(wǎng)內(nèi),主機的網(wǎng)絡接口將到目的主機的數(shù)據(jù)包發(fā)送到高層,這些數(shù)據(jù)包中的目的地址是物理接口地址或廣播地址。
如果主機已經(jīng)加入到一個組播組中,主機的網(wǎng)絡接口就會識別出發(fā)送到該組成員的數(shù)據(jù)包。
因此,如果主機接口的物理地址為80:C0:F6:A0:4A:B1,其加入的組播組為224.0.1.10,則發(fā)送給主機的數(shù)據(jù)包中的目的地址必是下面三種類型之一:
接口地址:80:C0:F6:A0:4A:B1
廣播地址:FF:FF:FF:FF:FF:FF:FF:FF
組播地址:01:00:5E:00:01:0A
廣域網(wǎng)中,路由器必須支持組播路由。當主機中運行的進程加入到某個組播組中時,主機向子網(wǎng)中的所有組播路由器發(fā)送IGMP(Internet分組管理協(xié)議)報文,告訴路由器凡是發(fā)送到這個組播組的組播報文都必須發(fā)送到本地的子網(wǎng)中,這樣主機的進程就可以接收到報文了。子網(wǎng)中的路由器再通知其它的路由器,這些路由器就知道該將組播報文轉(zhuǎn)發(fā)到哪些子網(wǎng)中去。
子網(wǎng)中的路由器也向224.0.0.1發(fā)送一個IGMP報文(224.0.0.1代表組中的全部主機),要求組中的主機提供組的相關(guān)信息。組中的主機收到這個報文后,都各將計數(shù)器的值設為隨機值,當計數(shù)器遞減為0時再向路由器發(fā)送應答。這樣就防止了組中所有的主機同時向路由器發(fā)送應答,造成網(wǎng)絡擁塞。主機向組播地址發(fā)送一個報文做為對路由器的應答,組中的其它主機一旦看到這個應答報文,就不再發(fā)送應答報文了,因為組中的主機向路由器提供的都是相同的信息,所以子網(wǎng)路由器只需得到組中一個主機提供的信息就可以了。
如果組中的主機都退出了,路由器就收不到應答,因此路由器認為該組目前沒有主機加入,遂停止到該子網(wǎng)報文的路由。IGMPv2的解決方案是:組中的主機在退出時向224.0.0.2 發(fā)送報文通知組播路由器。
四、應用編程接口(API)
如果你有套接字編程的經(jīng)驗,就會發(fā)現(xiàn),對組播選項所進行的操作只需五個新的套接字操作。函數(shù)setsockopt()及getsockopt()用來建立和讀取這五個選項的值。下表中列出了組播的可選項,并列出其數(shù)據(jù)類型和描述:
IPv4 選項 數(shù)據(jù)類型 描 述
IP_ADD_MEMBERSHIP struct ip_mreq 加入到組播組中
IP_DROP_MEMBERSHIP struct ip_mreq 從組播組中退出
IP_MULTICAST_IF struct ip_mreq 指定提交組播報文的接口
IP_MULTICAST_TTL u_char 指定提交組播報文的TTL
IP_MULTICAST_LOOP u_char 使組播報文環(huán)路有效或無效
在頭文件中定義了ip_mreq結(jié)構(gòu):
struct ip_mreq {
struct in_addr imr_multiaddr; /* IP multicast address of group */
struct in_addr imr_interface; /* local IP address of interface */
};
在頭文件中組播選項的值為:
#define IP_MULTICAST_IF 32
#define IP_MULTICAST_TTL 33
#define IP_MULTICAST_LOOP 34
#define IP_ADD_MEMBERSHIP 35
#define IP_DROP_MEMBERSHIP 36
IP_ADD_MEMBERSHIP
若進程要加入到一個組播組中,用soket的setsockopt()函數(shù)發(fā)送該選項。該選項類型是ip_mreq結(jié)構(gòu),它的第一個字段imr_multiaddr指定了組播組的地址,第二個字段imr_interface指定了接口的IPv4地址。
IP_DROP_MEMBERSHIP
該選項用來從某個組播組中退出。數(shù)據(jù)結(jié)構(gòu)ip_mreq的使用方法與上面相同。
IP_MULTICAST_IF
該選項可以修改網(wǎng)絡接口,在結(jié)構(gòu)ip_mreq中定義新的接口。
IP_MULTICAST_TTL
設置組播報文的數(shù)據(jù)包的TTL(生存時間)。默認值是1,表示數(shù)據(jù)包只能在本地的子網(wǎng)中傳送。
IP_MULTICAST_LOOP
組播組中的成員自己也會收到它向本組發(fā)送的報文。這個選項用于選擇是否激活這種狀態(tài)。
五、一個組播通信的例子
下面給出一個簡單的例子實現(xiàn)文中闡述的思想:由一個進程向一個組播組發(fā)送報文,組播組中的相關(guān)進程接收報文,并將報文顯示到屏幕上。
下面的代碼實現(xiàn)了一個服務進程,它將標準輸入接口輸入的信息全部發(fā)送到組播組224.0.1.1。你會發(fā)現(xiàn),將信息發(fā)送到組播組不需要特別的操作,只要設置好組播組的目的地址就足夠了。若在開發(fā)過程中,Loopback和TTL這兩個選項的默認值不適合應用程序,可以加以調(diào)整。
服務程序
將標準輸入端口的輸入發(fā)送到組播組224.0.1.1。
#include
#include
#include
#include
#include
#include
#define MAXBUF 256
#define PUERTO 5000
#define GROUP "224.0.1.1"
int main(void) {
int s;
struct sockaddr_in srv;
char buf;
bzero(&srv, sizeof(srv));
srv.sin_family = AF_INET;
srv.sin_port = htons(PUERTO);
if (inet_aton(GRUPO, &srv.sin_addr) perror("inet_aton");
return 1;
}
if ((s = socket(AF_INET, SOCK_DGRAM, 0)) perror("socket");
return 1;
}
while (fgets(buf, MAXBUF, stdin)) {
if (sendto(s, buf, strlen(buf), 0,
(struct sockaddr *)&srv, sizeof(srv)) perror("recvfrom");
} else {
fprintf(stdout, "Enviado a %s: %s", GRUPO, buf);
}
}
}
客戶端程序
#include
#include
#include
#include
#include
#include
#define MAXBUF 256
#define PUERTO 5000
#define GROUP "224.0.1.1"
int main(void) {
int s, n, r;
struct sockaddr_in srv, cli;
struct ip_mreq mreq;
char buf;
bzero(&srv, sizeof(srv));
srv.sin_family = AF_INET;
srv.sin_port = htons(PUERTO);
if (inet_aton(GRUPO, &srv.sin_addr) perror("inet_aton");
return 1;
}
if ((s = socket(AF_INET, SOCK_DGRAM, 0)) perror("socket");
return 1;
}
if (bind(s, (struct sockaddr *)&srv, sizeof(srv)) perror("bind");
return 1;
}
if (inet_aton(GRUPO, &mreq.imr_multiaddr) perror("inet_aton");
return 1;
}
mreq.imr_interface.s_addr = htonl(INADDR_ANY);
if (setsockopt(s,IPPROTO_IP,IP_ADD_MEMBERSHIP,&mreq,sizeof(mreq))
perror("setsockopt");
return 1;
}
n = sizeof(cli);
while (1) {
if ((r = recvfrom(s, buf, MAXBUF, 0, (struct sockaddr *)
&cli, &n)) perror("recvfrom");
} else {
buf = 0;
fprintf(stdout, "Mensaje desde %s: %s
",
inet_ntoa(cli.sin_addr), buf);
}
}
}
六、內(nèi)核與組播
在上面的例子中我們看到:如果一個進程要加入到組播組中,就要使用setsockopt()函數(shù)在IP層設置IP_ADD_MEMBERSHIP。
在/usr/src/
linux
/net/ipv4/ip_sockglue.c文件中可以找見該函數(shù)的源代碼。 其中設置IP_ADD_MEMBERSHIP和IP_DROP_MEMBERSHIP的部分代碼如下:
struct ip_mreqn mreq;
if (optlen return -EINVAL;
if (optlen >= sizeof(struct ip_mreqn)) {
if(copy_from_user(&mreq,optval,sizeof(mreq)))
return -EFAULT;
} else {
memset(&mreq, 0, sizeof(mreq));
if (copy_from_user(&mreq,optval,sizeof(struct ip_mreq)))
return -EFAULT;
}
if (optname == IP_ADD_MEMBERSHIP)
return ip_mc_join_group(sk,&mreq);
else
return ip_mc_leave_group(sk,&mreq);
程序一開始先檢查輸入?yún)?shù)ip_mreq結(jié)構(gòu)的長度是否正確,并將其從用戶區(qū)復制到內(nèi)核區(qū)。在得到參數(shù)的值后,接著調(diào)用ip_mc_join_group()加入到組播組或調(diào)用ip_mc_leave_group()退出組播組。
在/usr/src/
linux
/net/ipv4/igmp.c中可以找到這些函數(shù)的代碼。加入組播組的源程序代碼如下:
int ip_mc_join_group(struct sock *sk , struct ip_mreqn *imr)
{
int err;
u32 addr = imr->imr_multiaddr.s_addr;
struct ip_mc_socklist, *iml, *i;
struct in_device *in_dev;
int count = 0;
在開始部分用MULTICAST宏檢查組的地址,確認其在保留的組播組地址范圍內(nèi)。只要檢查IP地址的第一部分是不是224就可以確認地址是否有效:
if (!MULTICAST(addr))
return -EINVAL;
rtnl_shlock();
檢查完組播地址后,接著就要設置網(wǎng)絡接口了。如果不能通過接口索引獲得網(wǎng)絡接口(如在IPv6下),在這種情況下可以調(diào)用函數(shù)ip_mc_find_dev()獲取網(wǎng)絡接口。在本文中不存在這個問題,因為我們的工作都是在IPv4下進行的。若地址的格式是INADDR_ANY,內(nèi)核就依照路由表的定義,按照組地址在路由表中查找網(wǎng)絡接口。
if (!imr->imr_ifindex)
in_dev = ip_mc_find_dev(imr);
else
in_dev = inetdev_by_index(imr->imr_ifindex);
if (!in_dev) {
iml = NULL;
err = -ENODEV;
goto done;
}
接著給ip_mc_socklist結(jié)構(gòu)分配內(nèi)存,然后比較套接字的每個組地址和接口。只要發(fā)現(xiàn)了一個匹配項就跳出該函數(shù),因為有一個匹配項就可以了。若網(wǎng)絡接口地址不是INADDR_ANY,相應的計數(shù)器值就要增加。
iml = (struct ip_mc_socklist *)sock_kmalloc(sk, sizeof(*iml),
GFP_KERNEL);
err = -EADDRINUSE;
for (i=sk->ip_mc_list; i; i=i->next) {
if (memcmp(&i->multi, imr, sizeof(*imr)) == 0) {
/* New style additions are reference counted */
if (imr->imr_address.s_addr == 0) {
i->count ;
err = 0;
}
goto done;
}
count ;
}
err = -ENOBUFS;
if (iml == NULL' 'count >= sysctl_igmp_max_memberships)
goto done;
到這里,就可以用新創(chuàng)建的套接字與組播組建立鏈接了,這時還必須創(chuàng)建一個新的記錄,記錄下屬于該套接字的組的列表。首先還是要預先分配內(nèi)存,然后只要給相關(guān)結(jié)構(gòu)中的幾個字段賦值,就完成了這個操作:
memcpy(&iml->multi,imr, sizeof(*imr));
iml->next = sk->ip_mc_list;
iml->count = 1;
sk->ip_mc_list = iml;
ip_mc_inc_group(in_dev,addr);
iml = NULL;
err = 0;
done:
rtnl_shunlock();
if (iml)
sock_kfree_s(sk, iml, sizeof(*iml));
return err;
}
用函數(shù)ip_mc_leave_group()從一個組播組中退出,它的工作過程比前面的函數(shù)要來得簡單。首先在套接字記錄中查找組播組及接口地址,找到后,將調(diào)用這個接口地址的進程數(shù)的值遞減,若該值為0,就刪除該計數(shù)器,因為與組播組相關(guān)的進程至少要有一個。
int ip_mc_leave_group(struct sock *sk, struct ip_mreqn *imr)
{
struct ip_mc_socklist *iml, **imlp;
for (imlp=&sk->ip_mc_list;(iml=*imlp)!=NULL; imlp=&iml->next) {
if (iml->multi.imr_multiaddr.s_addr==imr->imr_multiaddr.s_addr
&& iml->multi.imr_address.s_addr==imr->imr_address.s_addr &&
(!imr->imr_ifindex' 'iml->multi.imr_ifindex==imr->imr_ifindex)) {
struct in_device *in_dev;
if (--iml->count)
return 0;
*imlp = iml->next;
synchronize_bh();
in_dev = inetdev_by_index(iml->multi.imr_ifindex);
if (in_dev)
ip_mc_dec_group(in_dev, imr->imr_multiaddr.s_addr);
sock_kfree_s(sk, iml, sizeof(*iml));
return 0;
}
}
return -EADDRNOTAVAIL;
}
其它的組播選項都很簡單,只要給當前套接字內(nèi)的字段賦值就可以了,賦值的過程由ip_setsockopt()函數(shù)完成。
本文來自ChinaUnix博客,如果查看原文請點:http://blog.chinaunix.net/u1/35065/showart_2154942.html |
|