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