14.多播与广播
前言
假设大家经营网络电台,需要向用户发送多媒体信息。如果有1000名用户,则需要向1000名用户发送数据;如果有10000名用户,则需要向10000名用户发送数据。此时,如果基于TCP提供服务,则需要维护1000个或10000个套接字连接,即使用UDP套接字提供服务,也需要1000次或10000次数据传输。像这样,向大量客户端发送相同数据时,也会对服务器端和网络流量产生负面影响。可以使用多播技术解决该问题。
一、多播
多播(Multicast)方式的数据传输是基于UDP完成的。因此,与UDP服务器端/客户端的实现方式非常接近。区别在于,UDP数据传输以单一目标进行,而多播数据同时传递到加入(注册)特定组的大量主机。换言之,采用多播方式时,可以同时向多个主机传递数据。
1.多播的数据传输方式及流量方面的优点
多播的数据传输特点可整理如下:
■ 多播服务器端针对特定多播组,只发送1次数据。
■ 即使只发送1次数据,但该组内的所有客户端都会接收数据。
■ 多播组数可在IP地址范围内任意增加。
■ 加入特定组即可接收发往该多播组的数据。
多播组是D类IP地址(224.0.0.0 ~ 239.255.255.255),“加人多播组”可以理解为通过程序完成如下声明:
– 在D类IP地址中,我希望接收发往目标239.234.218.234的多播数据。–
多播是基于UDP完成的,也就是说,多播数据包的格式与UDP数据包相同。只是与一般的UDP数据包不同,向网络传递1个多播数据包时,路由器将复制该数据包并传递到多个主机。像这样,多播需要借助路由器完成。
上图表示传输至AAA组的多播数据包借助路由器传递到加入AAA组的所有主机的过程。
– “但这种方式不利于网络流量啊!”
我门在本章节开始部分讲过:
– “像这样,向大量客户端发送相同数据时,也会对服务器端和网络流量产生负面影响。可以使用多播技术解决该问题。”
只看图,大家也许会认为这不利于网络流量,因为路由器频繁复制同一数据包。但请从另一方面考虑。
– “不会向同一区域发送多个相同数据包!”
若通过TCP或UDP向1000个主机发送文件,则共需要传递1000次。即便将10台主机合为1个网络,使99%的传输路径相同的情况下也是如此。但此时若使用多播方式传输文件,则只需发送1次。这时由1000台主机构成的网络中的路由器负责复制文件并传递到主机。就因为这种特性,多播主要用于“多媒体数据的实时传输”。
另外,虽然理论上可以完成多播通信,但不少路由器并不支持多播,或即便支持也因网络拥堵问题故意阻断多播。因此,为了在不支持多播的路由器中完成多播通信,也会使用隧道(Tunneling)技术(这并非多播程序开发人员需要考虑的问题)。我们只讨论支持多播服务的环境
下的编程方法吧。
2.路由(Routing)和TTL(Time toLive,生存时间),以及加入组的方法
接下来讨论多播相关编程方法。为了传递多播数据包,必需设置TTL。TTL是TimetoLive的简写,是决定“数据包传递距离”的主要因素。TTL用整数表示,并且每经过1个路由器就减1。
TTL变为0时,该数据包无法再被传递,只能销毁。因此,TTL的值设置过大将影响网络流量。当然,设置过小也会无法传递到目标,需要引起注意。
接下来给出TTL设置方法。程序中的TTL设置是通过第9章的套接字可选项完成的。与设置TTL相关的协议层为IPPROTO_IP,选项名为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;struct in_addr imr_interface;
}
第3章节讲过in_addr结构体,因此只介绍结构体成员。首先,第一个成员imr_multiaddr中写人加人的组IP地址。第二个成员imrinterface是加人该组的套接字所属主机的IP地址,也可使用INADDR_ANY。
3.实现多播Sender和Receiver
多播中用“发送者”(以下称为Sender)和“接受者”(以下称为Receiver)替代服务器端和客户端。顾名思义,此处的Sender是多播数据的发送主体,Receiver是需要多播组加人过程的数据接收主体。下面讨论即将给出的示例,该示例的运行场景如下。
■ Sender:向AAA组广播(Broadcasting)文件中保存的新闻信息。
■ Receiver:接收传递到AAA组的新闻信息。
接下来给出Sender代码。Sender比Receiver简单,因为Receiver需要经过加人组的过程,而Sender只需创建UDP套接字,并向多播地址发送数据。
#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>In", 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 IPmul_adr.sin_port=htons(atoi(argv[2])); // Multicast Portsetsockopt(send_sock, IPPROTO_IP,IP_MULTICAST_TTL, (void*)&time_live,sizeof(time_live));if((fp=fopen("news.txt","r"))==NULL)error_handling("sopen() 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('\n', stderr);exit(1);
}
第24行:多播数据通信是通过UDP完成的,因此创建UDP套接字。
第26~28行:设置传输数据的目标地址信息。重要的是,必须将IP地址设置为多播地址。
第30行:指定套接字TTL信息,这是Sender中的必要过程。
第35~41行:实际传输数据的区域。基于UDP套接字传输数据,因此需要利用sendto函数。另外,第40行的sleep函数调用主要是为了给传输数据提供一定的时间间隔而添加的,没有其他特殊意义。
#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 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>\n", 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('\n', stderr);exit(1);
}
第33、34行:初始化结构体ip_mreg变量。第26行初始化多播组地址,第27行初始化待加入主机的IP地址。
第36行:利用套接字选项IP_ADD_MEMBERSHIP加入多播组。至此完成了接收第26行指定的多播组数据的所有准备。
第41行:通过调用recvfrom函数接收多播数据。如果不需要知道传输数据的主机地址信息,可以向recvfrom函数的第五个和第六个参数分别传递NULL和0。
大家位是否运行过该示例?Sender和Receiver之间的端口号应保持一致,虽然未讲,但理所应当。运行顺序并不重要,因为不像TCP套接字在连接状态下收发数据。只是因为多播属于广播的范畴,如果延迟运行Receiver,则无法接收之前传输的多播数据。
–多播是基于MBone这个虚拟网络工作的。或许大家对虚拟网络感到陌生,但可将其理解为“通过网络中的特殊协议工作的软件概念上的网络”。也就是说,MBone并非可以触及的物理网络。它是以物理网络为基础,通过软件方法实现的多播通信必备虚拟网络。用于多播通信的虚拟网络的研究目前仍在进行,这与多播应用程序的编写属于不同领域。–
二、广播
这次介绍的广播(Broadcast)在“一次性向多个主机发送数据”这一点上与多播类似,但传输数据的范围有区别。多播即使在跨越不同网络的情况下,只要加人多播组就能接收数据。相反,广播只能向同一网络中的主机传输数据。
1.广播的理解及实现方法
广播是向同一网络中的所有主机传输数据的方法。与多播相同,广播也是基于UDP完成的。
根据传输数据时使用的IP地址的形式,广播分为如下2种:
■ 直接广播(Directed Broadcast)
■ 本地广播(LocalBroadcast)
二者在代码实现上的差别主要在于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;
intbcast=1; //对变量进行初始化以将SO_BROADCAST选项信息改为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的实现不需要该过程。
2.实现广播数据的Sender和Receiver
下面实现基于广播的Sender和Receiver。为了与多播示例进行对比,将之前的news_sender.c和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 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>\n", 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('\n', stderr);exit(1);
}
第29行更改第23行创建的UDP套接字的可选项,使其能够发送广播数据,其余部分与UDPSender一致。接下来给出广播Receiver。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.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>\n", 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('\n',stderr);exit(1);
}
源代码中没有需要特别讲解的内容,有条件的话,希望大家进一步验证直接广播的运行结果。
总结
emmmm,貌似这章节的代码不能验证,大家应该都没有组网吧…