多播的實現(xiàn)和需要注意的問題
前段時間研究了一小段時間的網(wǎng)絡(luò)多播問題,自己很有感觸,把自己的經(jīng)歷寫出來,希望有需要的可以少走一些彎路。
先說一下原理,我覺得這個還是需要說一下的。
網(wǎng)絡(luò)中存在三種傳輸概念,單播,多播,廣播,單播和廣播大家可能都很了解,單播,連接的建立是一對一的,廣播則是向一個網(wǎng)絡(luò)內(nèi)所有用戶發(fā)送。
我們這里只說多播,多播的好處我就不說了,節(jié)省帶寬什么的。
其實我個人覺得,單播多播都可以看錯是某種意義上的廣播,單播可以理解為網(wǎng)絡(luò)只有一個用戶,多播則可以理解為是受限制的一組廣播用戶(指定的一組用戶)。
網(wǎng)絡(luò)中存在五種IP地址,A,B,C,D,E類
需要明白的一點事,IP地址分為兩部分,IP=類別+網(wǎng)絡(luò)號+主機號
其中,對于A類地址來說,10.0.0.0 ~ 10.255.255.255為私有地址,127.0.0.0~127.255.255.255為回環(huán)地址,主機ID全0標(biāo)識一個網(wǎng)絡(luò),主機ID全1表示廣播地址,B類地址:172.16.0.0 ~ 172.31.255.255為私有地址,主機ID全0標(biāo)識一個網(wǎng)絡(luò),主機ID全1表示廣播地址,C類地址:192.168.0.0 ~ 192.168.255.255為私有地址(這個應(yīng)該很熟悉吧),主機ID全0標(biāo)識一個網(wǎng)絡(luò),主機ID全1表示廣播地址。
組播對應(yīng)的MAC地址:01-00-5e-xx-xx-xx
映射關(guān)系:
多播只會由感興趣的端口接收,他是怎么知道這些端口的呢?這里就要說D類地址了。這里一定要理解一個概念:多播組。多播的數(shù)據(jù)是定向的發(fā)給一個多播組的,這樣凡是多播組內(nèi)的成員就會收到數(shù)據(jù),有人問了,網(wǎng)絡(luò)上有那么多多播組,是怎么知道要發(fā)給哪個組。這里就是D類地址了,D類地址充當(dāng)了多播組的標(biāo)識,記住,僅僅是標(biāo)識??梢岳斫鉃椋嗖ソM的目的地址,多播組的ID。所有的主機可以選擇加入多播組,也就是被標(biāo)記為一個該多播組的一個ID。如何加入多播組是技術(shù)問題,我們后面講。
那么接下來的問題是,分布在全球的這么多臺主機,如果美國的一臺主機加入了這個多播組,英國的一個主機也加入了同樣一個多播組,而我源頭是中國這邊的主機,那么他怎么發(fā)過去呢?
需要考慮的問題,數(shù)據(jù)包如何到達子網(wǎng)路由器(主機-路由器之間的組成員關(guān)系協(xié)議),數(shù)據(jù)包如何在公網(wǎng)內(nèi)路由(路由器-路由器之間的組播路由協(xié)議)(轉(zhuǎn)發(fā)規(guī)則),數(shù)據(jù)如何被目的子網(wǎng)路由器接受并轉(zhuǎn)發(fā)。
首先,這個數(shù)據(jù)包要能到達你所在的子網(wǎng)的路由器,這一步如何實現(xiàn)的?答案,是IGMP協(xié)議。
IGMP(Internet Group Management Protocol),看名字就可以看出是因特網(wǎng)組播管理協(xié)議。是主機與路由器之間唯一的信令協(xié)議。目前有三個版本,V1,V2,V3(不同之處主要是V1,V2,V2是主動離開組播組,V1則是不會主動離開).通過用wireshark軟件抓包測試,你會發(fā)現(xiàn),目前網(wǎng)絡(luò)上大部分是V2版本的組播協(xié)議包。主機向本地路由器發(fā)送一個IGMP,加入相對應(yīng)的組播(組播地址端口,自己定義)。這樣主機是可以加入組播了,但是到來的組播數(shù)據(jù)包是如何知道數(shù)據(jù)包要發(fā)給誰呢?還是IGMP!當(dāng)發(fā)現(xiàn)有數(shù)據(jù)包來的時候,本地路由器向本地子網(wǎng)內(nèi)的主機發(fā)送一個查詢報文(IGMP),加入了多播組的主機則會發(fā)送一個回復(fù)給路由器(IGMP包),那后面就會轉(zhuǎn)發(fā)此數(shù)據(jù)包了。如果主機要離開組播組怎么辦呢?也是IGMP!主機只需要向路由器發(fā)送一個離開的消息(IGMP包)給路由器就可以了。
數(shù)據(jù)發(fā)送到路由器后,路由器根據(jù)什么將數(shù)據(jù)包轉(zhuǎn)發(fā)到其他路由器呢(公網(wǎng)內(nèi)路由器的轉(zhuǎn)發(fā))?答案是:域內(nèi)組播路由協(xié)議及域間組播路由協(xié)議。其實個人感覺不需要區(qū)分這個域間和域內(nèi)路由,我們只需要關(guān)心,數(shù)據(jù)包可以在因特網(wǎng)上自由轉(zhuǎn)發(fā)就可以了。這里需要知道的是兩個域內(nèi)路由協(xié)議,PIM-SM,PIM-DM,DVMRP(主要區(qū)別是密集模式和稀疏模式區(qū)別)。路由器間的轉(zhuǎn)發(fā)需要的是這幾個路由協(xié)議,原理在網(wǎng)上一搜一大堆,我就不講了。這幾個協(xié)議主要在轉(zhuǎn)發(fā),鄰居發(fā)現(xiàn)什么的有些區(qū)別,比如說剪枝策略。
組播的轉(zhuǎn)發(fā)利用了一個叫逆向路徑轉(zhuǎn)發(fā)策略(RPF),RPF協(xié)議決定是否轉(zhuǎn)發(fā)次數(shù)據(jù)包以及丟棄掉。
路由器檢查到達組播包的源地址,如果信息包是在可返回源站點的接口上到達,則RPF檢查成功,信息包被轉(zhuǎn)發(fā)如果RPF檢查失敗,丟棄信息包。
大家這個時候可能對這個有些概念,組播說的也很神乎,比如很省帶寬,畢竟是一發(fā)多,只需要發(fā)送一份,但是可以很多都接受。比單播好用多了,那你可能會問,那組播的應(yīng)用應(yīng)該很廣了?答案是肯定的,組播的應(yīng)用很多,比如多媒體會議,聯(lián)網(wǎng)游戲等。但是有個問題目前確實致命的!應(yīng)用的條件:路由器沒有開啟這個功能!
大致可以說一下組播路由的過程,數(shù)據(jù)包從源端口出發(fā),經(jīng)路由器轉(zhuǎn)發(fā)(這個應(yīng)該是所有路由器都會經(jīng)過,可是所有哦),然后到達有目的組播的成員則轉(zhuǎn)發(fā)給他。這個時候你可能會發(fā)現(xiàn)一個問題,如果組播大規(guī)模應(yīng)用的話,那網(wǎng)絡(luò)上這種數(shù)據(jù)包會非常多,畢竟誰都可以創(chuàng)建組播組,然后發(fā)送,路由器是要進行轉(zhuǎn)發(fā)的。
我自己本來也是想實現(xiàn)一個組播功能的類似于視頻會議的應(yīng)用的,但是測試的時候發(fā)現(xiàn),數(shù)據(jù)包就是沖不出去內(nèi)網(wǎng),只能在局域網(wǎng)內(nèi)轉(zhuǎn)(局域網(wǎng)內(nèi)可以收到)。后來發(fā)現(xiàn)是路由器雖然有這個功能,但是默認都給關(guān)閉了。記得當(dāng)時查這個資料的時候,在一個路由器管理員配置的一個BBS,上面一個人發(fā)帖,有人回答說:管理員如果開啟這個功能,那只能說有病。足可以看出,目前路由器對組播支持的尷尬處境。
所以這里只是提醒一下做這個的朋友,路由器對這個支持不是很好,如果要做的話多考慮一下??赡芪依斫獠粚Γ绻凶龀鰜磉@個的,希望能提供觀點哈。
附一個簡單的多播程序,同一子網(wǎng)下運行無誤,只需要打開這個客戶端就可以。兩個進程,一個負責(zé)發(fā),一個負責(zé)接收。
- #include <iostream>
- #include <winsock2.h> //注意這里的include文件順序
- #include <Ws2tcpip.h>
- #include <process.h> //_beginthread要求
- #pragma comment(lib, "ws2_32.lib")
- using namespace std;
- const char* MULTICAST_IP = "230.1.1.99"; //多播組地址
- const int MULTICAST_PORT = 2002; //多播組端口
- const int BUFFER_SIZE = 1024;
- void do_send(void* arg); //讀取用戶輸入并發(fā)送到多播組線程函數(shù)
- void do_read(void* arg); //讀物多播組數(shù)據(jù)函數(shù)
- int main()
- {
- //這個結(jié)構(gòu)被用來存儲被WSAStartup函數(shù)調(diào)用后返回的Windows Sockets數(shù)據(jù)。
- //它包含Winsock.dll執(zhí)行的數(shù)據(jù)。
- WSAData wsaData;
- /*
- 使用Socket的程序在使用Socket之前必須調(diào)用WSAStartup函數(shù)。該函數(shù)的第一個參數(shù)指明程序請求使用的Socket版本,其中高位字節(jié)指明副版本、低位字節(jié)指明主版本;操作系統(tǒng)利用第二個參數(shù)返回請求的Socket的版本信息。
- 加載Windows套接字動態(tài)鏈接庫
- */
- if( WSAStartup(MAKEWORD(2,2), &wsaData) != 0 )
- {
- cout <<"Error in WSAStartup"<<endl;
- return 0;
- }
- SOCKET server;
- //原始的方式
- /*
- 第一個參數(shù)指定應(yīng)用程序使用的通信協(xié)議的協(xié)議族,對于TCP/IP協(xié)議族,該參數(shù)置AF_INET;
- 第二個參數(shù)指定要創(chuàng)建的套接字類型,流套接字類型為SOCK_STREAM、數(shù)據(jù)報套接字類型為SOCK_DGRAM、
- 原始套接字SOCK_RAW(WinSock接口并不適用某種特定的協(xié)議去封裝它,而是由程序自行處理數(shù)據(jù)包以及協(xié)議首部);
- 另一種方式WSASocket
- */
- server = socket(AF_INET, SOCK_DGRAM, 0); //創(chuàng)建一個UDP套接口
- cout<<"create socket: "<<server<<endl;
- int ret ;
- const int on = 1; //允許程序的多個實例運行在同一臺機器上
- /*
- 調(diào)用setsockopt()函數(shù)為套接字設(shè)置SO_REUSEADDR選項,以允許套接字綁扎到一個已在使用的地址上。設(shè)置套接字的選項
- */
- ret = setsockopt(server, SOL_SOCKET, SO_REUSEADDR, (char *)&on, sizeof(on));
- if( ret == SOCKET_ERROR )
- {
- WSACleanup();
- cout<<"Error in setsockopt(SO_REUSEADDR): "<<WSAGetLastError()<<endl;
- return 0;
- }
- const int routenum = 10;
- //ret = setsockopt(server,IPPROTO_IP,IP_MULTICAST_TTL,\
- ret = setsockopt(server,IPPROTO_IP,IP_MULTICAST_TTL,\
- (char*)&routenum,sizeof(routenum));
- if( ret == SOCKET_ERROR )
- {
- WSACleanup();
- cout<<"Error in setsockopt(IP_MULTICAST_TTL): "<<WSAGetLastError()<<endl;
- return 0;
- }
- const int loopback = 0; //禁止回饋
- //使組播報文環(huán)路有效或無效
- ret = setsockopt(server,IPPROTO_IP,IP_MULTICAST_LOOP,\
- (char*)&loopback,sizeof(loopback));
- if( ret == SOCKET_ERROR )
- {
- WSACleanup();
- cout<<"Error in setsockopt(IP_MULTICAST_LOOP): "<<WSAGetLastError()<<endl;
- return 0;
- }
- //地址信息,local設(shè)置為多播組端口
- sockaddr_in local;
- memset(&local, 0, sizeof(local));
- local.sin_family = AF_INET;
- local.sin_port = htons(MULTICAST_PORT);
- //INADDR_ANY為0.0.0.0
- local.sin_addr.S_un.S_addr = INADDR_ANY;
- ret = bind(server, (sockaddr*)(&local), sizeof(local));
- if( ret == SOCKET_ERROR )
- {
- WSACleanup();
- cout<<"Error in bind: "<<WSAGetLastError()<<endl;
- return 0;
- }
- //多播組結(jié)構(gòu)
- ip_mreq mreq;
- memset(&mreq, 0, sizeof(mreq));
- //本機地址
- mreq.imr_interface.S_un.S_addr = INADDR_ANY;
- //點分十進制地址轉(zhuǎn)化為IP地址
- mreq.imr_multiaddr.S_un.S_addr = inet_addr(MULTICAST_IP);
- //加入一個多播組
- ret = setsockopt(server,IPPROTO_IP,IP_ADD_MEMBERSHIP,\
- (char*)&mreq,sizeof(mreq));
- if( ret == SOCKET_ERROR )
- {
- WSACleanup();
- cout<<"Error in setsockopt(IP_ADD_MEMBERSHIP): "<<WSAGetLastError()<<endl;
- return 0;
- }
- //創(chuàng)建了兩個線程,一個讀用戶輸入并發(fā)送,一個讀多播組數(shù)據(jù)
- HANDLE hHandle[2];
- hHandle[0] = (HANDLE)_beginthread(do_send,0,(void*)server);
- hHandle[1] = (HANDLE)_beginthread(do_read,0,(void*)server);
- //如果用戶輸入結(jié)束,程序就終止了
- WaitForSingleObject(hHandle[0], INFINITE);
- WSACleanup();
- return 0;
- }
- void do_send(void* arg)
- {
- SOCKET server = (SOCKET)arg;
- char sendline[BUFFER_SIZE+1];
- sockaddr_in remote;
- memset(&remote, 0, sizeof(remote));
- remote.sin_addr.s_addr = inet_addr ( MULTICAST_IP );
- remote.sin_family = AF_INET ;
- remote.sin_port = htons(MULTICAST_PORT);
- for(;;) //讀取用戶輸入知道用戶輸入"end"
- {
- cin.getline(sendline, BUFFER_SIZE);
- if(strncmp(sendline,"end",3)==0)
- break;
- //發(fā)送用戶輸入的數(shù)據(jù)到多播組
- sendto(server, sendline, strlen(sendline), 0, (sockaddr*)(&remote), sizeof(remote));
- }
- cout<<"do_send end..."<<endl;
- }
- void do_read(void* arg)
- {
- SOCKET server = (SOCKET)arg;
- char buf[BUFFER_SIZE+1];
- int ret;
- sockaddr_in client;
- int clientLen;
- for(;;) //一直讀取知道主線程終止
- {
- clientLen = sizeof(client);
- memset(&client, 0, clientLen);
- ret = recvfrom(server, buf, BUFFER_SIZE, 0, (sockaddr*)(&clientLen), &clientLen);
- if ( ret == 0) //do_read在用戶直接回車發(fā)送了一個空字符串
- {
- continue;
- }
- else if( ret == SOCKET_ERROR )
- {
- if( WSAGetLastError() == WSAEINTR ) //主線程終止recvfrom返回的錯
- break;
- cout<<"Error in recvfrom: "<<WSAGetLastError()<<endl;
- break ;
- }
- buf[ret] = '\0';
- cout<<"received: "<<buf<<endl;
- }
- cout<<"do_read end..."<<endl;
- }