引言
假设我们经营一家网络电台,需要向订阅用户发送多媒体信息。如果有 1000 名用户,则需要向 1000 名用户发送数据;如果有10000名用户,则需要向10000名用户发送数据。此时,如果基于 TCP 提供服务,则需要维护1000个或10000个套接字连接,即使使用UDP套接字提供服务,也需要1000次或10000次数据传输。像这样,向大量客户端发送相同数据时,也会对服务器端和网络流量产生负面影响。该如何解决这一问题呢?
这时就可以考虑使用多播的数据传输方式解决该问题。
一 多播
多播(Multicast,也被译为组播),它是一种一对多的通信方式。与单播相比,多播可以大大节约网络资源。
多播(Multicast)方式的数据传输时基于UDP协议完成的。因此,与UDP服务器端/客户端的实现方式非常接近。区别在于,UDP数据传输以单一目标进行,而多播数据同时传递到加入(注册)特定组(称为多播组)的大量主机。换言之,采用多播方式时,可以同时向多个主机传递数据。
1.1 多播的数据传输方式及流量方面的特点
- 多播的数据传输特点
- 多播服务器端针对特定多播组,只发送一次数据。一个多播组里面有多个主机。
- 即使只发送一次数据,但该组内的所有客户端都能接收到数据。
- 多播组数可在IP地址范围内任意增加。
- 加入特定多播组即可接收发往该多播组的数据。
多播组是D类IP地址(224.0.0.0 ~ 239.255.255.255),“加入多播组” 可以理解为通过程序完成如下声明:
“在D类IP地址中,我希望接收发往目标 239.234.218.234 的多播数据。”
多播是基于UDP协议完成的,也就是说,多播数据包的格式与UDP数据包相同。只是与一般UDP数据包不同,向网络传递一个多播数据包时,路由器将复制该数据包并传递到多个主机。多播需要借助路由器完成,如下图 1 所示。

上图1表示传输至AAA组的多播数据包借助路由器传递到加入AAA组的所有主机的过程。
“但这种方式不利于网络流量啊!”
在本文引言部分讲过:“像这样,向大量客户端发送相同数据时,也会对服务器端和网络流量产生负面影响。可以使用多播技术解决该问题。”
只看上图1,各位也许会认为则不利于网络流浪,因为路由器频繁复制同一数据包。但请从另一方面考虑:
“不会向同一区域发送多个相同数据包。”
《知识补充》多播路由器
在互联网范围内的多播传输要靠路由器来实现,这些路由器必须增加一些能够识别多播数据报的软件。能够运行多播协议的路由器称为多播路由器(Multicast router)。多播路由器当然也可以转发普通的单播 IP 数据报。
若通过 TCP 和 UDP 向1000个主机发送文件,则共需要传递1000次。即便将10台主机合为一个网络,使99%的传输路径相同的情况下也是如此。但此时若使用多播方式传输文件,则只需发送一次。这时由1000台主机构成的网络中的路由器负责复制文件并传递到主机。就因为这种特性,多播主要用于 “多媒体数据的实时传输”。
另外,虽然理论上可以完成多播通信,但不少路由器并不支持多播,或即便支持也因网络拥堵问题故意阻断多播。因此,为了在不支持多播的路由器中完成多播通信,也会使用隧道(Tunneling)技术(这并非多播程序开发人员需要考虑的问题)。我们只讨论在支持多播通信的环境下的网络编程方法。
【多播相关博文参考链接】
1.2 路由(Routing)和 TTL(Time toLive,生存时间)以及加入组的方法
接下来讨论多播相关网络编程方法。为了传递多播数据包,必须设置 TTL。TTL 是 Time to Live 的简写,是决定 “数据包传递距离” 的主要因素。TTL 用整数表示,并且每经过一个路由器就减1。当 TTL 的值变为 0 时,该数据包无法继续被传递,只能丢弃。因此,TTL 的值设置过大将影响网络流量。当然,设置过小也会无法传递到目标,需要引起注意。

接下来给出 TTL 设置方法。程序中的 TTL 设置是通过套接字(socket)可选项完成的。与设置 TTL 相关的协议层为IP层 IPPROTO_IP,选项名为 IP_MULTICAST_TTL。因此,可以通过如下代码把 TTL 设置为 64。
int send_sock;
int time_live = 64;
......
send_sock = socket(PF_INET, SOCK_STREAM, 0);
setsockopt(send_sock, IPPROTO_IP, IP_MULTICAST_TTL, &time_live, sizeof(time_live));
......
另外,加入多播组也通过设置套接字可选项完成。加入多播组相关的协议层为网络层 IPPROTO_IP,选项名为 IP_ADD_MEMBERSHIP。可通过如下代码加入多播组。
int recv_sock;
struct ip_mreq join_adr;
......
recv_sock = socket(PF_INET, SOCK_STREAM, 0);
......
join_adr.imr_multiaddr.s_addr = inet_addr("多播组IP地址字符串");
join_adr.imr_interface.s_addr = inet_addr("加入多播组的主机IP地址字符串");
setsockopt(recv_sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, &join_adr, sizeof(join_adr));
......
上述代码只给出了与 setsockopt 函数相关的部分,详细内容将在下文的示例中给出。
- ip_mreq 结构体,该结构体的定义如下:
struct ip_mreq
{
struct in_addr imr_multiaddr; //存放首多播组的IP地址
struct in_addr imr_interface; //存放加入多播组的主机IP地址,也可使用 INADDR_ANY
};
//in_addr 结构体定义
struct in_addr
{
in_addr_t s_addr; //用来存放32位IPv4地址
};
1.3 实现基于多播通信方式的 Sender 和 Receiver 程序
多播通信中用 “发送者”(以下称为Sender)和 “接收者”(以下称为Receiver)替代服务器端和客户端。顾名思义,此处的 Sender 是多播数据的发送主体,Receiver 是加入多播组的数据接收主体。
编程实例:下面讨论多播网络编程的示例,该示例的运行场景如下。
- Sender:向 AAA 多播组广播(Broadcasting)文件中保存的新闻信息。
- Receiver:接收传递到 AAA 组的新闻信息。
接下来先给出 Sender端的代码。Sender 比 Receiver 简单,因为 Receiver 需要经过加入多播组的过程,而 Sender 只需创建UDP套接字,并向多播地址发送数据即可。
- 多播通信发送者程序:multicast_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 1024
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] = {0};
if(argc != 3){
printf("Usage: %s <GroupIP> <Port>\n", argv[0]);
exit(1);
}
//初始化多播组网络地址结构体变量
send_sock = socket(PF_INET, SOCK_DGRAM, 0); //创建UDP套接字
memset(&mul_adr, 0, sizeof(mul_adr));
mul_adr.sin_family = AF_INET;
mul_adr.sin_addr.s_addr = inet_addr(argv[1]); //多播组IP地址
mul_adr.sin_port = htons(atoi(argv[2])); //多播组端口号
//TTL设置
setsockopt(send_sock, IPPROTO_IP, IP_MULTICAST_TTL, &time_live, sizeof(time_live));
if((fp=fopen("news.txt", "r")) == NULL)
error_handling("fopen() error!");
while(!feof(fp)) //将news.txt文件内容广播出去
{
fgets(buf, BUF_SIZE, fp); //按行读取文件内容到buf数组中
sendto(send_sock, buf, strlen(buf), 0, (struct sockaddr*)&mul_adr, sizeof(mul_adr));
sleep(2); //暂停2秒
}
fclose(fp);
close(send_sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
- 代码说明
- 第26行:多播数据通信是基于UDP协议实现的,因此需要创建UDP套接字。
- 第27~30行:设置传输数据的目标网络地址信息。重要的是,必须将IP地址设置为多播地址。
- 第33行:指定套接字 TTL 信息,这是 Sender 中的必要过程。
- 第38~43行:实际传输数据的代码区域。基于UDP套接字传输数据,因此需要利用 sendto 函数。另外,第42行的 sleep 函数调用主要是为了给传输数据提供一定的时间间隔而条件的,没有其他特殊意义。
从上述代码中可以看到,Sender 与普通的UDP套接字程序相比差别不大。但多播 Receiver 则有些不同。为了接收传向任意多播地址的数据,需要经过加入多播组的过程。除此之外,Receiver 同样与UDP套接字程序差不多。接下来给出与上述示例结合使用的 Receiver 程序。
- 多播通信接受者程序:multicast_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 1024
void error_handling(char *message);
int main(int argc, char *argv[])
{
int recv_sock;
int str_len;
char buf[BUF_SIZE] = {0};
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!");
//初始化ip_mreq结构体变量join_adr
join_adr.imr_multiaddr.s_addr = inet_addr(argv[1]); //初始化多播组IP地址
join_adr.imr_interface.s_addr = htonl(INADDR_ANY); //初始化待加入多播组主机的IP地址
setsockopt(recv_sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, &join_adr, sizeof(join_adr));
while(1)
{
str_len = recvfrom(recv_sock, buf, BUF_SIZE-1, 0, NULL, NULL);
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);
}
- 代码说明
- 第34、35行:初始化 ip_mreg 结构体变量 join_adr。第34行初始化多播组IP地址,第27行初始化待加入多播组主机的IP地址。利用常数 INADDR_ANY 分配主机的IP地址,可以自动获取本地主机IP地址,这是一种十分便捷的方式。
- 第37行:利用套接字可选项 IP_ADD_MEMBERSHIP 将主机加入多播组。至此完成了接收第34行指定的多播组数据的所有准备工作。
- 第41行:通过调用 recvfrom 函数接收多播数据。如果不需要知道传输数据的主机地址信息,可以向 recvfrom 函数的第5个参数和第6个参数都传递 NULL。
- 运行结果
- 多播通信发送者:multicast_sender.c
$ gcc multicast_sender.c -o sender
$ ./sender
Usage: ./sender <GroupIP> <Port>
$ ./sender 224.1.1.2 9190
- 多播通信接收者:multicast_receiver.c
$ gcc multicast_receiver.c -o receriver
$ ./receriver
Usage: ./receriver <GroupIP> <Port>
$ ./receriver 224.1.1.2 9190
11111111111111111
22222222222222222
33333333333333333
44444444444444444
- 程序说明
Sender 和 Receiver 程序之间的端口号应保持一致。程序运行先后顺序并不重要,因为不像TCP套接字在连接状态下收发数据。只是因为多播数据广播的范畴,如果延迟运行Receiver,则无法接收之前传输的多播数据,因此建议先运行 Receiver程序,再运行 Sender程序,这样运行结果会符合我们的预期。
《知识补充》MBone(Multicast Backbone,多播主干网)
多播是基于 MBone 这个虚拟网络工作的。各位或许对虚拟网络感到陌生,但可将其理解为 “通过网络中的特殊协议工作的软件概念上的网络”。也就是说,MBone 并非可以触及的物理网络。它是以物理网络为基础,通过软件方法实现的多播通信必备虚拟网络。用于多播通信的虚拟网络的研究目前仍在进行,这与多播应用程序的编写属于不同领域。
二 广播
广播(Broadcast)是指 “一次性向多个主机发送相同的数据” 这一点上与多播类似,但传输数据的范围有区别。多播即使在跨越不同网络的情况下,只要加入多播组就能接收数据。相反,广播只能向同一网络中的主机传输数据。这是二者之间的区别所在。
2.1 广播的理解及实现方法
广播是向同一网络中的所有主机传输数据的方式。与多播相同,广播通信也是基于UDP协议实现的。根据传输数据时使用的IP地址的形式,广播分为如下两种。
- 直接广播(Directed Broadcast)
- 本地广播(Local Broadcast)
二者在代码实现上的差别主要在于 IP 地址。直接广播的 IP 地址中除了网络地址外,其余主机地址全部设置为 1。例如,希望向网络地址 192.12.34.0 中的所有主机传输数据时,可以向 192.12.34.255 传输。换言之,可以采用直接广播的方式向特定区域内所有主机传输数据。
反之,本地广播中使用的 IP 地址限定为 255.255.255.255。例如,192.32.24.0 网络中的主机向 255.255.255.255 传输数据时,数据将传递到 192.32.24.0 网络中的所有主机。
那么,应当如何实现 Sender 和 Receiver 呢?实际上,如果不仔细观察广播示例中使用的 IP 地址,则很难与 UDP 示例进行区分。也就是说,数据通信中使用的 IP 地址是与 UDP 示例的唯一区别。默认生成的套接字会阻止广播,因此,只需通过如下代码更改默认设置。
int send_sock;
int bcast = 1; //对变量进行初始化以将SO_BROADCAST可选项信息改为1
. . . .
send_sock = socket(PF_INET, SOCK_DGRAM, 0);
. . . .
setsockopt(send_sock, SOL_SOCKET, SO_BROADCAST, &bcast, sizeof(bcast));
. . . .
调用 setsockopt 函数,将 SO_BROADCAST 可选项设置为 bcast 变量中的值 1。则意味着可以进行数据广播传输。当然,上述套接字选项只需在 Sender 中更改,Receiver 的实现不需要该过程。
2.2 实现基于广播通信方式的 Sender 和 Receiver 程序
编程实例:下面实现基于广播的 Sender 和 Receiver。为了与多播示例进行对比,将之前的 multicast_sender.c 和 multicast_receiver.c 程序改为广播的示例。
- 广播通信发送者程序:broadcast_sender.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
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] = {0};
if(argc != 3){
printf("Usage: %s <Broadcast 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]));
int bcast = 1;
setsockopt(send_sock, SOL_SOCKET, SO_BROADCAST, &bcast, sizeof(bcast));
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~30行:使用 setsockopt 函数设置 UDP 套接字的 SO_BROADCAST 选项,使其能够发送广播数据。
- 广播通信接收者程序:broadcast_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 1024
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] = {0};
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, NULL);
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);
}
下面给出上述广播通信示例的运行结果,我给出的是本地广播的运行结果,有条件的话,可以进一步验证直接广播的运行结果。建议先运行接受者程序 Receiver,然后再运行发送者程序 Sender。
- 运行结果
- 广播通信发送者:broadcast_sender.c
$ gcc broadcast_sender.c -o sender
$ ./sender 255.255.255.255 9190
- 广播通信接收者:broadcast_receiver.c
$ gcc broadcast_receiver.c -o receiver
$ ./receiver 9190
11111111111111111
22222222222222222
33333333333333333
44444444444444444
三 习题
1、TTL的含义是什么?请从路由器的角度说明较大的TTL值与较小的TTL值之间的区别及问题。
答:TTL(Time to Live,生存时间),它的含义是一个 IP 数据报(或者叫IP分组)在网络中的存在时间,用整数表示,并且每经过一个路由器就减1,直到 TTL 变为0时,该IP数据报无法继续被传递和转发,最后作丢弃处理。如果 TTL 的值设置过大,会对网络流量造成不良影响;如果 TTL 的值设置过小,有可能导致IP数据报无法到达目的地。
2、多播和广播的异同是什么?请从数据通信的角度进行说明。
- 相同点:(1)二者都是基于UDP协议进行数据传输;(2)只需发送一次数据,就可以同时向多个主机传输数据。
- 不同点:二者传输的范围不同:多播是对多播组中的所有主机传递数据,可以跨越不同网络,只要主机均在同一个多播组;而广播是对同一网络内的所有主机传递数据。
3、下列关于多播的描述错误的是?
a. 多播是用来向加入多播组的所有主机传输数据的协议。
b. 主机连接到同一网络才能加入多播组,也就是说,多播组无法跨越多个网络。
c. 能够加入多播组的主机并无限制,但只能有一个主机(Sender)向该组发送数据。
d. 多播时使用的套接字是UDP套接字,因为多播时基于UDP进行数据通信的。
答:b、c。分析如下:
- b:多播组中的主机并不要求是在同一网络中才能加入多播组,多播组是可以跨越多个网络的,因此 b 的描述是错误的。
- c:多播组中的任何一个主机都可以向组内的其他主机发送多播数据包,因此,c 的描述是错误的。
4、多播也对网络流量有利,请比较 TCP 数据交换方式解释其原因。
答:多播发送端每次只需发送一次数据即可向多播组内的所有主机传递数据,虽然多播数据包在经过路由器时会复制数据包,但是不会向相同区域发送多个相同数据包,而如果使用TCP数据交换方式,则需要根据主机数量分别进行数据传输,数据流量会大大增加。
5、多播方式的数据通信需要MBone虚拟网络。换言之,MBone是用于多播的网络,但它是虚拟网络。请解释此处的“虚拟网络”。
答:以物理网络为基础,通过软件方法实现的多播通信必备的虚拟网络。
参考
《TCP-IP网络编程(尹圣雨)》第14章 - 多播与广播
《计算机网络(第7版-谢希仁)》第4章 - 网络层:第4.7节 - IP多播