多播/组播
从基础知识 ip 地址一节知识,可以知道多播使用 D 类 IP 地址。
下面就通过程序来完成一个组播的实现。
实现逻辑如下:发送方应该创建一个多播组,并且发送组间消息。接收方应该加入多播组,并且接收组间消息。因此,这个组就必须成为大家都应该知道的事情。因此应该在双方约定的协议中进行定义。
在 proto.h 中,
//定义一个多播组,它的组号(ip地址)为“224.2.2.2”
#define MTGROUP "224.2.2.2"
在 snder.c 中,
发送的目标地址就应该是
inet_pton(AF_INET,MTGROUP,&raddr.sin_addr.s_addr);
多播也是不能随时发送的,它也有它自己所谓的开关或旋钮。
使用命令 “man 7 ip” 查看 ip 层的手册,然后使用命令 “/socket opt” 来查看,
Socket options
IP supports some protocol-specific socket options that can be set with setsockopt(2) and read with getsockopt(2). The socket option level for IP is IPPROTO_IP. A boolean integer flag is zero when it is false,otherwise true.
翻译如下:
IP支持一些特定于协议的套接字选项,这些选项可以用setsockopt(2)设置,也可以用getsockopt(2)读取。IP的套接字选项级别是IPPROTO_IP。
如果想要加入多播组,看下面。
IP_ADD_MEMBERSHIP (since Linux 1.2)
Join a multicast group. Argument is an ip_mreqn structure.
struct ip_mreqn {
struct in_addr imr_multiaddr; /* IP multicast group
address 多播组地址*/
struct in_addr imr_address; /* IP address of local
interface 当前自己的Ip地址*/
int imr_ifindex; /* interface index 网络索引号*/
};
//struct in_addr 实际就是一个uint32_t的大整数,只不过封装了一下。
//这里使用inet_pton来进行转换
//对于网络索引号应该怎么查看呢?
//我们可以使用命令 ip ad sh来查看
在程序里面,我们可以使用一个函数来获取,
SYNOPSIS
#include <net/if.h>
unsigned int if_nametoindex(const char *ifname);
而如果想要离开多播组,看下面。
IP_DROP_MEMBERSHIP (since Linux 1.2)
Leave a multicast group. Argument is an ip_mreqn or ip_mreq structure similar to IP_ADD_MEMBERSHIP.
而如果想要创建一个多播组,看下面。
IP_MULTICAST_IF (since Linux 1.2)
Set the local device for a multicast socket. The argument for setsockopt(2) is an ip_mreqn or (since Linux 3.5) ip_mreq structure similar to IP_ADD_MEMBERSHIP, or an in_addr structure. (The kernel determines which structure is being passed based on the size passed in optlen.) For getsockopt(2), the argument is an in_addr structure.
配置本设备为组播套接字。setsockopt(2)的参数是一个ip_mreqn或ip_mreq结构体(从Linux 3.5开始),类似于IP_ADD_MEMBERSHIP,或in_addr结构。(内核根据optlen中传递的大小确定传递的是哪个结构。)对于getsockopt(2),参数是一个in_addr结构。
因此,作为发送方,应该创建多播组,而作为接收方,则应该加入多播组。
NAME
getsockopt, setsockopt - get and set options on sockets
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int getsockopt(int sockfd, int level, int optname,
void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname,
const void *optval, socklen_t optlen);
sockfd -- socket 文件描述符
level -- 在Socket options处提到IP的套接字选项级别是IPPROTO_IP。
optname -- 属性名,这里设置为IP_MULTICAST_IF,表明创建一个多播组
optval -- 应该是一个ip_mreqn或ip_mreq结构体(从Linux 3.5开始),类似于IP_ADD_MEMBERSHIP,或in_addr结构。
optlen -- 传入这个数据类型的大小。
我要对某一个 socket 描述符的level 层 及 optname 进行指定 ,由于是不同的level ,不同的optname ,所以就会有不同的 optval 参数,optval的类型可能是int、bool、结构体等等,所以 optlen 也就不一样。
RETURN VALUE
On success, zero is returned for the standard options. On error, -1 is
returned, and errno is set appropriately.
回顾下,inet_pton
NAME
inet_pton - convert IPv4 and IPv6 addresses from text to binary form
将IPv4 and IPv6 addresse转换成二进制形式
SYNOPSIS
#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst);
RETURN VALUE
inet_pton() returns 1 on success (network address was successfully converted). 0 is returned if src does not contain a character string representing a valid network address in the specified address family. If af does not contain a valid address family, -1 is returned and errno is set to EAFNOSUP‐PORT.
第一个参数af,因为转换的是ipv4和ipv6的地址,所以使用的协议族要么是 AF_INET 要么是AF_INET6.
第二个参数const char *src,待转换的ip地址。
第三个参数void *dst,表示转换完成后,放到哪块空间去。
snder.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <net/if.h>
#include "proto.h"
int main(int argc,char* argv[])
{
//修改1:添加 name 字段
if(argc < 2)
{
fprintf(stderr,"%s ipaddress message.\n",argv[0]);
exit(1);
}
if(strlen(argv[1]) > NAMEMAX)
{
fprintf(stderr,"message is too long.\n");
exit(1);
}
// 1、使用SOCKET来创建套接字
int sd;
int size;
//修改2:将要发送的信息的地址需要修改成指针
struct msg_st* psbuf;
struct sockaddr_in raddr;
//给要发送的信息申请空间
//申请多大空间呢
size = sizeof(struct msg_st)+strlen(argv[1]);
psbuf = malloc(size);
if(psbuf == NULL)
{
fprintf(stderr,"malloc fail.\n");
exit(1);
}
sd = socket(AF_INET,SOCK_DGRAM,0);//这里相当于是用IPv4协议族中的IPPROTO_UDP>来实现SOCK_DGRAM的操作
if(sd < 0)
{
perror("socket()");
exit(1);
}
struct ip_mreqn mreq;
inet_pton(AF_INET,MTGROUP,&mreq.imr_multiaddr);
inet_pton(AF_INET,"0.0.0.0",&mreq.imr_address);
mreq.imr_ifindex = if_nametoindex("eth0");
// 创建多播组
if(setsockopt(sd,IPPROTO_IP,IP_MULTICAST_IF,&mreq,sizeof(mreq))<0)
{
perror("setsockopt()");
exit(1);
}
// 2、给SOCKET取得地址(可省略)
// 3、收/发消息
// 字符串,不能直接使用"=",应该使用strcpy函数。
strcpy(psbuf->name, argv[1]);
psbuf->math = htonl(rand()%100);
psbuf->chinese = htonl(rand()%100);
raddr.sin_family = AF_INET;
raddr.sin_port = htons(atoi(RCVPORT));//对端的端口
inet_pton(AF_INET,MTGROUP,&raddr.sin_addr.s_addr);
if(sendto(sd,psbuf,size,0,(void *)&raddr,sizeof(raddr))<0)
{
perror("sendto()");
exit(1);
}
puts("OK!\n");
// 4、关闭SOCKET
close(sd);
return 0;
}
proto.h
//定义一个多播组,它的组号(ip地址)为“224.2.2.2”
#define MTGROUP "224.2.2.2"
//将来使用到时用atoi进行转换
#define RCVPORT "1995"
#define NAMEMAX (512-8-8)
//512是udp包的推荐大小
//第一个8是udp包头的大小
//第二个8是uint32_t math;和uint32_t chinese;所占的8个字节大小
struct msg_st
{
uint32_t math;
uint32_t chinese;
// 尽管使用 name[0] 来做占位符操作,但是还是有必要定义以name为起始地址空间的
最大值
uint8_t name[0];
// 需要注意在数组中写0是C99的标准,因此如果使用C99以前的GCC编译器来编译代码>是会出错的
// 因此,有时候为了避免出错,可以将0写成1.
}__attribute__((packed));
#endif
下面再来看看接收方,接收方应该加入多播组。因此也需要设置setsockopt。加入多播组的属性为IP_ADD_MEMBERSHIP (看前面介绍)
rcver.c
#include <stdio.h>
...
#include <net/if.h>
#include "proto.h"
...
struct ip_mreqn mreq;
inet_pton(AF_INET,MTGROUP,&mreq.imr_multiaddr);
inet_pton(AF_INET,"0.0.0.0",&mreq.imr_address);
mreq.imr_ifindex = if_nametoindex("eth0");
if(setsockopt(sd,IPPROTO_IP,IP_ADD_MEMBERSHIP,&mreq,sizeof(mreq))<0)
{
perror("setsockopt() error!!!\n");
exit(1);
}
...
编译代码后运行,
这里就是 snder 方负责创建多播组,而 rcver 方则负责加入多播组。这样就完成了组间通信。
此外,因为多播中有一个特殊的 ip 地址(224.0.0.1),它表示,所有支持多播的地址默认都存在这个组当中,并且无法离开。如果snder 方向这个 ip 地址发送信息,就相当于向 255.255.255.255 上发消息。