UDP組播接收端解析
網路中的一臺主機如果希望能夠接收到來自網路中其它主機發往某一個組播組的資料報,那麼這麼主機必須先加入該組播組,然後就可以從組地址接收資料包。在廣域網中,還涉及到路由器支援組播路由等,但本文希望以一個最為簡單的例子解釋清楚協議棧關於組播的一個最為簡單明瞭的工作過程,甚至,我們不希望涉及到 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已經可以接收組播資料了,關於組播資料如何接收,下回分解。