網絡中的一台主機如果希望能夠接收到來自網絡中其它主機發往某一個組播組的數據報,那么這么主機必須先加入該組播組,然后就可以從組地址接收數據包。在廣域網中,還涉及到路由器支持組播路由等,但本文希望以一個最為簡單的例子解釋清楚協議棧關於組播的一個最為簡單明了的工作過程,甚至,我們不希望涉及到IGMP包。
我們先從一個組播客戶端的應用程序入手來解析組播的工作過程:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include "my_inet.h"
#include <arpa/inet.h>
#define MAXBUF 256
#define PUERTO 5000
#define GRUPO "224.0.1.1"
int main(void)
{
int fd, n, r;
struct sockaddr_in srv, cli;
struct ip_mreq mreq;
char buf[MAXBUF];
memset( &srv, 0, sizeof(struct sockaddr_in) );
memset( &cli, 0, sizeof(struct sockaddr_in) );
memset( &mreq, 0, sizeof(struct ip_mreq) );
srv.sin_family = MY_AF_INET;
srv.sin_port = htons(PUERTO);
if( inet_aton(GRUPO, &srv.sin_addr ) < 0 ) {
perror("inet_aton");
return -1;
}
if( (fd = socket( MY_AF_INET, SOCK_DGRAM, MY_IPPROTO_UDP) ) < 0 ){
perror("socket");
return -1;
}
if( bind(fd, (struct sockaddr *)&srv, sizeof(srv)) < 0 ){
perror("bind");
return -1;
}
if (inet_aton(GRUPO, &mreq.imr_multiaddr) < 0) {
perror("inet_aton");
return -1;
}
inet_aton( "172.16.48.2", &(mreq.imr_interface) );
if( setsockopt(fd, SOL_IP, IP_ADD_MEMBERSHIP, &mreq,sizeof(mreq)) < 0 ){
perror("setsockopt");
return -1;
}
n = sizeof(cli);
while(1){
if( (r = recvfrom(fd, buf, MAXBUF, 0, (struct sockaddr *)&cli, (socklen_t*)&n)) < 0 ){
perror("recvfrom");
}else{
buf[r] = 0;
fprintf(stdout, "Mensaje desde %s: %s", inet_ntoa(cli.sin_addr), buf);
}
}
}
這是一個非常簡單的組播客戶端,它指定從組播組224.0.1.1的5000端口讀數據,並顯示在終端上,下面我們通過分析該程序來了解內核的工作過程。
前面我們講過,bind操作首先檢查用戶指定的端口是否可用,然后為socket的一些成員設置正確的值,並添加到哈希表myudp_hash中。然后,協議棧每次收到UDP數據,就會檢查該數據報的源和目的地址,還有源和目的端口,在myudp_hash中找到匹配的socket,把該數據報放入該socket的接收隊列,以備用戶讀取。在這個程序中,bind操作把socket綁定到地址224.0.0.1:5000上, 該操作產生的直接結果就是,對於socket本身,下列值受影響:
struct inet_sock{
.rcv_saddr = 224.0.0.1;
.saddr = 0.0.0.0;
.sport = 5000;
.daddr = 0.0.0.0;
.dport = 0;
}
這五個數據表示,該套接字在發送數據包時,本地使用端口5000,本地可以使用任意一個網絡設備接口,發往的目的地址不指定。在接收數據時,只接收發往IP地址224.0.0.1的端口為5000的數據。
程序中,緊接着bind有一個setsockopt操作,它的作用是將socket加入一個組播組,因為socket要接收組播地址224.0.0.1的數據,它就必須加入該組播組。結構體struct ip_mreq mreq是該操作的參數,下面是其定義:
struct ip_mreq
{
struct in_addr imr_multiaddr; // 組播組的IP地址。
struct in_addr imr_interface; // 本地某一網絡設備接口的IP地址。
};
一台主機上可能有多塊網卡,接入多個不同的子網,imr_interface參數就是指定一個特定的設備接口,告訴協議棧只想在這個設備所在的子網中加入某個組播組。有了這兩個參數,協議棧就能知道:在哪個網絡設備接口上加入哪個組播組。為了簡單起見,我們的程序中直接寫明了IP地址:在172.16.48.2所在的設備接口上加入組播組224.0.1.1。
這個操作是在網絡層上的一個選項,所以級別是SOL_IP,IP_ADD_MEMBERSHIP選項把用戶傳入的參數拷貝成了struct ip_mreqn結構體:
struct ip_mreqn
{
struct in_addr imr_multiaddr;
struct in_addr imr_address;
int imr_ifindex;
};
多了一個輸入接口的索引,暫時被拷貝成零。
該操作最終引發內核函數myip_mc_join_group執行加入組播組的操作。首先檢查imr_multiaddr是否為合法的組播地址,然后根據imr_interface的值找到對應的struct in_device結構。接下來就要為socket加入到組播組了,在inet_sock的結構體中有一個成員mc_list,它是一個結構體struct ip_mc_socklist的鏈表,每一個節點代表socket當前正加入的一個組播組,該鏈表是有上限限制的,缺省值為IP_MAX_MEMBERSHIPS(20),也就是說一個socket最多允許同時加入20個組播組。下面是struct ip_mc_socklist的定義:
struct ip_mc_socklist
{
struct ip_mc_socklist *next;
struct ip_mreqn multi;
unsigned int sfmode; /* MCAST_{INCLUDE,EXCLUDE} */
struct ip_sf_socklist *sflist;
};
struct ip_sf_socklist
{
unsigned int sl_max;
unsigned int sl_count;
__u32 sl_addr[0];
};
除了multi成員,它還有一個源過濾機制。如果我們新添加的struct ip_mreqn已經存在於這個鏈表中(表示socket早就加入這個組播組了),那么不做任何事情,否則,創建一個新的struct ip_mc_socklist:
struct ip_mc_socklist
{
.next = inet->mc_list; //新節點放到鏈表頭。
.multi = 傳入的參數; //這是關鍵的組信息。
.sfmode = MCAST_EXCLUDE; //過濾掉sflist中的所有源。
.sflist = NULL; //沒有源需要過濾。
};
最后,調用myip_mc_inc_group函數在struct in_device和struct net_device的mc_list鏈表中都添上相應的組播組節點,關於這部分的細節可以在前一篇文章《初識組播2》中找到。不再重復。
到此為止,我們完成了最為簡單的加入組播組的操作,對於同一子網內的情況,socket已經可以接收組播數據了,關於組播數據如何接收,下回分解。