• 广播和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
    ";
    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;
    }
    
  • 相关阅读:
    2022 flag 150篇文章 63 ES elastic search
    2022 flag 150编文章 005 java 网络编程
    2022 flag 150篇文章 91 分布式系统协调服务 分布式基础
    2022 flag 150篇文章 50 PMP
    2022 flag 150篇文章 61 消息队列 Kafka
    2022年1月最新最全的微前端框架调研
    sqlconnection,sqlcommand,sqldataadapter,sqldatareader,dataset的关系详解
    [日常] 解决docker拉取镜像速度慢的问题
    jupyter notebook下python2和python3共存(kali)
    Jboss弱密码及反序列化漏洞
  • 原文地址:https://www.cnblogs.com/binarysystemloophole/p/13508339.html
Copyright © 2020-2023  润新知