TCP/IP 网络编程(十三)---多播与广 播

1,017 阅读9分钟

多播

(一)定义

多播(Multicast) 是一种网络通信方式,它允许一个发送方将数据同时发送给多个接收方,而不必为每个接收方单独发送数据包。 多播在网络中的效率较高,特别适合需要向多个目标发送相同数据的应用场景,如视频会议、直播流媒体、网络游戏和分布式计算。

(二)多播的工作原理

多播通过使用专门的多播地址和多播组的概念来实现,以下是其基本工作原理:

  1. 多播地址

    • IPv4 中,多播地址范围是 224.0.0.0239.255.255.255,即 D 类地址。
    • IPv6 中,多播地址以 ff00::/8 开头。
    • 这些地址专门用于多播通信,而不是单播(点对点通信)或广播(发送给网络中所有设备)。
  2. 多播组

    • 多播组是指一组对同一多播地址感兴趣的接收方。接收方通过加入(subscribe)一个多播组来表明它们对该组的流量感兴趣。
    • 发送方发送数据到多播组地址,只有加入该组的接收方会接收数据
  3. 组播协议

    • 多播通信依赖于组播路由协议,如 IGMP(Internet Group Management Protocol)用于 IPv4,MLD(Multicast Listener Discovery)用于 IPv6。
    • 这些协议帮助路由器维护多播组成员信息,并确保数据包只传递给需要的子网和设备。
  4. 多播数据传输

    • 当一个发送方发送多播数据时,数据包只会被发送一次,即使有多个接收方。
    • 网络设备(如路由器和交换机)会复制数据包,并将其发送到加入多播组的各个子网或设备。

多播是基于 UDP 完成的,也就是说多播数据包的格式与 UDP 数据包相同。只是与一般的 UDP 数据包不同,向网络传递1个多播数据包时,路由器将复制该数据包并传递到多个主机。下图表示多播路由:

image.png

(三)多播数据传输特点

多播服务器端针对特定多播组,只发送1次数据。

即使只发送1次数据,该组内的所有客户端都会接收数据。

多播数组可在 IP 地址范围内任意增加。

加入特定组即可接收发往该多播组的数据。

(四)路由(Routing)和 TTL(Time to Live,生存时间)

(1)TTL 与路由的联系

为了传递多播数据包,必须设置 TTL。TTL 是决定 “数据包传递距离” 的主要因素。TTL 用整数表示,并且每经过1个路由器就减1。TTL 变为0时,该数据包无法再被传递,只能销毁。 因此,TTL 的值设置过大将影响网络流量。当然,设置过小也会导致无法传递到目标。下图表示 TTL 和多播路由的联系:

image.png

(2)TTL 的设置方法

TTL 的设置是通过套接字可选项完成的。 与设置 TTL 相关的协议层为 IPPROTO_IP ,选项名为 IP_MULTICAST_TTL。因此可用如下代码将 TTL 设置为64。

int send_sock;
int time_live = 64;
...
send_sock = secket(PF_INET, SOCK_SGRAM, 0);
setsockopt(send_sock, IPPROTO_IP, IP_MULTICAST_TTL, (void*) &time_live, sizeof(time_live));
...

(五)加入多播组

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

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));
...
  • ip_mreq 结构体
    struct ip_mreq
    {
        struct in_addr imr_multiaddr;
        struct in_addr imr_interface;
    }
    
    • imr_multiaddr:

      • 类型:struct in_addr
      • 说明:指定要加入或离开的多播组的IP地址。通常是一个D类地址(224.0.0.0 到 239.255.255.255)。
    • imr_interface:

      • 类型:struct in_addr
      • 说明:表示加入该组的套接字所属主机的 IP 地址,也可使用 INADDR_ANY

(六)实现多播 Sender 和 Receiver

多播中用 “发送者” 和 “接收者” 替代服务器端和客户端。顾名思义,此处的 Sender 是多播数据的发送主体,Receiver 是需要多播数据的接收主体。

(1)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>\n", 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('\n', stderr);
	exit(1);
}
  • 套接字创建和配置

    • 创建了一个UDP套接字 (send_sock),用于发送多播数据。
    • 配置了多播地址和端口。多播地址是通过 inet_addr(argv[1]) 设置的,端口号是通过 htons(atoi(argv[2])) 设置的。
  • TTL (Time To Live) 配置

    • 使用 setsockopt 函数配置了多播数据包的 TTL 值。TTL 值是数据包在网络中能够经过的最大路由器数。它可以控制多播数据包的传播范围。
    • 通过 setsockopt(send_sock, IPPROTO_IP, IP_MULTICAST_TTL, (void*)&time_live, sizeof(time_live)) 设置了 TTL 值为 64。
  • 读取文件并发送数据

    • 打开文件 news.txt,并使用 fgets 逐行读取文件内容。
    • 每读取一行内容,程序使用 sendto 函数将数据发送到指定的多播组中。
    • 每次发送后,程序等待2秒 (sleep(2)) 再发送下一行内容。

(2)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>\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);
}
  • 套接字创建和绑定

    • 创建了一个UDP套接字 (recv_sock),用于接收多播数据。
    • 绑定了套接字到一个本地地址和端口。adr.sin_addr.s_addr=htonl(INADDR_ANY) 表示接收来自任何IP地址的数据。
  • 加入多播组

    • struct ip_mreq join_adr 用于指定加入的多播组的地址和本地接口。
    • join_adr.imr_multiaddr.s_addr 是要加入的多播组的IP地址(由用户通过命令行参数指定)。
    • join_adr.imr_interface.s_addr 是加入多播组的本地接口地址,使用 INADDR_ANY 表示默认接口。
    • 使用 setsockopt 函数调用 IP_ADD_MEMBERSHIP 告诉操作系统将此套接字加入到指定的多播组中。
  • 接收数据

    • 在一个无限循环中,使用 recvfrom 函数从多播组中接收数据,并将其打印到标准输出。
    • recvfrom 函数接收的数据长度存储在 str_len 中,如果接收到的数据长度小于0,意味着接收过程中出错,循环结束。
    • 将接收到的数据终止符设置为0(字符串结束符),然后将其打印到标准输出。

image.png

广播

(一)定义

广播(Broadcasting)是一种在计算机网络中向一个特定子网内的所有主机发送数据的技术。

(二)广播的分类

  • 直接广播(Directed Broadcast)

    • 直接广播是指将数据包发送到一个特定网络的所有主机上。
    • 直接广播地址是通过将主机部分全置为 1 来形成的。例如,网络 192.168.1.0/24 的直接广播地址为 192.168.1.255。发送到这个地址的数据包将传送到网络 192.168.1.0/24 中的所有主机。
  • 本地广播(Local Broadcast)

    • 本地广播是指将数据包发送到当前子网中的所有主机。
    • 本地广播地址通常是 255.255.255.255,它指向当前网络中的所有设备。发送到这个地址的数据包不会被路由器转发,仅在本地网络中传播。

(三)广播的实现

数据通信中使用的 IP 地址是与 UDP 示例的唯一区别。默认生成的套接字会阻止广播。

(1)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 <Broadcast IP> <PORT>\n", argv[0]);
        exit(1);
    }

    // 创建一个 UDP 套接字
    send_sock = socket(PF_INET, SOCK_DGRAM, 0);    
    if (send_sock == -1) {
        error_handling("socket() error");
    }

    // 初始化广播地址结构体
    memset(&broad_adr, 0, sizeof(broad_adr));
    broad_adr.sin_family = AF_INET;  // 地址族为 IPv4
    broad_adr.sin_addr.s_addr = inet_addr(argv[1]);  // 设置广播 IP 地址
    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);  // 暂停 2 秒钟,以便接收端有时间处理数据
    }

    // 关闭套接字和文件
    close(send_sock);
    fclose(fp);
    
    return 0;
}

// 错误处理函数定义
void error_handling(char *message)
{
    fputs(message, stderr);  // 将错误信息写入标准错误流
    fputc('\n', stderr);    // 写入换行符
    exit(1);                // 退出程序
}

(2)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     // 定义缓冲区大小为 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);
    }
  
    // 创建一个 UDP 套接字
    recv_sock = socket(PF_INET, SOCK_DGRAM, 0);
    if (recv_sock == -1) {
        error_handling("socket() error");
    }

    // 初始化地址结构体
    memset(&adr, 0, sizeof(adr));
    adr.sin_family = AF_INET;                  // 地址族为 IPv4
    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);                // 退出程序
}