多播--概念和程式設計
11.3 多播
單播用於兩個主機之間的端對端通訊,廣播用於一個主機對整個區域網上所有主機上的資料通訊。單播和廣播是兩個極端,要麼對一個主機進行通訊,要麼對整個區域網上的主機進行通訊。實際情況下,經常需要對一組特定的主機進行通訊,而不是整個區域網上的所有主機,這就是多播的用途。
11.3.1 多播的概念
多播,也稱為"組播",將網路中同一業務型別主機進行了邏輯上的分組,進行資料收發的時候其資料僅僅在同一分組中進行,其他的主機沒有加入此分組不能收發對應的 資料。
在廣域網上廣播的時候,其中的交換機和路由器只向需要獲取資料的主機複製並轉發資料。主機可以向路由器請求加入或退出某個組,網路中的路由器和交換機有選擇地複製並傳輸資料,將資料僅僅傳輸給組內的主機。多播的這種功能,可以一次將資料傳送到多個主機,又能保證不影響其他不需要(未加入組)的主機的其他通訊。
相對於傳統的一對一的單播,多播具有如下的優點:
具有同種業務的主機加入同一資料流,共享同一通道,節省了頻寬和伺服器的優點,具有廣播的優點而又沒有廣播所需要的頻寬。
伺服器的總頻寬不受客戶端頻寬的限制。由於組播協議由接收者的需求來確定是否進行資料流的轉發,所以伺服器端的頻寬是常量,與客戶端的數量無關。
與單播一樣,多播是允許在廣域網即Internet上進行傳輸的,而廣播僅僅在同一區域網上才能進行。
組播的缺點:
多播與單播相比沒有糾錯機制,當發生錯誤的時候難以彌補,但是可以在應用層來實現此種功能。
多播的網路支援存在缺陷,需要路由器及網路協議棧的支援。
多播的應用主要有網上視訊、網上會議等。
11.3.2 廣域網的多播
多播的地址是特定的,D類地址用於多播。D類IP地址就是多播IP地址,即224.0.0.0至239.255.255.255之間的IP地址,並被劃分為區域性連線多播地址、預留多播地址和管理許可權多播地址3類:
區域性多播地址:在224.0.0.0~224.0.0.255之間,這是為路由協議和其他用途保留的地址,路由器並不轉發屬於此範圍的IP包。
預留多播地址:在224.0.1.0~238.255.255.255之間,可用於全球範圍(如Internet)或網路協議。
管理許可權多播地址:在239.0.0.0~239.255.255.255之間,可供組織內部使用,類似於私有IP地址,不能用於Internet,可限制多播範圍。
11.3.3 多播的程式設計
多播的程式設計使用setsockopt()函式和getsockopt()函式來實現,組播的選項是IP層的,其選項值和含義參見11.5所示。
表11.5 多播相關的選項
getsockopt()/setsockopt()的選項 |
含 義 |
IP_MULTICAST_TTL |
設定多播組資料的TTL值 |
IP_ADD_MEMBERSHIP |
在指定介面上加入組播組 |
IP_DROP_MEMBERSHIP |
退出組播組 |
IP_MULTICAST_IF |
獲取預設介面或設定介面 |
IP_MULTICAST_LOOP |
禁止組播資料回送 |
1.選項IP_MULTICASE_TTL
選項IP_MULTICAST_TTL允許設定超時TTL,範圍為0~255之間的任何值,例如:
unsigned char ttl=255;
setsockopt(s,IPPROTO_IP,IP_MULTICAST_TTL,&ttl,sizeof(ttl));
2.選項IP_MULTICAST_IF
選項IP_MULTICAST_IF用於設定組播的預設網路介面,會從給定的網路介面傳送,另一個網路介面會忽略此資料。例如:
struct in_addr addr;
setsockopt(s,IPPROTO_IP,IP_MULTICAST_IF,&addr,sizeof(addr))
引數addr是希望多播輸出介面的IP地址,使用INADDR_ANY地址回送到預設介面。
預設情況下,當本機發送組播資料到某個網路介面時,在IP層,資料會回送到本地的迴環介面,選項IP_MULTICAST_LOOP用於控制資料是否回送到本地的迴環介面。例如:
unsigned char loop;
setsockopt(s,IPPROTO_IP,IP_MULTICAST_LOOP,&loop,sizeof(loop))
引數loop設定為0禁止回送,設定為1允許回送。
3.選項IP_ADD_MEMBERSHIP和IP_DROP_MEMBERSHIP
加入或者退出一個組播組,通過選項IP_ADD_MEMBERSHIP和IP_DROP_MEMBER- SHIP,對一個結構struct ip_mreq型別的變數進行控制,struct ip_mreq原型如下:
struct ip_mreq
{
struct in_addr imn_multiaddr; /*加入或者退出的廣播組IP地址*/
struct in_addr imr_interface; /*加入或者退出的網路介面IP地址*/
}
選項IP_ADD_MEMBERSHIP用於加入某個廣播組,之後就可以向這個廣播組傳送資料或者從廣播組接收資料。此選項的值為mreq結構,成員imn_multiaddr是需要加入的廣播組IP地址,成員imr_interface是本機需要加入廣播組的網路介面IP地址。例如:
struct ip_mreq mreq;
setsockopt(s,IPPROTO_IP,IP_ADD_MEMBERSHIP,&mreq,sizeof(mreq))
使用IP_ADD_MEMBERSHIP選項每次只能加入一個網路介面的IP地址到多播組,但並不是一個多播組僅允許一個主機IP地址加入,可以多次呼叫IP_ADD_MEMBERSHIP選項來實現多個IP地址加入同一個廣播組,或者同一個IP地址加入多個廣播組。當imr_ interface為INADDR_ANY時,選擇的是預設組播介面。
4.選項IP_DROP_MEMBERSHIP
選項IP_DROP_MEMBERSHIP用於從一個廣播組中退出。例如:
struct ip_mreq mreq;
setsockopt(s,IPPROTP_IP,IP_DROP_MEMBERSHIP,&mreq,sizeof(sreq))
其中mreq包含了在IP_ADD_MEMBERSHIP中相同的值。
5.多播程式設計的框架
要進行多播的程式設計,需要遵從一定的程式設計框架,其基本順序如圖11.6所示。
(點選檢視大圖)圖11.6 多播的程式設計流程 |
多播程式框架主要包含套接字初始化、設定多播超時時間、加入多播組、傳送資料、接收資料以及從多播組中離開幾個方面。其步驟如下:
(1)建立一個socket。
(2)然後設定多播的引數,例如超時時間TTL、本地迴環許可LOOP等。
(3)加入多播組。
(4)傳送和接收資料。
(5)從多播組離開。
11.3.4 核心中的多播
Linux核心中的多播是利用結構struct ip_mc_socklist來將多播的各個方面連線起來的,其示意圖如圖11.7所示。
圖11.7 多播的核心結構 |
- struct inet_sock {
- ...
- __u8 mc_ttl; /*多播TTL*/
- ...
- __u8 ...
- mc_loop:1; /*多播迴環設定*/
- int mc_index; /*多播裝置序號*/
- __be32 mc_addr; /*多播地址*/
- struct ip_mc_socklist *mc_list; /*多播群陣列*/
- ...
- }
結構成員mc_ttl用於控制多播的TTL;
結構成員mc_loop表示是否迴環有效,用於控制多播資料的本地傳送;
結構成員mc_index用於表示網路裝置的序號;
結構成員mc_addr用於儲存多播的地址;
結構成員mc_list用於儲存多播的群組。
1.結構ip_mc_socklist
結構成員mc_list的原型為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;
- }
成員引數next指向連結串列的下一個節點。
成員引數multi表示組資訊,即在哪一個本地介面上,加入到哪一個多播組。
成員引數sfmode是過濾模式,取值為 MCAST_INCLUDE或MCAST_EXCLUDE,分別表示只接收sflist所列出的那些源的多播資料報,和不接收sflist所列出的那些源的多播資料報。
成員引數sflist是源列表。
2.結構ip_mreqn
multi成員的原型為結構struct ip_mreqn,定義如下:
- struct ip_mreqn
- {
- struct in_addr imr_multiaddr; /*多播組的IP地址*/
- struct in_addr imr_address; /*本地址網路介面的IP地址*/
- int imr_ifindex; /*網路介面序號*/
- }
該結構體的兩個成員分別用於指定所加入的多播組的組IP地址,和所要加入組的那個本地介面的IP地址。該命令字沒有源過濾的功能,它相當於實現IGMPv1的多播加入服務介面。
3.結構ip_sf_socklist
成員sflist的原型為結構struct ip_sf_socklist,定義如下:
- struct ip_sf_socklist
- {
- unsigned int sl_max; /*當前sl_addr陣列的最大可容納量*/
- unsigned int sl_count; /*源地址列表中源地址的數量*/
- __u32 sl_addr[0]; /*源地址列表*/
- }
成員引數sl_addr表示是源地址列表;
成員引數sl_count表示是源地址列表中源地址的數量;
成員引數sl_max表示是當前sl_addr陣列的最大可容納量(不確定)。
4.選項IP_ADD_MEMBERSHIP
選項IP_ADD_MEMBERSHIP用於把一個本地的IP地址加入到一個多播組,在核心中其處理過程如圖11.8所示,在應用層呼叫函式setsockopt()函式的選項IP_ADD_MEMBE- RSHIP後,核心的處理過程如下,主要呼叫了函式ip_mc_join_group()。
圖11.8 選項IP_ADD_MEMBERSHIP的核心處理過程 |
(1)將使用者資料複製如核心。
(2)判斷廣播IP地址是否合法。
(3)查詢IP地址對應的網路介面。
(4)查詢多播列表中是否已經存在多播地址。
(5)將此多播地址加入列表。
(6)返回處理值。
5.選項IP_DROP_MEMBERSHIP
選項IP_DROP_MEMBERSHIP用於把一個本地的IP地址從一個多播組中取出,在核心中其處理過程如圖11.9所示,在應用層呼叫setsockopt()函式的選項IP_DROP_ MEMBERSHIP後,核心的處理過程如下,主要呼叫了函式ip_mc_leave_group()。
(點選檢視大圖)圖11.9 選項IP_DROP_MEMBERSHIP的核心處理過程 |
(1)將使用者資料複製入核心。
(2)查詢IP地址對應的網路介面。
(3)查詢多播列表中是否已經存在多播地址。
(4)將此多播地址從源地址中取出。
(5)將此地址結構從多播列表中取出。
(6)返回處理值。
11.3.5 一個多播例子的伺服器端
下面是一個多播伺服器的例子。多播伺服器的程式設計很簡單,建立一個數據包套接字,選定多播的IP地址和埠,直接向此多播地址傳送資料就可以了。多播伺服器的程式設計,不需要伺服器加入多播組,可以直接向某個多播組傳送資料。
下面的例子持續向多播IP地址"224.0.0.88"的8888埠傳送資料"BROADCAST TEST DATA",每傳送一次間隔5s。
/*
*broadcast_server.c - 多播服務程式
*/
#define MCAST_PORT 8888;
#define MCAST_ADDR "224.0.0.88"/ /*一個區域性連線多播地址,路由器不進行轉發*/
#define MCAST_DATA "BROADCAST TEST DATA" /*多播發送的資料*
#define MCAST_INTERVAL 5 /*傳送間隔時間*/
int main(int argc, char*argv)
{
int s;
struct sockaddr_in mcast_addr;
s = socket(AF_INET, SOCK_DGRAM, 0); /*建立套接字*/
if (s == -1)
{
perror("socket()");
return -1;
}
memset(&mcast_addr, 0, sizeof(mcast_addr));/*初始化IP多播地址為0*/
mcast_addr.sin_family = AF_INET; /*設定協議族類行為AF*/
mcast_addr.sin_addr.s_addr = inet_addr(MCAST_ADDR);/*設定多播IP地址*/
mcast_addr.sin_port = htons(MCAST_PORT); /*設定多播埠*/
/*向多播地址傳送資料*/
while(1) {
int n = sendto(s, /*套接字描述符*/
MCAST_DATA, /*資料*/
sizeof(MCAST_DATA), /*長度*/
0,
(struct sockaddr*)&mcast_addr,
sizeof(mcast_addr)) ;
if( n < 0)
{
perror("sendto()");
return -2;
}
sleep(MCAST_INTERVAL); /*等待一段時間*/
}
return 0;
}
11.3.6 一個多播例子的客戶端
多播組的IP地址為224.0.0.88,埠為8888,當客戶端接收到多播的資料後將打印出來。
客戶端只有在加入多播組後才能接受多播組的資料,因此多播客戶端在接收多播組的資料之前需要先加入多播組,當接收完畢後要退出多播組。
/*
*broadcast_client.c - 多播的客戶端
*/
#define MCAST_PORT 8888;
#define MCAST_ADDR "224.0.0.88" /*一個區域性連線多播地址,路由器不進行轉發*/
#define MCAST_INTERVAL 5 /*傳送間隔時間*/
#define BUFF_SIZE 256 /*接收緩衝區大小*/
int main(int argc, char*argv[])
{
int s; /*套接字檔案描述符*/
struct sockaddr_in local_addr; /*本地地址*/
int err = -1;
s = socket(AF_INET, SOCK_DGRAM, 0); /*建立套接字*/
if (s == -1)
{
perror("socket()");
return -1;
}
/*初始化地址*/
memset(&local_addr, 0, sizeof(local_addr));
local_addr.sin_family = AF_INET;
local_addr.sin_addr.s_addr = htonl(INADDR_ANY);
local_addr.sin_port = htons(MCAST_PORT);
/*繫結socket*/
err = bind(s,(struct sockaddr*)&local_addr, sizeof(local_addr)) ;
if(err < 0)
{
perror("bind()");
return -2;
}
/*設定迴環許可*/
int loop = 1;
err = setsockopt(s,IPPROTO_IP, IP_MULTICAST_LOOP,&loop, sizeof(loop));
if(err < 0)
{
perror("setsockopt():IP_MULTICAST_LOOP");
return -3;
}
struct ip_mreq mreq; /*加入廣播組*/
mreq.imr_multiaddr.s_addr = inet_addr(MCAST_ADDR); /*廣播地址*/
mreq.imr_interface.s_addr = htonl(INADDR_ANY); /*網路介面為預設*/
/*將本機加入廣播組*/
err = setsockopt(s, IPPROTO_IP, IP_ADD_MEMBERSHIP,&mreq, sizeof
(mreq));
if (err < 0)
{
perror("setsockopt():IP_ADD_MEMBERSHIP");
return -4;
}
int times = 0;
int addr_len = 0;
char buff[BUFF_SIZE];
int n = 0;
/*迴圈接收廣播組的訊息,5次後退出*/
for(times = 0;times<5;times++)
{
addr_len = sizeof(local_addr);
memset(buff, 0, BUFF_SIZE); /*清空接收緩衝區*/
/*接收資料*/
n = recvfrom(s, buff, BUFF_SIZE, 0,(struct sockaddr*)&local_addr,
&addr_len);
if( n== -1)
{
perror("recvfrom()");
}
/*列印資訊*/
printf("Recv %dst message from server:%s\n", times, buff);
sleep(MCAST_INTERVAL);
}
/*退出廣播組*/
err = setsockopt(s, IPPROTO_IP, IP_DROP_MEMBERSHIP,&mreq, sizeof
(mreq));
close(s);
return 0;
}