1. 程式人生 > 實用技巧 >廣播和ip多播

廣播和ip多播

套接字選項和I/O控制命令

  • 套接字建立之後,可以使用套接字選項和ioctl命令操作他的屬性,以改變套接字的預設行為。有些套接字選項僅僅是返回資訊,有些選項可以影響套接字的行為。I/O控制命令縮寫為ioctl,他也影響套接字的行為。

套接字選項

  • 選項影響套接字的操作,如封包路由和OOB資料傳輸,獲取和設定套接字選項的函式分別是getsockopt和setsockopt,他們的用法如下。
int getsockopt(
SOCKET s,//套接字控制代碼
int level,//指定此選項被定義在哪個級別,如SIL_SOCKET、IPPROTO_TCP、IPPROTO_IP等
int optname,//套接字選項名稱,如SO_ACCEPTCONN
char* optval,//指定一個緩衝區,所請求的選項的值將會被返回到這裡
int* optlen//指定上面緩衝區大小,返回所需大小
);//函式調用出錯返回SOCKET_ERROR
  • 協議是分層的,每層又有多個協議,這就造成了選項有不同的級別(level),最高層的是應用層,套接字就工作在這一層,這一層屬性對應著SOL_SOCKET級別,在下一層是傳輸層有TCP和UDP協議,分別對應IPPROTO_TCP、IPPROTO_UDP級別,在下面是網路層有IP協議,對應著IPPROTO_IP級別。各級別的屬性不同,同一級別不同屬性也可能不同,所以一定要指定恰當的level引數。
  • 比如,阻塞模式下呼叫recbform在指定埠接收網路封包時,如果過一段時間封包還達不到recvform能夠超時返回,而不是永遠等待下去,僅需要設定套接字選項即可,如下所示,其中nTine是要等待的時間
BOOL SetTimeout(SOCKET s,int nTime,BOOL bRecv)//自定義設定套接字超時值的函式
{
int ret=::setsockopt(s,SOL_SOCKET,bRecv?SO_REVTIMEO:SO_SNDTIMEO,(char*)&nTime,sizeof(nTime));
return ret!=SOCKET_ERROR;
}

SOL_SOCKET級別

  • SO_ACCEPTCONN:BOOL型別,檢查套接字是否進入監聽模式,如果套接字已進入此選項返回TRUE。SOCK_DGRAM型別的套接字不支援此選項
  • SO_BROADCAST:BOOL型別,設定套接字傳輸和接收廣播訊息,如果給定套接字已經被設定為接收或傳送廣播資料,查詢此套接字選項將返回TRUE,此選項對不是SOCK_STREAM型別的套接字有效。
  • SO_CONNECT_TIME:int型別,這是一個僅Microsoft相關選項,他返回連線已建立的時間,它可以在客戶端套接字控制代碼上呼叫,確定是否有連線,連線已建立多長時間,沒有連線返回值為0Xffffffff。
  • SO_DONTROUTE:BOOL型別,SO_DONTROUTE選項告訴下層網路堆疊忽略路由表,直接傳送資料到此套接字繫結的介面。
  • SO_REUSEADDR:BOOL型別,如果值為TRUE,套接字可以被繫結到一個已經被另一個套接字使用的本地地址,或者是繫結到一個處於TIME_WAIT狀態的地址
  • SO_EXCLUSIVEADDRUSE:BOOL型別,如果值為TRUE,套接字繫結到的本地埠就不能被其他程序重用。這個選項是SO_REUSEADDR的補充,阻止其他程序在你的應用程式使用的地址上使用SO_REUSEADDR
  • SO_RCVBUF和SO_SNDTIMEO:int型別,獲取或者設定套接字內部為接收(傳送)操作分配緩衝區的大小,套接字床建立時,會被分配一個接收緩衝區和傳送緩衝區
  • SO_RCVTIMEO和SO_SNDTIMEO:int型別,獲取或設定套接字上接收(傳送資料的超時)

IPPROTO_IP級別

  • 在IPPROTO_IP級別上的套接字選項與IP協議屬性相關,如修改IP頭的特定域,新增一個套接字到IP多播組等。
  • IP_OPTIONS:char型別,獲取設定IP頭中的IP選項,這個標識允許你設定IP頭中的IP選項域
  • IP_HDRINCL:BOOL型別,如果值為TRUE,IP頭和資料會一塊提交給Winsock傳送呼叫。置IP_HDRINCL為TRUE導致傳送函式在資料前包含ip頭
  • IP_TTL:int型別,設定和獲取IP頭中的TTL引數,資料報設定TTL限制它能夠經過路由器的數量

IOCTL

  • 用來控制套接字上I/O行為,也可以用來獲取套接字上未決的I/O資訊,向套接字上傳送ioctl命令的函式有兩個,一個是Winsock1的ioctlsocket,另一個是Winsock2的WSAIoctl。
int ioctlsocket(
SOCKET s,//套接字控制代碼
long cmd,//在套接字上要執行的命令
u_long* argp//指向cmd的引數
)
  • WSAsock2新引進的ioctl函式WSAIoctl添加了一些新的選項
int WSAIoctl(
SOCKET s,//套接字控制代碼
DWORD dwIoControlCode,//在套接字上要執行的命令
LPVOID lpvInBuffer,//指向輸入緩衝區
DWORD cbInBuffer,//輸入緩衝區大小
LPVOID lpvOutBuffer,//指向輸出緩衝區
DWORD cbOutBuffer,//輸出緩衝區的大小
LPDWORD lpcbBytesReturned,//用來返回實際返回的位元組數
LPWSAOVERLAPPED lpOverlapped,//指向一個WSAOVERLAPPED結構
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine//指向自定義完成例程
)
  • FIONBIO:將套接字置於非阻塞模式,這個命令啟動或者關閉套接字s上的非阻塞模式。預設情況下,所有套接字在建立時都處於阻塞模式,如果要開啟非阻塞模式,呼叫I/O控制函式時設定argp設定為非0,如果要關閉非阻塞模式,設定argp為0.
    • WSAAsyncSelect或者WSAEventSelect函式自動設定套接字為非阻塞模式,任何試圖將套接字設定為阻塞模式的呼叫都將以WSAEINVAL錯誤失敗,為了將套接字設定為阻塞模式,應用程式首先讓IEVENT引數等於0呼叫WSAAsyncSelect無效或者通過使NetworkEvents引數等於0呼叫WSAEventSelect無效。
  • FIONREAD:返回在套接字上要讀的資料的大小。
  • SIO_GET_EXTENSION_FUNCTION_POINTER:取得與特定下層提供者相關的函式指標。
  • SIO_RCVALL:接收網路上所有的封包,,套接字必須繫結要一個明確的介面不能繫結到INADDR_ANY。一旦套接字被繫結,這個ioctl被設定,對recv/WSARecv的呼叫將返回IP資料報。
//設定SIO_RCVALL控制碼,以便接收所有IP包
DWORD dwValue=1;
if(ioctlsocket(sRaw,SIO_RCBALL,&dwValue)!=0)
return;

廣播通訊

  • 利用廣播可以傳送給本地子網上的每個機器,為了進行廣播通訊,必須開啟廣播選項S0_BROADCAST,然後使用recvform、sendto等函式收發廣播資料
  • 對於UDP,存在一個特定的廣播地址255.255.255.255,廣播資料都應該傳送到這裡。
  • 傳送放程式建立套接字後使用setsockopt函式開啟SO_BROADCAST選項,然後設定廣播地址向4567不斷髮送廣播資料
SOCKET s=::socket(AF_INET,SOCK_DGRAM,0)
BOOL bBroadcast=TRUE;
::setsockopt(s,SOL_SOCKET,SO_BROADCAST,(char*)&bBroadcast,sizeof(BOOL));
SOCKADDR_IN bcast;
bcast.sin_family=AF_INET;
bcast.sin_addr.s_addr=INVADDR_BROADCASTl
bcast.sin_port=htons(4567);
char sz[]="This is just a test\r\n";
while(TRUE){
::sendto(s,sz,strlen(sz),0,(sockaddr*)&bcast,sizeof(bcast));
::Sleep(5000);
}
  • 可以將廣播通訊的埠看作電臺的頻率,廣播程式不斷向埠號傳送資料,電臺播放節目一樣,呼叫recvform函式即可接收到廣播資料這和其他UDP程式沒有什麼不同。
SOCKET s=::socket(AF_INET,SOCK_DGRAM,0)
SOCKADDR_IN sin;
sin.sin_famoly=AF_INET;
sin.sin_addr.S_un.S_addr=INADDR_ANY;
sin.sin_port=::ntohs(4567);
if(::bind(s,(sockaddr*)&sin,sizeof(sin))==SOCKET_ERROR){
print("bind() failed\n");
return ;
}
SOCKADDR_IN addrRemote;
int nLen=sizeof(addrRemote);
char sz[256];
while(TRUE){
int nRet=::recvfrom(s,sz,256,0,(sockaddr*)&addrRemote,&nLen);
if(nRet>0){
sz[nRet]='\0';
printf(sz);
}
}

IP多播

  • 使用廣播封包可以傳送到網路中的每個節點,多播封包僅被髮送到網路節點的一個集合。

多播地址

  • 為了傳送IP多播資料,傳送者需要確定一個合適的多播地址,這個地址代表一個組。IP多播採用D類地址確定多播的組。,地址範圍是224.0.0.0~239.255.255.255。不過有許多多播地址保留為特殊目的使用。
    地址|用途
    ---|:-------:
    224.0.0.0|基地址
    224.0.0.1|本子網上的所有節點
    224.0.0.2|本子網上的所有路由器
    224.0.0.4|網段中所有的DVMRP路由器
    224.0.0.5|所有的OSPE路由器
    224.0.0.6|所有的OSPE指派路由器
    224.0.0.9|所有的RIPv2路由器
    224.0.0.13|所有的PIM路由器

組管理協議

  • IGMP是IPV4引入的管理多播客戶,和他們之間關係的協議。為了多播能正常工作,兩個多播幾點之間所有的路由器必須支援IGMP協議。一旦路由器有一個或者多個客戶主機註冊的多播組,他就時不時的接收到加入命令時在內部記錄下所有主機地址傳送“組詢問”訊息。仍然存活了多播使用者,會用另一個訊息來響應,以便路由器支援需要繼續轉發與那個地址相關的資料,如果客戶主機不傳送響應,路由器就會認為該客戶離開了多播組,從此就不再為他轉發資料了。
  • 加入和離開多播組可以使用setsockopt函式,也可以使用WSAJoinLeaf函式。
加入和離開組
  • 有兩個套接字選項控制組的加入和離開:IP_ADD_MEMBERSHIP和IP_DROP_MEMBERSHIP,套接字選項級別分別是IPPROTO_IP,輸入引數是一個ip_mreq結構定義如下:
typedef struct{
struct in_addr IMR_MULTIADDRl//多播組的IP地址
struct in_addr IMR_INTERFACE;//將要加入或者離開多播組的本地地址
}ip_mreq;
  • 下面程式碼示例如何加入組,其中s是已經建立好的資料報套接字
ip_mreq mcast;
mcast.imr_interface.S_un.S_addr=INADDR_ANY;
mcast.imr_multiaddr.S_un.S_addr=::inet_addr("234.5.6.7");
int nRet=::setsockopt(s,IPPROTO_IP,IP_ADD_MEMBERSHIP,(char*)&mcast,sizeof(mcast));
  • 加入一個或者多個多播組之後,可以使用IP_DROP_MEMBERSHIP選項離開特定的組。
ip_merq mcast;
mcast.imr_interface.Sun.S_addr=dwInterFace;
mcast.imr_multiaddr.Sum.S_addr=dwMultiAddr;
int nRet=::setsockopt(s,IPPROTO_IP,IP_DROP_MEMBERSHIP,(char*)&mcast,sizeof(mcast));
  • 每個組關係和介面關聯,如果使用預設的介面,講imr_interface設為INADDR_ANY即可,也可指明本地地址。

接收多播資料

  • 主機在接收多播資料之前,必須成為ip多播組的成員。和單播封包一樣,到特定套接字的多播封包的傳送也是基於目的埠號的。為了接收發送到特定埠的多播封包,有必要繫結到那個本地埠,而不是顯示的指定本地地址。
  • 如果繫結套接字設定了SO_REUSEADDR選項,就有不止一個程序可以繫結到UDP埠。
BOOL bReuse=TRUE;
::setsockopt(s,SOL_SOCKET,SO_REUSEADDR,(char*)&bReuse,sizeof(BOOL));
  • 如此一來。每個來到這個共享埠的多播或廣播UDP封包都會被髮送給所有繫結到此埠的套接字。由於向前相容的原因,這並不包括單播封包-單播封包永遠不會發送到多個套接字。
  • 繫結到本地埠4567之後,便加入多播組234.5.6.7,迴圈呼叫recvfrom函式接收發送到多播組中的資料

傳送多播資料

  • 要想組傳送資料,沒有必要加入那個組以234.5.6.7為目的地址,4567為目的埠呼叫sendto函式,即可向多播組傳送資料
  • 預設情況加發送的IP多播資料報的TTL等於1,這使得他們不能被髮出子網。套接字選項IP_MULTICAST_TTL用來設定多播資料報TTL的值(範圍0~255)
BOOL SetTTL(SOCKET s,int nTTL){//自定義設定多播資料TTL的函式
int nRet=::setsockopt(s,IPPROTO_IP,IP_MULTICAST_TTL,(char*)&nTTL,sizeof(nTTL));
return nRet!=SOCKET_ERROR;
}
  • TTL為0的多播組不會在任何子網上傳輸,但是如果傳送放屬於目的的組就能夠在本地傳輸
    • 初始TTL為0的多播封包被限制在一臺主機
    • 初始TTL為1的多播封包被限制在一個子網
    • 初始TTL為32的多播封包被限制在一個站點
    • 初始TTL為64的多播封包被限制在一個地區
    • 初始TTL為128的多播封包被限制在一個大陸
    • 初始TTL為255的多播封包沒有限制
  • 許多多播路由器拒絕轉發目的地址在224.0.0.0~224.0.0.255之間的任何多播資料報,不管他的TTL是多少,這個地址範圍是為路由器和其他底層拓撲協議或者維護協議預留的。
  • 每個多播傳輸僅從一個網路接口出發,即便是主機有多個剝奪介面。系統管理者在安裝過程中就指定了多播使用的預設介面,可以使用套接字選項IP_MULTICAST_IF改變預設的傳送資料的介面
struct in_addr addr;
setsockopt(sock,IPPROTO_IP,IP_MULTICAST_IF,&addr,sizeof(addr));
  • addr是本地對外介面,設定為INADDR_ANY可以恢復使用預設介面,IP_MULTICAST_IF可以設定多播迴環是否開啟,如果值為真傳送到多播地址的資料會回顯到套接字的接收緩衝區。預設情況下,當傳送IP多播資料時,如果傳送方也是多播組的一個成員。資料講回到傳送套接字。如果設定為FALSE,任何傳送的資料都不會被髮送回來。

帶源地址的IP多播

  • 帶源地址的IP多播允許加入組時候,指定要接收哪些成員的資料,這種情況下有兩種方式加入組。第一種是“包含”方式,為套接字指定N個有效原地址,套接字僅接收這些源地址的有效資料。另一種是“排除”,為套接字指定N個源地址,套接字接收來自這些源地址之外的資料。
  • 要使用“包含”方式加入多播組,應該使用套接字選項IP_ADDR_SOURCE_MEMBERSHIP和IP_DROP_SOURCE_MEMBERSHIP。第一步是新增一個或者多個源地址。這兩個套接字選項的輸入輸出引數都是一個ip_mreq_source
struct ip_mreq_source{
struct in_addr imr_multiaddr;//多播組的ip地址
struct in_addr imr_sourceaddr,//指定的源ip地址
struct in_addr imr_interface;//本地ip地址介面
}
  • imr_sourceaddr域指定了源ip地址,套接字接收來自此IP地址的資料,如果有多個有效的源地址,IP_ADD_SOURCE_MEMBERSHIP就應該被呼叫多次
SOCKET s=::socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);

//本地介面
SOCKETADDR_IN localif;
localif.sin_family=AF_INET;
localif.sin_port=HTONS(5150);
localif.SI_ADDR.S_ADDR=HTONL(inaddr_any);
::bind(s,(SOCKADDR*)&localif,sizeof(localif));

//設定ip_mreq_source 結構
struct ip_mreq_source mreqsrc;
mreqsrc.imr_interface.s_addr=inet_addr("192.168.0.46");

mreqsrc.imr_multiaddr.s_addr=inet_addr("234.5.6.7");

//新增源地址
mreqsrc.imr_sourceaddr.s_addr=inet_addr("218.12.255.113");
::setsockopt(s,IPPROTO_IP,IP_ADD_SOURCE_MEMBERSHIP,(char*)&mreqsrc.sizeof(mreqsrc));
mreqsrc.imr_sourceaddr.s_addr=inet_addr("218.12.174.222");
::setsockopt(s,IPPROTO_IP,IP_ADD_SOURCE_MEMBERSHIP,(char*)&mreqsrc,sizeof(mreqsrc));
  • 為了從包含集合中移除源地址,要使用IP_DROP_SOURCE_MEMBERSHIP選項為他轉遞多播組、本地介面和要移除的源地址
  • 為了加入多播組,同時排除一個或者多個源地址,加入組時使用IP_ADD_MEMBERSHIP選項。使用IP_ADD_MEMBERSHIP加入組等價“排除“方式加入,但是源地址也沒有被排除。加入組後可以使用IP_BLOCK_SOURCE選項指定要排除的源地址,輸入引數也是ip_mreq_source結構。
  • 如果應用程式想從之前排除的地址接收資料,可以通過IP_UNBLOCK_SOURCE選項從排除集合中移除此地址,輸入引數仍然是ip_mreq_source結構。
#include<WinSock2.h>
#include<stdio.h>


#define IP_ADD_MEMBERSHIP 12
typedef struct {

    struct in_addr imr_multiaddr;//多播組的ip地址
    struct in_addr imr_interface;//將要加入或者離開多播組的本地地址
}ip_mreq;

int main() {
    SOCKET s = ::socket(AF_INET, SOCK_DGRAM, 0);
    //允許其他程序使用繫結的地址
    BOOL bReuse = TRUE;
    ::setsockopt(s,SOL_SOCKET, SO_REUSEADDR, (char*)&bReuse, sizeof(BOOL));
    

    //繫結到4567埠
    sockaddr_in si;
    si.sin_family = AF_INET;
    si.sin_port = ::ntohs(4567);
    si.sin_addr.S_un.S_addr = INADDR_ANY;
    ::bind(s, (sockaddr*)&si, sizeof(si));

    //加入多播組
    ip_mreq mcast;
    mcast.imr_interface.S_un.S_addr = INADDR_ANY;
    mcast.imr_multiaddr.S_un.S_addr = ::inet_addr("234.5.6.7");//多播地址
    ::setsockopt(s, IPPROTO_IP, IP_ADD_MEMBERSHIP, (char*)&mcast, sizeof(mcast));

    //接收多播資料
    printf("開始接收多播組234.5.6.7上的資料");
    char buf[1280];
    int nAddrLen(sizeof(si));
    while (TRUE)
    {
        int nRet = ::recvfrom(s, buf, strlen(buf), 0, (sockaddr*)&si, &nAddrLen);
        if (nRet!=SOCKET_ERROR)
        {
            buf[nRet] = '\0';
            printf(buf);
        }
        else
        {
            int i = ::WSAGetLastError();
            break;
        }
    }
    return 0;
}