视频会议、实时股票报价都应用到组播通讯。多播通信通常用于需要将数据同时发送给多个接收者的场景。
组播,顾名思义,即是组内成员广播。
首先当然是要定义组、加入组的了。
发送的时候,sendto 的地址参数上,就能直接设置组信息。
接受某个组的时候要单独进行设置。
下面2个数据结构与函数完成用于完成该工作。
- struct ip_mreq
setsockopt()
struct ip_mreq
是一个结构体,用于指定多播(Multicast)通信中的多播组IP地址和本地网络接口地址。这个结构体通常与 setsockopt()
函数一起使用,以加入一个多播组。
以下四个步骤完整这个设置。
-
定义
struct ip_mreq
结构体:
struct ip_mreq
{
struct in_addr imr_multiaddr; /* 指定多播组IP */
struct in_addr imr_interface; /* 本地网卡地址,通常指定为 INADDR_ANY (0.0.0.0) */
};
它包含两个 struct in_addr 类型的成员:imr_multiaddr 用于存储多播组的IP地址,imr_interface 用于存储本地网络接口的地址。
2、使用前要声明并初始化 struct ip_mreq
变量:
struct ip_mreq mreq;
bzero(&mreq, sizeof(mreq));
使用 bzero() 函数将其内存区域清零。bzero() 函数将指定大小的内存区域设置为零,这里的大小是 sizeof(mreq),即 struct ip_mreq 结构体的大小。
3、设置多播组地址和本地网络接口地址:
mreq.imr_multiaddr.s_addr = inet_addr("224.10.10.1");
mreq.imr_interface.s_addr = INADDR_ANY;
这里将多播组的IP地址设置为 224.10.10.1,这是一个有效的多播地址。本地网络接口地址被设置为 INADDR_ANY,表示程序将在所有可用的网络接口上监听多播数据。
4、调用 setsockopt()
函数加入多播组:
setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq));
这里调用 setsockopt()
函数,将套接字 sockfd
加入到之前指定的多播组。IPPROTO_IP
表示使用IPv4协议,IP_ADD_MEMBERSHIP
是用于加入多播组的选项。&mreq
是指向 struct ip_mreq
变量的指针,sizeof(mreq)
是结构体的大小。
我们的目的是将一个套接字加入到指定的多播组,以便能够接收发送到该多播组的数据。
多播地址的规则:
- 地址范围:
- IPv4中的多播组地址范围是从224.0.0.0到239.255.255.255。
- 地址格式:
- 多播组地址的前4位固定为“1110”,这是多播地址的标识。
- 剩余的28位用于标识特定的多播组。
- 地址类型:
- 多播组地址可以分为全球唯一地址和本地唯一地址。
- 全球唯一地址可以在全球范围内使用,而本地唯一地址则限制在特定的网络或组织内使用。
- 特殊地址:
- 某些多播组地址被指定为特殊用途,如224.0.0.1用于本子网上所有参加多播的主机和路由器,224.0.0.2用于本子网上所有参加多播的路由器等。
- 地址分配:
- 多播组地址的使用和分配现在由互联网地址指派机构(IANA)控制。
IPv6中的多播组地址规则
- 地址前缀:
- IPv6中的多播组地址以FF00::/8为前缀。
- 地址格式:
- 多播组地址的前8位固定为“FF”,这是多播地址的标识。
- 剩余的位数用于标识特定的多播组。
- 地址类型:
- 与IPv4类似,IPv6中的多播组地址也可以分为全球唯一地址和本地唯一地址。
- 特殊地址:
- 某些多播组地址在IPv6中也有特殊的用途,如FF02::1是所有设备都可以加入的广播地址。
其他:
- 在配置网络以支持多播时,应确保网络设备(如路由器和交换机)支持多播协议,并正确配置了多播路由和转发规则。
一个完整演示的demo:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
#include <netinet/in.h>
#include <sys/time.h> // for settimeofday, though not used directly here
#define MULTICAST_ADDR "239.255.255.250"
#define MULTICAST_PORT 12345
#define BUFFER_SIZE 1024
void* send_multicast_message(void* arg) {
int sockfd = *((int*)arg);
const char* message = "Hello, Multicast!";
struct sockaddr_in multicast_addr;
memset(&multicast_addr, 0, sizeof(multicast_addr));
multicast_addr.sin_family = AF_INET;
multicast_addr.sin_port = htons(MULTICAST_PORT);
inet_pton(AF_INET, MULTICAST_ADDR, &multicast_addr.sin_addr);
while (1) {
sendto(sockfd, message, strlen(message), 0,
(struct sockaddr*)&multicast_addr, sizeof(multicast_addr));
printf("Sent: %s\n", message);
sleep(1);
}
return NULL;
}
void* receive_multicast_message(void* arg) {
int sockfd = *((int*)arg);
char buffer[BUFFER_SIZE];
struct sockaddr_in src_addr;
socklen_t addr_len = sizeof(src_addr);
while (1) {
int num_bytes = recvfrom(sockfd, buffer, BUFFER_SIZE, 0,
(struct sockaddr*)&src_addr, &addr_len);
if (num_bytes > 0) {
buffer[num_bytes] = '\0';
printf("Received from %s: %s\n", inet_ntoa(src_addr.sin_addr), buffer);
}
}
return NULL;
}
int main() {
int sockfd;
struct sockaddr_in local_addr;
pthread_t sender_thread, receiver_thread;
// 创建UDP套接字
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
int allow = 1;
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT,
&allow, sizeof(allow)) < 0) {
perror("setsockopt");
close(sockfd);
exit(EXIT_FAILURE);
}
// 绑定套接字到本地地址和端口(可选,但推荐)
memset(&local_addr, 0, sizeof(local_addr));
local_addr.sin_family = AF_INET;
local_addr.sin_addr.s_addr = htonl(INADDR_ANY);
local_addr.sin_port = htons(MULTICAST_PORT);
if (bind(sockfd, (const struct sockaddr*)&local_addr, sizeof(local_addr)) < 0) {
perror("bind failed");
close(sockfd);
exit(EXIT_FAILURE);
}
// 允许接收组播消息
struct ip_mreq mreq;
mreq.imr_multiaddr.s_addr = inet_addr(MULTICAST_ADDR);
mreq.imr_interface.s_addr = htonl(INADDR_ANY);
if (setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, (char*)&mreq, sizeof(mreq)) < 0) {
perror("setsockopt");
close(sockfd);
exit(EXIT_FAILURE);
}
// 启动发送和接收线程
if (pthread_create(&sender_thread, NULL, send_multicast_message, &sockfd) != 0) {
perror("pthread_create sender failed");
close(sockfd);
exit(EXIT_FAILURE);
}
if (pthread_create(&receiver_thread, NULL, receive_multicast_message, &sockfd) != 0) {
perror("pthread_create receiver failed");
close(sockfd);
pthread_cancel(sender_thread); // 尝试取消已创建的发送线程
exit(EXIT_FAILURE);
}
// 注意:在纯C中,我们通常不会在这里等待线程完成,因为它们是无限循环的。
// 如果需要终止程序,应该使用某种信号或全局变量来控制线程的退出。
// 这里为了示例,我们让主线程也无限等待(不推荐在实际应用中使用)。
// 通常,你会在另一个线程或信号处理程序中设置某个条件来优雅地关闭所有线程。
// 无限等待(不推荐)
while (1) sleep(1);
// 正确的方式是使用某种机制来通知主线程可以安全地关闭套接字和退出程序。
// 由于示例的简化性质,这里我们不会实现这样的机制。
// 注意:在实际应用中,不要忘记在程序退出前清理资源,包括取消线程和关闭套接字。
// 示例代码结束,这里不会真正退出程序。
// 清理代码(在实际应用中需要实现):
// pthread_cancel(sender_thread);
// pthread_cancel(receiver_thread);
// pthread_join(sender_thread, NULL);
// pthread_join(receiver_thread, NULL);
// close(sockfd);
return 0; // 这行代码在实际应用中不会被执行,因为上面有一个无限循环。
}
编译它:
gcc multcast.c -o multcast -lpthread
运行结果: