在linux下編程
網絡中的一臺主機如果希望能夠接收到來自網絡中其它主機發往某一個組播組的數據報,那麼這麼主機必須先加入該組播組,然後就可以從組地址接收數據包。在廣域網中,還涉及到路由器支持組播路由等,但本文希望以一個最爲簡單的例子解釋清楚協議棧關於組播的一個最爲簡單明瞭的工作過程,甚至,我們不希望涉及到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已經可以接收組播數據了,關於組播數據如何接收,下回分解。
前面我們講到如何加入到一個組播組中,當一個客戶端完成了加入一個組播組的操作後,就可以從該組接收數據了。下面我們看看組播數據報接收的詳細流程。
通過加入組播組的操作後,網絡設備接口已經知道要接收該組的數據報,所以組播數據會從網卡接收進來,一直到達myip_rcv函數,我們就從myip_rcv函數開始,跟蹤整個組播數據報的接收流程。
同樣,myip_rcv還是先檢查數據報的類型(是否爲本機需要接收的包),ip首部是否正確,然後調用myip_rcv_finish。myip_rcv_finish對任何數據報都要先查找輸入路由,輸入路由查找函數是myip_route_input,當該函數在路由緩存myrt_hash_table中找不到相應的路由項時,判斷數據報的輸入地址,如果發現是組播地址,就不能簡單地查找FIB,而是要作特殊處理。
首先,調用myip_check_mc對這個組播數據報作檢查,從網絡設備接口的struct in_device中去匹配組播地址,如果匹配不到,表示這個不是我們希望接收的組播包,丟棄。匹配到了,則作下一步檢查,如果這本身就是一個IGMP包,則接收,否則,查看這個組播組在我們的struct in_device中設置的過濾機制,如果該數據報的源地址在我們的過濾名單中,則丟棄,否則接收。
如果檢查通過,準備接收這個組播包,則調用myip_route_input_mc查找組播輸入路由,這是一個專門爲組播設置的函數,它第一步要檢查數據報源地址的有效性,即源地址不能是組播地址,不能是廣播地址,也不能是迴環地址,同時,該數據報必須是一個因特網協議包(ETH_P_IP)。如果源地址爲0,那麼只有當目的地址是224.0.0.0-224.0.0.255之間的值(只能在發送主機所在的一個子網內的傳送,不會通過路由器轉發。)時,系統可以自己選定一個scope爲RT_SCOPE_LINK的源地址,否則出錯。
當驗證了源地址的有效性之後,我們建立路由項,即結構體struct rtable。該路由項的rt_type值是RTN_MULTICAST,表示這一條組播路由。對於本地接收的組播包,我們設置接收函數爲myip_local_deliver。
有了這個路由項,我們可以通過調用myip_local_deliver,繼續接收流程,這部分流程前面已有多次介紹,所以講得簡單一點,只注意組播特有的。同樣,到myip_local_deliver_finish後,首先要檢查是否有raw socket要接收這個組播包。然後根據IP首部裏協議字段,調用相應協議的接收函數,我們這兒是一個UDP組播包,所以調用myudp_rcv。
myudp_rcv首先會對路由項的成員rt_flags作一個檢查,如果發現它有RTCF_BROADCAST或者RTCF_MULTICAST,就不會走常規的從myudp_hash中匹配源和目的地址,找到socket,把數據報放入接收隊列這麼一個流程。而是調用函數myudp_v4_mcast_deliver,這是一個專用於接收UDP組播數據報的函數,它首先根據目的端口確定在哈希表mydup_hash中的位置,然後遍歷找到的這個鏈表。與普通的UDP數據報接收相比,它多一個過濾檢查,即在套接字結構體的成員mc_list中找到與該數據報所屬組對應的ip_mc_socklist項,查看它的過濾配置,確認該數據報的源地址是否在過濾列表中。如果不在,則把數據放到該socket的接收隊列中,完成組播數據報的接收。