zoukankan      html  css  js  c++  java
  • TCP/IP网络编程之多播与广播

    多播

    多播方式的数据传输是基于UDP完成的,因此,与UDP服务端/客户端的实现非常接近。区别在于,UDP数据传输以单一目标进行,而多播数据同时传递到加入(注册)特定组的大量主机。换言之,采用多播方式时,可以同时向多个主机传递数据

    多播的数据传输方式及流量方面的优点:

    • 多播服务端针对特定多播组,只发送一次数据
    • 即使只发送一次数据,但该组内的所有客户端都会接收数据
    • 多播组数可在IP地址范围内任意增加
    • 加入特定组即可接收发往该多播组的数据

    多播组是D类IP(224.0.0.0~239.255.255.255),“加入多播组”可以理解为通过程序完成如:“在D类IP地址中中,我希望接收发往目标239.234.218.234的多播数据”。多播是基于UDP完成的,也就是说,多播数据包的格式与UDP数据包相同。只是与一般的UDP数据包不同,向网络传递一个多播数据包时,路由器将复制该数据包并传递到多个主机。像这种,多播需要借助路由器完成。如图1-1所示

    图1-1   多播路由

    图1-1表示传输至AAA组的多播数据包借助路由器传递到加入AAA组的所有主机的过程,可能有人认为这种方式不利于网络流量,但像这样向大量客户端发送数据时,也会对服务端和网络流量产生负面影响,所以可以借助多播技术解决该问题。只看图1-1,各位会认为不利于网络流量,因为路由器频繁复制同一数据包。但请从另一方面考虑,这样做至少不会向同一区域发送多个相同数据包。

    若通过TCP或UDP向1000个主机发送文件,则共需要传递1000次。即便将10台主机合为一个网络,使99%的传输路径相同的情况下也是如此。但此时若使用多播方式传输文件,则只需发送一次,这时由1000台主机构成的网络中的路由器负责复制文件并传递到主机。就因为这种特性,多播主要用于“多媒体数据的实时传输”

    另外,虽然理论上可以完成多播通信,但不少路由器并不支持多播,或即便支持也因网络拥堵问题故意阻断多播。因此,为了在不支持多播的路由器完成多播通信,也会使用隧道技术(非多播程序员需考虑的问题)。我们只讨论支持多播服务的环境下的编程方法

    路由(Routing)和TTL(Time to Live,生存时间),以及加入组的方法

    接下来讨论多播相关编程,为了传递多播数据包,必须设置TTL,TTL是Time to Live的简写,是决定“数据包传递距离”的主要因素。TTL用整数表示,并且每经过一个路由器就减1。当TTL变为0时,该数据包无法再被传递,只能销毁。因此,TTL的值设置过大将影响网络流量。当然,设置过小也会无法传递到目标主机

    图1-2   TTL和多播路由

    接下来给出TTL设置方法,程序中TTL设置是通过套接字可选项(TCP/IP网络编程之套接字的多种可选项)完成的。与设置TTL相关的协议层为IPPROTO_TCP ,选项名为IP_MULTICAST_TTL。因此,可以用如下代码把TTL设置为64 :

    int send_sock;
    int time_live=64;
    ……
    send_sock=socket(PF_INET, SOCK_DGRAM, 0);
    setsockopt(send_sock, IPPROTO_IP, IP_MULTICAST_TTL, (void*) &time_live, sizeof(time_live));
    ……
    

      

    另外,加入多播组也通过设置套接字选项完成,加入多播组相关的协议层为IPPROTO_IP,选项名为IP_ADD_MEMBERSHIP。可通过如下代码加入多播组:

    int recv_sock;
    struct ip_mreq join_adr;
    ……
    recv_sock=socket(PF_INET, SOCK_DGRAM, 0);
    ……
    join_adr.imr_multiaddr.s_addr="多播组地址信息";
    join_adr.imr_interface.s_addr="加入多播组的主机地址信息";
    setsockopt(recv_sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, (void*) &join_adr, sizeof(join_adr));
    ……
    

      

    上述代码只给出了与setsockopt函数相关的部分,详细内容会在后面给出。此处之讲解ip_mreq结构体,该结构体定义如下:

    struct ip_mreq 
    { 
    	struct in_addr imr_multiaddr; //多播组的IP地址 
    	struct in_addr imr_interface; //加入的客服端主机IP地址 
    }
    

      

    之前我们介绍过in_addr结构体,因此这里只介绍ip_mreq的结构体成员。第一个成员imr_multiaddr中写入加入的组IP地址,第二个成员imr_interface是加入该组的套接字所属主机的IP地址,也可用INADDR_ANY

    实现多播Sender和Receiver

    多播中用“发送者”(以下称为Sender)和“接受者”(以下称为Receiver)替代服务端和客户端,顾名思义,此处的Sender是多播数据的发送主体,Receiver是需要多播组加入过程的数据接收主体。下面讨论即将给出的示例,该示例的运行场景如下:

    • Sender:向AAA组广播文件中保存的新闻信息
    • Receiver:接收传递到AAA组的新闻信息

    接下来只给出Sender代码,Sender比Receiver简单,因为Receiver需要经过加入组的过程,而Sender只需创建UDP套接字,并向多播地址发送数据

    news_sender.c 

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <arpa/inet.h>
    #include <sys/socket.h>
    
    #define TTL 64
    #define BUF_SIZE 30
    void error_handling(char *message);
    
    int main(int argc, char *argv[])
    {
        int send_sock;
        struct sockaddr_in mul_adr;
        int time_live = TTL;
        FILE *fp;
        char buf[BUF_SIZE];
        if (argc != 3) {
            printf("Usage : %s <GroupIP> <PORT>
    ", argv[0]);
            exit(1);
        }
    
        send_sock = socket(PF_INET, SOCK_DGRAM, 0);
        memset(&mul_adr, 0, sizeof(mul_adr));
        mul_adr.sin_family = AF_INET;
        mul_adr.sin_addr.s_addr = inet_addr(argv[1]); // Multicast IP
        mul_adr.sin_port = htons(atoi(argv[2]));      // Multicast Port
    
        setsockopt(send_sock, IPPROTO_IP,
                   IP_MULTICAST_TTL, (void *)&time_live, sizeof(time_live));
        if ((fp = fopen("news.txt", "r")) == NULL)
            error_handling("fopen() error");
    
        while (!feof(fp)) /* Broadcasting */
        {
            fgets(buf, BUF_SIZE, fp);
            sendto(send_sock, buf, strlen(buf),
                   0, (struct sockaddr *)&mul_adr, sizeof(mul_adr));
            sleep(2);
        }
        fclose(fp);
        close(send_sock);
        return 0;
    }
    
    void error_handling(char *message)
    {
        fputs(message, stderr);
        fputc('
    ', stderr);
        exit(1);
    }
    

      

    •  第24行:多播数据通信是通过UDP完成的,因此创建UDP套接字
    • 第26~28行:设置传输数据的目标地址信息,重要的是,必须将IP地址设置为多播地址
    • 第30行:指定套接字TTL信息,这是Sender中的必要过程
    • 第35~41行:实际传输数据的区域,基于UDP套接字传输数据,因此需要利用sendto函数。另外,第40行的sleep函数调用主要是为了给传输数据提供一定的时间间隔而添加的,没有其他特殊意义

    从上述代码中可以看到,Sender与普通的UDP套接字程序相比差别不大。但多播Receiver则有些不同,为了接收传向任意多播地址的数据,需要经过加入多播组的过程。除此之外,Receiver同样与UDP套接字程序差不多,接下来给出上述示例结合使用的Receiver程序

    news_receiver.c 

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <arpa/inet.h>
    #include <sys/socket.h>
    
    #define BUF_SIZE 30
    void error_handling(char *message);
    
    int main(int argc, char *argv[])
    {
        int recv_sock;
        int str_len;
        char buf[BUF_SIZE];
        struct sockaddr_in adr;
        struct ip_mreq join_adr;
    
        if (argc != 3) {
            printf("Usage : %s <GroupIP> <PORT>
    ", argv[0]);
            exit(1);
        }
    
        recv_sock = socket(PF_INET, SOCK_DGRAM, 0);
        memset(&adr, 0, sizeof(adr));
        adr.sin_family = AF_INET;
        adr.sin_addr.s_addr = htonl(INADDR_ANY);
        adr.sin_port = htons(atoi(argv[2]));
    
        if (bind(recv_sock, (struct sockaddr *)&adr, sizeof(adr)) == -1)
            error_handling("bind() error");
    
        join_adr.imr_multiaddr.s_addr = inet_addr(argv[1]);
        join_adr.imr_interface.s_addr = htonl(INADDR_ANY);
    
        setsockopt(recv_sock, IPPROTO_IP,
                   IP_ADD_MEMBERSHIP, (void *)&join_adr, sizeof(join_adr));
    
        while (1)
        {
            str_len = recvfrom(recv_sock, buf, BUF_SIZE - 1, 0, NULL, 0);
            if (str_len < 0)
                break;
            buf[str_len] = 0;
            fputs(buf, stdout);
        }
        close(recv_sock);
        return 0;
    }
    
    void error_handling(char *message)
    {
        fputs(message, stderr);
        fputc('
    ', stderr);
        exit(1);
    }
    

      

    •  第33、34行:初始化结构体ip_mreg变量,第26行初始化多播组地址,第27行初始化待加入组的主机IP地址
    • 第36行:利用套接字选项IP_ADD_MEMBERSHIP加入多播组,至此完成了接收第33行指定的多播组数据的所有准备
    • 第41行:通过调用recvfrom函数接收多播数据,如果不需要知道传输数据的主机地址信息,可以向recvfrom函数的第五个和第六个参数分别传递NULL和0

    创建一个news.txt

    # cat news.txt 
    Hello world!
    

        

    编译news_receiver.c并运行

    # gcc news_receiver.c -o news_receiver
    # ./news_receiver  224.1.1.2 8500
    Hello world!
    Hello world!
    

      

    编译news_sender.c 并运行 

    # gcc news_sender.c -o news_sender
    # ./news_sender 224.1.1.2 8500
    

      

    Sender和Receiver之间的端口应保持一致,虽然未讲,但理所应当。运行顺序并不重要,因为不像TCP套接字在连接状态下收发数据。只是因为多播属于广告的范畴,如果延迟运行Receiver,则无法接收之前传输的多播数据

    广播

    本节介绍的广告在“一次性向多个主机发送数据”这一点上与多播类似,但传输数据的范围有区别。多播即使在跨越不同网络的情况下,只要加入多播组就能接收数据。相反,广播只能向同一网络中的主机传输数据。广播是向同一网络中的所有主机传输数据的方法,广播是基于UDP完成的,这一点与多播相同。根据传输数据时使用的IP地址的形式,广播分为以下两种:

    • 直接广播
    • 本地广播

    二者在代码实现上的差别主要在IP地址,直接广播的IP地址中除了网络之外,其余主机地址全部设置为1。例如,希望向网络地址192.12.34中的所有主机传输数据时,可以向192.12.34.255传输。换言之,可以采用直接广播的方式向特定区域内所有主机传输数据

    反之,本地广播中使用的IP地址限定为255.255.255.255。例如,192.32.24网络中的主机向255.255.255.255传输数据时,数据将传递到192.32.24网络中的所有主机

    那么,应当如何实现Sender和Receiver呢?实际上,如果不仔细观察广播示例中通信时使用的IP地址,则很难与UDP示例进行区分。也就是说,数据通信中使用的IP地址是与UDP示例的唯一区别。默认生成的套接字会阻止广播,因此,只需要通过如下代码更改默认设置

    int send_sock;
    int bcast = 1;
    ……
    send_sock = socket(PF_INET, SOCK_DGRAM, 0);
    ……
    setsockopt(send_sock, SOL_SOCKET, SO_BROADCAST, (void*) &bcast, sizeof(bcast));
    ……
    

      

    调用setsockopt函数,将SO_BROADCAST选项设置为bcast变量中的值1。这意味着可以进行数据广播,当然,上述套接字选项只需在Sender中更改,Receiver的实现不需要该过程

    实现广播数据的Sender和Receiver

    下面是基于广播的Sender和Receiver,为了与多播示例进行对比,将之前的news_sender.c和news_receiver.c改为广播的示例

    news_sender_brd.c 

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <arpa/inet.h>
    #include <sys/socket.h>
    
    #define BUF_SIZE 30
    void error_handling(char *message);
    
    int main(int argc, char *argv[])
    {
        int send_sock;
        struct sockaddr_in broad_adr;
        FILE *fp;
        char buf[BUF_SIZE];
        int so_brd = 1;
        if (argc != 3) {
            printf("Usage : %s <Boradcast IP> <PORT>
    ", argv[0]);
            exit(1);
        }
    
        send_sock = socket(PF_INET, SOCK_DGRAM, 0);
        memset(&broad_adr, 0, sizeof(broad_adr));
        broad_adr.sin_family = AF_INET;
        broad_adr.sin_addr.s_addr = inet_addr(argv[1]);
        broad_adr.sin_port = htons(atoi(argv[2]));
    
        setsockopt(send_sock, SOL_SOCKET,
                   SO_BROADCAST, (void *)&so_brd, sizeof(so_brd));
        if ((fp = fopen("news.txt", "r")) == NULL)
            error_handling("fopen() error");
    
        while (!feof(fp))
        {
            fgets(buf, BUF_SIZE, fp);
            sendto(send_sock, buf, strlen(buf),
                   0, (struct sockaddr *)&broad_adr, sizeof(broad_adr));
            sleep(2);
        }
        close(send_sock);
        return 0;
    }
    
    void error_handling(char *message)
    {
        fputs(message, stderr);
        fputc('
    ', stderr);
        exit(1);
    }
    

      

    第29行更改第23行创建的UDP套接字的可选项,使其能够发送广播数据,其余部分与UDP Sender一致,下面给出广播Receiver 

    news_receiver_brd.c

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <arpa/inet.h>
    #include <sys/socket.h>
    
    #define BUF_SIZE 30
    void error_handling(char *message);
    
    int main(int argc, char *argv[])
    {
        int recv_sock;
        struct sockaddr_in adr;
        int str_len;
        char buf[BUF_SIZE];
    
        if (argc != 2) {
            printf("Usage : %s  <PORT>
    ", argv[0]);
            exit(1);
        }
    
        recv_sock = socket(PF_INET, SOCK_DGRAM, 0);
    
        memset(&adr, 0, sizeof(adr));
        adr.sin_family = AF_INET;
        adr.sin_addr.s_addr = htonl(INADDR_ANY);
        adr.sin_port = htons(atoi(argv[1]));
    
        if (bind(recv_sock, (struct sockaddr *)&adr, sizeof(adr)) == -1)
            error_handling("bind() error");
    
        while (1)
        {
            str_len = recvfrom(recv_sock, buf, BUF_SIZE - 1, 0, NULL, 0);
            if (str_len < 0)
                break;
            buf[str_len] = 0;
            fputs(buf, stdout);
        }
    
        close(recv_sock);
        return 0;
    }
    
    void error_handling(char *message)
    {
        fputs(message, stderr);
        fputc('
    ', stderr);
        exit(1);
    }
    

      

    编译news_receiver_brd.c并运行

    # gcc news_receiver_brd.c -o news_receiver_brd
    # ./news_receiver_brd 8500
    Hello world!
    Hello world!
    

      

    编译news_sender_brd.c 并运行

    # gcc news_sender_brd.c -o news_sender_brd
    # ./news_sender_brd 255.255.255.255 8500
    

      

  • 相关阅读:
    linux下安装rpc.rstatd
    myeclipse下编译jmeter2.4
    2010我最喜爱的耳机评选结果q
    HTTP/1.1 Range和ContentRange
    top命令的load average是什么意思?
    用户 'sa' 登录失败。该用户与可信 SQL Server 连接无关联。
    自定义ListBox,实现单多选切换(复选框)
    自定义水印输入框和密码框
    获取Windows Phone设备信息
    启动器和选择器学习(7)选择器之联系人信息保存
  • 原文地址:https://www.cnblogs.com/beiluowuzheng/p/9693677.html
Copyright © 2011-2022 走看看