廣播和ip多播
阿新 • • 發佈:2020-08-15
套接字選項和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;
}