套接字选项和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
";
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
");
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]=' ';
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] = ' ';
printf(buf);
}
else
{
int i = ::WSAGetLastError();
break;
}
}
return 0;
}