LV06-03-网络编程-10-广播与多播

本文主要是网络编程——广播和多播的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
Windows windows11
Ubuntu Ubuntu16.04的64位版本
VMware® Workstation 16 Pro 16.2.3 build-19376536
SecureCRT Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日
开发板 正点原子 i.MX6ULL Linux阿尔法开发板
uboot NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03)
linux内核 linux-4.15(NXP官方提供)
STM32开发板 正点原子战舰V3(STM32F103ZET6)
点击查看本文参考资料
参考方向 参考原文
------
点击查看相关文件下载
--- ---

一、广播编程

1. 广播的概念

在前面介绍的数据包发送方式只有一个接受方,称为单播,那什么叫广播呢?广播broadcast)是指封包在计算机网络中传输时,目的地址为网络中所有设备的一种传输方式。实际上,这里所说的“所有设备”也是限定在一个范围之中,称为广播域

也就是说,如果同时发给局域网中的所有主机,称为广播。需要注意的是只有用户数据报(使用UDP协议)套接字才能广播

2. 广播的缺点

  • 无法针对每个客户的要求和时间及时提供个性化服务。
  • 网络允许服务器提供数据的带宽有限,客户端的最大带宽=服务总带宽。例如有线电视的客户端的线路支持100个频道(如果采用数字压缩技术,理论上可以提供500个频道),即使服务商有更大的财力配置更多的发送设备、改成光纤主干,也无法超过此极限。也就是说无法向众多客户提供更多样化、更加个性化的服务。
  • 广播禁止在Internet宽带网上传输。

3. 广播分类

广播分为两种,直接广播本地广播(也叫受限广播),两者的主要差别如下

  • 目标网络方面

直接广播的目标网络和发送端不在同一个网络,本地广播的目标网络就是发送端本机所在的网络。

  • IP地址方面

直接广播的IP地址,除了网络地址外,其余主机地址全部设置为1.如希望向网络地址192.168.5的所有主机传输数据时,发送端的目标IP要设置为192.168.5.255传输。而本地广播中使用的IP地址限定为255.255.255.255

需要注意的是本地广播只能在局域网内转发,主要用于不知道目标主机的掩码与网络地址的情况,本地转发同本地网络下的所有主机。

4. 广播地址

其实上边已经提过了,主机地址全部为1的地址就是该网段的广播地址。以192.168.1.0 (255.255.255.0) 网段为例,最大的主机地址192.168.1.255代表该网段的广播地址,发到该地址的数据包被所有的主机接收。

【注意】255.255.255.255在所有网段中都代表广播地址。

5. 广播编程步骤

  • 创建用户数据报套接字,注意soclet()函数第二个参数要写为SOCK_DGRAM
  • 设置网络属性,允许广播数据包,这是因为默认的情况下创建的套接字是不允许广播数据包的。
  • 接收方地址指定为广播地址。
  • 指定端口信息。
  • 发送数据包。
  • 创建用户数据报套接字,注意soclet()函数第二个参数要写为SOCK_DGRAM
  • 绑定本机IP地址和端口,绑定的端口必须和发送方指定的端口相同。
  • 等待接收数据。

6. 使用实例

6.1 server服务器端

点击查看实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
/** =====================================================
* Copyright © hk. 2022-2022. All rights reserved.
* File name: server.c
* Author : fanhua
* Description: server服务器端——广播(UDP)
* ======================================================
*/
/* 头文件 */
#include <stdio.h> /* perror */
#include <stdlib.h> /* exit atoi */
#include <errno.h> /* errno号 */
#include <sys/types.h> /* socket bind listen recvfrom sendto */
#include <sys/socket.h> /* socket inet_addr bind listen recvfrom sendto */
#include <strings.h> /* bzero */
#include <arpa/inet.h> /* htons inet_addr inet_pton */
#include <netinet/in.h> /* ntohs inet_addr inet_ntop*/
#include <unistd.h> /* close */
#include <string.h> /* strlen strcat*/

void usage(char *str); /* 提示信息打印函数 */

int main(int argc, char *argv[])
{
/* 1.参数判断及端口号处理 */
int port = -1;
if (argc != 3)/* 参数数量不对时打印提示信息 */
{
usage(argv[0]);
exit(-1);
}
port = atoi(argv[2]);/* 字符串转数字 */
if (port < 5000)
{
usage(argv[0]);
exit(-1);
}
/* 2.创建套接字,得到socket描述符 */
int socket_fd = -1; /* 接收服务器端socket描述符 */
if((socket_fd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)/* SOCK_STREAM,使用TCP协议 */
{
perror ("socket");
exit(-1);
}
/* 3.网络属性设置 */
/* 3.1允许绑定地址快速重用 */
int b_reuse = 1;
setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof (int));

/* 4.将套接字与指定端口号和IP进行绑定 */
/* 4.1填充struct sockaddr_in结构体变量 */
struct sockaddr_in sin;
bzero (&sin, sizeof (sin)); /* 将内存块(字符串)的前n个字节清零 */
sin.sin_family = AF_INET; /* 协议族, IPv4 */
sin.sin_port = htons(port); /* 网络字节序的端口号 */
if(inet_pton(AF_INET, argv[1], (void *)&sin.sin_addr) != 1)/* 填充IP地址,INADDR_ANY表示允许监听任意IP,但是它其实是(in_addr_t) 0x00000000 */
{
perror ("inet_pton");
exit(-1);
}
/* 4.2绑定 */
if(bind(socket_fd, (struct sockaddr *)&sin, sizeof(sin)) < 0)
{
perror("bind");
exit(-1);
}
printf ("Boardcast server starting....OK(UDP)!\n");
/* 5.数据读写相关变量定义 */
/* 5.2客户端IP地址及端口号获取的相关变量 */
struct sockaddr_in cin; /* 用于存储成功连接的客户端的IP信息 */
socklen_t addrlen = sizeof(cin);
char ipv4_addr[16];
/* 5.2数据读写相关变量 */
char buf[BUFSIZ]; /* BUFSIZ是在stdio.h中定义的一个宏,值为8192 */
/* 6.数据读写 */
while(1)
{
/* 6.1接收来自客户端的数据 */
bzero(buf, BUFSIZ);
if(recvfrom(socket_fd, buf, BUFSIZ-1, 0,(struct sockaddr *)&cin, &addrlen ) < 0)
{
perror("recvfrom");
continue;
}
/* 6.2打印客户端的IP信息 */
if (!inet_ntop (AF_INET, (void *) &cin.sin_addr, ipv4_addr, sizeof (cin)))
{
perror ("inet_ntop");
exit(-1);
}
printf("Recived from(%s:%d), data:%s", ipv4_addr, ntohs(cin.sin_port), buf);
/* 6.3判断是否需要退出 */
if(!strncasecmp(buf, "quit", strlen("quit"))) //用户输入了quit字符
{
printf ("Client(%s:%d) is exiting!\n", ipv4_addr, ntohs(cin.sin_port));
// break;
}
}
/* 7.关闭文件描述符 */
close(socket_fd);

return 0;
}

/**
* @Function: usage
* @Description: 用户提示信息打印函数
* @param str : 当前应用程序命令字符串,一般是./app
* @return : none
*/
void usage(char *str)
{
printf ("\n%s serv_ip serv_port", str);
printf ("\n\t serv_ip: server ip address");
printf ("\n\t serv_port: server port(>5000)\n\n");
printf ("\n\t Attention: The IP address must be the IP address of the local nic or 0.0.0.0 \n\n");
}

6.2 client客户端

点击查看实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
/** =====================================================
* Copyright © hk. 2022-2022. All rights reserved.
* File name: cilent.c
* Author : fanhua
* Description: client客户端程序——广播(UDP)
* ======================================================
*/
/* 头文件 */
#include <stdio.h> /* perror fgets */
#include <stdlib.h> /* exit atoi*/
#include <errno.h> /* errno号 */
#include <unistd.h> /* write close */

#include <sys/types.h> /* socket recvfrom sendto */
#include <sys/socket.h>/* socket inet_addr recvfrom sendto */
#include <netinet/in.h>/* inet_addr */
#include <arpa/inet.h> /* inet_addr inet_pton htonl*/

#include <string.h> /* bzero strncasecmp strlen */

void usage(char *str); /* 提示信息打印函数 */

/* 主函数 */
int main(int argc, char *argv[])
{
/* 1.参数判断及端口号处理 */
int port = -1;
if (argc != 3)/* 参数数量不对时打印提示信息 */
{
usage(argv[0]);
exit(-1);
}
port = atoi(argv[2]);/* 字符串转数字 */
if (port < 5000)
{
usage(argv[0]);
exit(-1);
}

/* 2.打开套接字,得到套接字描述符 */
int socket_fd = -1; /* 接收服务器端socket描述符 */
if ((socket_fd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
{
perror ("socket");
exit(-1);
}
/* 3.网络属性设置 */
/* 3.1允许广播设置 */
int b_br = 1;
setsockopt(socket_fd, SOL_SOCKET, SO_BROADCAST, &b_br, sizeof(int));

/* 4.连接服务器 */
/* 4.1填充struct sockaddr_in结构体变量 */
struct sockaddr_in sin;
bzero (&sin, sizeof (sin)); /* 将内存块(字符串)的前n个字节清零 */
sin.sin_family = AF_INET; /* 协议族 */
sin.sin_port = htons(port); /* 网络字节序的端口号 */
/* 客户端的 argv[1] 需要与系统的IP一致 */
if(inet_pton(AF_INET, argv[1], (void *)&sin.sin_addr) != 1)/* IP地址 */
{
perror ("inet_pton");
exit(-1);
}
printf("Broadcast client staring...OK(UDP)!\n");
/* 5.数据读写相关变量定义 */
char buf[BUFSIZ]; /* BUFSIZ是在stdio.h中定义的一个宏,值为8192 */
/* 6.数据读写 */
while (1)
{
/* 6.1标准输入获取发送数据 */
bzero(buf, BUFSIZ);
printf(">");
if (fgets(buf, BUFSIZ - 1, stdin) == NULL)
{
perror("fgets");
continue;
}
/* 6.2发送数据 */
if(sendto(socket_fd, buf, strlen(buf), 0, (struct sockaddr *)&sin, sizeof(sin)) < 0)
{
perror("sendto");
continue;
}
/* 6.3判断是否需要退出 */
if (!strncasecmp(buf, "quit", strlen ("quit"))) //用户输入了quit字符
{
printf ("Client is exiting!\n");
break;
}
}
/* 7.关闭文件描述符 */
close(socket_fd);

return 0;
}

/**
* @Function: usage
* @Description: 用户提示信息打印函数
* @param str : 当前应用程序命令字符串,一般是./app
* @return : none
*/
void usage(char *str)
{
printf ("\n%s boardcast_ip serv_port", str);
printf ("\n\t boardcast_ip: boardcast ip address(x.x.x.255 or 255.255.255.255)");
printf ("\n\t serv_port: server port(>5000)\n\n");
}

6.3 Makefile

由于需要生成两个可执行程序,自己输命令有些繁琐,这里使用make来进行。

点击查看 Makefile 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
## =====================================================
# Copyright © hk. 2022-2022. All rights reserved.
# File name: Makefile
# Author : fanhua
# Description: Makefile文件
## ======================================================
#
CC = gcc

DEBUG = -g -O2 -Wall
CFLAGS += $(DEBUG)

# 所有.c文件去掉后缀
TARGET_LIST = ${patsubst %.c, %, ${wildcard *.c}}
all : $(TARGET_LIST)

%.o : %.c
$(CC) $(CFLAGS) -c $< -o $@

.PHONY: all clean clean_o clean_out
clean : clean_o clean_out
@rm -vf $(TARGET_LIST)

clean_o :
@rm -vf *.o

clean_out :
@rm -vf *.out

6.4 测试结果

我们执行以下命令编译链接程序,生成两个可执行文件:

1
make

然后会有如下提示,并生成两个可执行文件吗,分别为服务器端程序server和客户端程序client

1
2
gcc -g -O2 -Wall    client.c   -o client
gcc -g -O2 -Wall server.c -o server

对于服务器端,我们执行以下命令启动服务器进程:

1
./server 0.0.0.0 5001 # 允许监听所有网卡IP及端口

对于客户端,我们执行以下命令启动客户端进程:

1
2
./client 192.168.10.255 5001  # 连接到本地一块网卡的IP
./client 192.168.239.255 5001 # 连接到本地一块网卡的IP

然后我们就会看到如下现象:

image-20220705174101472

二、组播编程

1. 组播的概念

我们知道单播方式只能发给一个接收方。广播方式发给所有的主机,而过多的广播会大量占用网络带宽,造成广播风暴,影响正常的通信。而组播(又称为多播)是一种折中的方式,只有加入某个多播组的主机才能收到数据。

组播指的是报文从一个源发出,被转发到一组特定的接收者,相同的报文在每条链路上最多有一份。相较于传统的单播和广播,组播可以有效地节约网络带宽、降低网络负载,所以被广泛应用于IPTV、实时数据传送和多媒体会议等网络业务中。

组播方式既可以发给多个主机,又能避免象广播那样带来过多的负载(每台主机要到传输层才能判断广播包是否要处理)

2. 组播地址

2.1IPv4组播地址

IANAD类地址空间分配给IPv4组播使用。IPv4地址一共32位,D类地址最高4位为1110,地址范围从224.0.0.0239.255.255.255,具体分类及含义如下:

224.0.0.0 ~ 224.0.0.255永久组地址。IANA为路由协议预留的IP地址(也称为保留组地址),用于标识一组特定的网络设备,供路由协议、拓扑查找等使用,不用于组播转发。
224.0.1.0 ~ 231.255.255.255
233.0.0.0 ~ 238.255.255.255
ASM组地址,全网范围内有效。
232.0.0.0 ~ 232.255.255.255缺省情况下的的SSM组地址范围,全网范围内有效。
239.0.0.0 ~ 239.255.255.255本地管理组地址,仅在本地管理域内有效。在不同的管理域内重复使用相同的本地管理组地址不会导致冲突。

2.2 IPv6组播地址

IPv6地址长度是128位,IPv6组播地址格式如图所示:

image-20220705193345376
  • IPv6组播地址总是以FF开头,高8位取值为1111 1111
  • flags字段(4位)用来标识组播地址的状态。例如取值为0表示保留组地址,取值为12表示ASM范围内的组播地址,取值为3表示SSM范围内的组播地址。
  • scope字段(4位)用来标识组播组的应用范围,指示组播组应用范围是只包含同一本地网络、同一站点、同一机构中的节点,还是包含全球地址空间内的任何节点。
  • Group ID112位)组播组标识符,用在由scope字段所指定的范围内标识组播组。
FF0x::/32保留组地址。
FF1x::/32(x不能是1或者2)
FF2x::/32(x不能是1或者2)
ASM组地址,全网范围内有效。
FF3x::/32(x不能是1或者2)缺省的SSM组地址范围,全网范围内有效。

3. 组播编程步骤

  • 创建用户数据报套接字,注意soclet()函数第二个参数要写为SOCK_DGRAM
  • 接收方地址指定为组播地址。
  • 指定端口信息。
  • 发送数据包。
  • 创建用户数据报套接字,注意soclet()函数第二个参数要写为SOCK_DGRAM
  • 加入多播组。
  • 绑定本机IP地址和端口,绑定的端口必须和发送方指定的端口相同。
  • 等待接收数据。

4. 两个socket选项

这里我们需要将接收端加入到多播组中,所以我们可能会用到两个socket的选项,IP_ADD_MEMBERSHIP*和IP_DROP_MEMBERSHIP,这两个都属于IPPROTO_IP层。

  • IP_ADD_MEMBERSHIP表示加入一个多播组;

  • IP_DROP_MEMBERSHIP表示退出一个多播组;

我们可以使用man 7 ip命令查看帮助手册的Socket options部分来查看这两个选项的详细说明。这两个选项对应的数据类型都是struct ip_mreq类型的结构体变量,该结构体原型如下:

1
2
3
4
5
6
struct ip_mreq
{
struct in_addr imn_multiaddr; /* 加入或者退出的广播组IP地址 */
struct in_addr imr_interface; /* 加入或者退出的网络接口IP地址 */
};

使用示例如下:

1
2
3
4
5
6
#define MULTICAST_IP "235.10.10.3"
struct ip_mreq mreq;
bzero(&mreq, sizeof(mreq));
mreq.imr_multiaddr.s_addr = inet_addr(MULTICAST_IP);
mreq.imr_interface.s_addr = htonl(INADDR_ANY);
setsockopt(socket_fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq));

5. 使用实例

5.1 server服务器端

点击查看实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
/** =====================================================
* Copyright © hk. 2022-2022. All rights reserved.
* File name: server.c
* Author : fanhua
* Description: server服务器端——组播(UDP)
* ======================================================
*/
/* 头文件 */
#include <stdio.h> /* perror */
#include <stdlib.h> /* exit atoi */
#include <errno.h> /* errno号 */
#include <sys/types.h> /* socket bind listen recvfrom sendto */
#include <sys/socket.h> /* socket inet_addr bind listen recvfrom sendto */
#include <strings.h> /* bzero */
#include <arpa/inet.h> /* htons inet_addr inet_pton */
#include <netinet/in.h> /* ntohs inet_addr inet_ntop*/
#include <unistd.h> /* close */
#include <string.h> /* strlen strcat*/

void usage(char *str); /* 提示信息打印函数 */

int main(int argc, char *argv[])
{
/* 1.参数判断及端口号处理 */
int port = -1;
if (argc != 4)/* 参数数量不对时打印提示信息 */
{
usage(argv[0]);
exit(-1);
}
port = atoi(argv[2]);/* 字符串转数字 */
if (port < 5000)
{
usage(argv[0]);
exit(-1);
}
/* 2.创建套接字,得到socket描述符 */
int socket_fd = -1; /* 接收服务器端socket描述符 */
if((socket_fd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)/* SOCK_STREAM,使用TCP协议 */
{
perror ("socket");
exit(-1);
}
/* 3.网络属性设置 */
/* 3.1允许绑定地址快速重用 */
int b_reuse = 1;
setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof (int));
/* 3.2加入多播组 */
struct ip_mreq mreq;
bzero(&mreq, sizeof(mreq));
mreq.imr_multiaddr.s_addr = inet_addr(argv[3]);
mreq.imr_interface.s_addr = inet_addr(argv[1]);
setsockopt(socket_fd, IPPROTO_IP, IP_ADD_MEMBERSHIP,&mreq, sizeof(mreq));

/* 4.将套接字与指定端口号和IP进行绑定 */
/* 4.1填充struct sockaddr_in结构体变量 */
struct sockaddr_in sin;
bzero (&sin, sizeof (sin)); /* 将内存块(字符串)的前n个字节清零 */
sin.sin_family = AF_INET; /* 协议族, IPv4 */
sin.sin_port = htons(port); /* 网络字节序的端口号 */
if(inet_pton(AF_INET, argv[1], (void *)&sin.sin_addr) != 1)/* 填充IP地址,INADDR_ANY表示允许监听任意IP,但是它其实是(in_addr_t) 0x00000000 */
{
perror ("inet_pton");
exit(-1);
}
/* 4.2绑定 */
if(bind(socket_fd, (struct sockaddr *)&sin, sizeof(sin)) < 0)
{
perror("bind");
exit(-1);
}
printf ("Multicast server starting....OK(UDP)!\n");
/* 5.数据读写相关变量定义 */
/* 5.2客户端IP地址及端口号获取的相关变量 */
struct sockaddr_in cin; /* 用于存储成功连接的客户端的IP信息 */
socklen_t addrlen = sizeof(cin);
char ipv4_addr[16];
/* 5.2数据读写相关变量 */
char buf[BUFSIZ]; /* BUFSIZ是在stdio.h中定义的一个宏,值为8192 */
/* 6.数据读写 */
while(1)
{
/* 6.1接收来自客户端的数据 */
bzero(buf, BUFSIZ);
if(recvfrom(socket_fd, buf, BUFSIZ-1, 0,(struct sockaddr *)&cin, &addrlen ) < 0)
{
perror("recvfrom");
continue;
}
/* 6.2打印客户端的IP信息 */
if (!inet_ntop (AF_INET, (void *) &cin.sin_addr, ipv4_addr, sizeof (cin)))
{
perror ("inet_ntop");
exit(-1);
}
printf("Recived from(%s:%d), data:%s", ipv4_addr, ntohs(cin.sin_port), buf);
/* 6.3判断是否需要退出 */
if(!strncasecmp(buf, "quit", strlen("quit"))) //用户输入了quit字符
{
printf ("Client(%s:%d) is exiting!\n", ipv4_addr, ntohs(cin.sin_port));
// break;
}
}
/* 7.关闭文件描述符 */
close(socket_fd);

return 0;
}

/**
* @Function: usage
* @Description: 用户提示信息打印函数
* @param str : 当前应用程序命令字符串,一般是./app
* @return : none
*/
void usage(char *str)
{
printf("\n%s serv_ip serv_port Multicast_ip", str);
printf("\n\t serv_ip: server ip address");
printf("\n\t serv_port: server port(>5000)");
printf("\n\t Multicast_ip: Multicast ip address(between 224~239 segment. eg. 235.10.10.3)\n");
printf("\n\t Attention: The serv_ip address must be the IP address of the local nic or 0.0.0.0 \n\n");
}

5.2 client客户端

点击查看实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
/** =====================================================
* Copyright © hk. 2022-2022. All rights reserved.
* File name: cilent.c
* Author : fanhua
* Description: client客户端程序——组播(UDP)
* ======================================================
*/
/* 头文件 */
#include <stdio.h> /* perror fgets */
#include <stdlib.h> /* exit atoi*/
#include <errno.h> /* errno号 */
#include <unistd.h> /* write close */

#include <sys/types.h> /* socket recvfrom sendto */
#include <sys/socket.h>/* socket inet_addr recvfrom sendto */
#include <netinet/in.h>/* inet_addr */
#include <arpa/inet.h> /* inet_addr inet_pton htonl*/

#include <string.h> /* bzero strncasecmp strlen */

void usage(char *str); /* 提示信息打印函数 */

/* 主函数 */
int main(int argc, char *argv[])
{
/* 1.参数判断及端口号处理 */
int port = -1;
if (argc != 3)/* 参数数量不对时打印提示信息 */
{
usage(argv[0]);
exit(-1);
}
port = atoi(argv[2]);/* 字符串转数字 */
if (port < 5000)
{
usage(argv[0]);
exit(-1);
}

/* 2.打开套接字,得到套接字描述符 */
int socket_fd = -1; /* 接收服务器端socket描述符 */
if ((socket_fd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
{
perror ("socket");
exit(-1);
}
/* 3.网络属性设置 */

/* 4.连接服务器 */
/* 4.1填充struct sockaddr_in结构体变量 */
struct sockaddr_in sin;
bzero (&sin, sizeof (sin)); /* 将内存块(字符串)的前n个字节清零 */
sin.sin_family = AF_INET; /* 协议族 */
sin.sin_port = htons(port); /* 网络字节序的端口号 */
/* 客户端的 argv[1] 需要与系统的IP一致 */
if(inet_pton(AF_INET, argv[1], (void *)&sin.sin_addr) != 1)/* IP地址 */
{
perror ("inet_pton");
exit(-1);
}
printf("Multicast client staring...OK(UDP)!\n");
/* 5.数据读写相关变量定义 */
char buf[BUFSIZ]; /* BUFSIZ是在stdio.h中定义的一个宏,值为8192 */
/* 6.数据读写 */
while (1)
{
/* 6.1标准输入获取发送数据 */
bzero(buf, BUFSIZ);
printf(">");
if (fgets(buf, BUFSIZ - 1, stdin) == NULL)
{
perror("fgets");
continue;
}
/* 6.2发送数据 */
if(sendto(socket_fd, buf, strlen(buf), 0, (struct sockaddr *)&sin, sizeof(sin)) < 0)
{
perror("sendto");
continue;
}
/* 6.3判断是否需要退出 */
if (!strncasecmp(buf, "quit", strlen ("quit"))) //用户输入了quit字符
{
printf ("Client is exiting!\n");
break;
}
}
/* 7.关闭文件描述符 */
close(socket_fd);

return 0;
}

/**
* @Function: usage
* @Description: 用户提示信息打印函数
* @param str : 当前应用程序命令字符串,一般是./app
* @return : none
*/
void usage(char *str)
{
printf ("\n%s Multicast_ip serv_port", str);
printf ("\n\t Multicast_ip: boardcast ip address(between 224~239 segment. eg. 235.10.10.3)");
printf ("\n\t serv_port: server port(>5000)\n\n");
}

5.3 Makefile

由于需要生成两个可执行程序,自己输命令有些繁琐,这里使用make来进行。

点击查看 Makefile 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
## =====================================================
# Copyright © hk. 2022-2022. All rights reserved.
# File name: Makefile
# Author : fanhua
# Description: Makefile文件
## ======================================================
#
CC = gcc

DEBUG = -g -O2 -Wall
CFLAGS += $(DEBUG)

# 所有.c文件去掉后缀
TARGET_LIST = ${patsubst %.c, %, ${wildcard *.c}}
all : $(TARGET_LIST)

%.o : %.c
$(CC) $(CFLAGS) -c $< -o $@

.PHONY: all clean clean_o clean_out
clean : clean_o clean_out
@rm -vf $(TARGET_LIST)

clean_o :
@rm -vf *.o

clean_out :
@rm -vf *.out

5.4 测试结果

我们执行以下命令编译链接程序,生成两个可执行文件:

1
make

然后会有如下提示,并生成两个可执行文件吗,分别为服务器端程序server和客户端程序client

1
2
gcc -g -O2 -Wall    client.c   -o client
gcc -g -O2 -Wall server.c -o server

对于服务器端,我们执行以下命令启动服务器进程:

1
2
./server 0.0.0.0 5001 235.10.10.3 # 允许监听所有网卡IP及端口
./server 0.0.0.0 5001 235.10.10.3 # 允许监听所有网卡IP及端口

对于客户端,我们执行以下命令启动客户端进程:

1
2
./client 235.10.10.3 5001  # 发送数据到组播IP
./client 235.10.10.3 5001 # 发送数据到组播IP

然后我们就会看到如下现象:

image-20220706055721882

这里我们可以七佛那个两个服务器,看会不会都能收到数据,服务器启动时,命令重要加入组播地址,用于让服务器加入多播组,客户端直接想组播地址发送数据即可。