IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 网络协议 -> 网络编程3——广播与组播 -> 正文阅读

[网络协议]网络编程3——广播与组播

广播与组播

1、广播

在前面讲的例子里,不管是TCP协议还是UDP协议,都是”单播”, 就是”点对点”的进行通信,如果要对网络里面的所有主机进行通信,实现”点对多”的通信,实现广播通信。

1) 广播发送不是循环给网络中的每一个IP发送数据,而是给网络中一个特定的IP发送信息,这个IP就是广播地址。

2) 只要给广播地址发送数据,那么就会把数据转发到网络中的每一个地址。

3) 广播数据发送只能采用UDP协议

==> 广播地址: 一个网络中最大的那个IP就是广播地址。
在这里插入图片描述
==> 主机IP : 192.168.15.3
==> 广播地址 : 192.168.15.255
==> 网关 : 网络地址 192.168.15.0 --> 默认网关:192.168.15.1

如何实现广播的发送与接收?
在这里插入图片描述

1)广播发送 (广播数据的端口号不一样 – 8888 )

==> 通过sendto函数往当前网络的广播地址(192.168.15.255 / 255.255.255.255)发送数据。
==> UDP套接字默认情况下是关闭了的广播属性的!如果要发送广播,那么就必须先开启广播属性。

2)广播接收

==> 广播数据与单播数据并无区别,只需要往UDP套接字中,调用recvfrom接收UDP数据包即可!

相关API :

setsockopt : 设置套接字属性

SYNOPSIS

   #include <sys/socket.h>

   int setsockopt(int socket, int level, int option_name,
       const void *option_value, socklen_t option_len);

sockfd:套接字
level:优先级
SOL_SOCKET:套接字
IPPROTO_IP:IP优先级
IPPRO_TCP:TCP优先级

optname:选项名字

optval:值,使能为1,不使能为0
optlen:值类型大小

示例代码1:

设计一个广播数据的发送端和接收端代码。
广播数据发送端

#include <stdio.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>

/*广播数据发送端*/
int main(int argc, char *argv[])
{
	//0,规定执行程序格式  ./board_send <port>
	if(argc != 2)
	{
		printf("按照格式执行程序:%s <port>\n", argv[0]);
		return -1;
	}
	
	//1,获取UDP套接字
	int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
	if(-1 == sockfd)
	{
		perror("socket failed");
		return -1;
	}
	
	//2,设置UDP套接字广播属性
	int on = 1;
	setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on));
	
	//3,循环发送广播数据
	struct sockaddr_in  dest_addr;		//接收方的IP地址结构体
	socklen_t addrlen = sizeof(dest_addr);
	
	dest_addr.sin_family = AF_INET;					// 地址族 IPV4
	dest_addr.sin_port = htons( atoi(argv[1]) );	//端口号
	dest_addr.sin_addr.s_addr = inet_addr("255.255.255.255");	//广播地址
	
	
	char buf[1024];
	while(1)
	{
		memset(buf, 0, sizeof(buf));
		fgets(buf, sizeof(buf), stdin);
		sendto(sockfd, buf, sizeof(buf), 0,(struct sockaddr *)&dest_addr, addrlen);
	}
	
	close(sockfd);
	
	return 0;
}

UDP通信接收端

#include <stdio.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>

/*UDP通信接收端*/
int main(int argc, char *argv[])
{
	//0,程序执行格式 ./udp_recv <端口号>
	if(argc != 2)
	{
		printf("按照格式执行程序:%s <端口号>\n", argv[0]);
		return -1;
	}	
	//1,获取UDP套接字 -- socket()
	int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
	if(-1 == sockfd)
	{
		perror("socket failed");
		return -1;
	}	
	//2,绑定IP端口号	-- bind() 
	struct sockaddr_in  recv_addr, send_addr;
	socklen_t addrlen = sizeof(recv_addr);
	
	recv_addr.sin_family = AF_INET;
	recv_addr.sin_port =  htons(atoi(argv[1]));	//把字符串的端口转换成网络字节序端口号
	//recv_addr.sin_addr.s_addr = inet_addr("192.168.15.3");
	recv_addr.sin_addr.s_addr = htonl(INADDR_ANY);	//本机的IP
	
	if(bind(sockfd, (struct sockaddr *)&recv_addr, addrlen) < 0)
	{
		perror("bind failed");
		return -1;
	}
	
	//3,循环接收数据 	-- recvfrom()
	char ip_addr[20] = {0};
	char buf[1024];
	while(1)
	{
		memset(buf, 0, sizeof(buf));
		recvfrom(sockfd, buf, sizeof(buf), 0, (struct sockaddr *)&send_addr, &addrlen);
		inet_ntop(AF_INET, &(send_addr.sin_addr), ip_addr, sizeof(ip_addr));
		printf("IP<%s>:Port<%d>:  %s\n",ip_addr, ntohs(send_addr.sin_port), buf);
	}
	
	//4,关闭UDP套接字	-- close()
	close(sockfd);
	
	return 0;
}

在这里插入图片描述

2、组播

组播是广播和单播之间的折中方案,即将一些IP拉进入一个多播组地址,然后在这个多播组中发送广播信息,组内的IP都能收到信息,没有进入组内的则不能收到信息。

1)IP地址分类

目前IPV4地址分类按照A,B,C,D分类。
A类地址: 1.0.0.1 ~ 126.255.255.255
A类地址:网络地址1字节, 主机地址3字节。 1.0.0.1 ~ 1.255.255.255

B类地址:128.0.0.1 ~ 191.255.255.255
B类地址: 网络地址2字节, 主机地址2字节。

C类地址: 192.0.0.1 ~ 223.255.255.255
C类地址:网络地址3字节,主机地址1字节

D类地址: 224.0.0.1 ~ 239.255.255.255
D类地址:一般不用做主机地址,用来作为组播地址
其他的IP作为保留使用。

2)组播功能实现

·组播发送端
==> 获取UDP套接字
==> 把套接字加入多播组 --> 加入到一个组播地址(D类地址)setsockopt
==> 使能套接字的广播属性
==> 往多播组地址发送数据

·组播接收端
==> 获取UDP套接字
==> bind自己的IP和端口号
==> 把套接字加入多播组 --> 加入到一个组播地址(D类地址)setsockopt
==> 循环接收数据

==> 多播组加入 setsockopt

加入多播组需要使用的参数

struct ip_mreq  {
	struct in_addr imr_multiaddr;	/* IP multicast address of group */	//组地址
	struct in_addr imr_interface;	/* local IP address of interface */	//主机地址
};

==> 示例代码2:

组播发送端代码和组播接收端代码
组播发送端

#include <stdio.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>

/*组播发送端*/
int main(int argc, char *argv[])
{
	//0,执行程序格式 ./mutil_send <port>
	if(argc != 2)
	{
		printf("按照格式执行程序:%s <port>\n", argv[0]);
		return 0;
	}
	
	//1,获取UDP套接字
	int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
	if(-1 == sockfd)
	{
		perror("socket failed");
		return -1;
	}
	
	//2,把套接字拉入多播组
	struct ip_mreq  mutil_ip;
	socklen_t optlen = sizeof(mutil_ip);
	
	inet_pton(AF_INET, "224.0.0.10", &mutil_ip.imr_multiaddr);		//组播地址IP
	inet_pton(AF_INET, "192.168.15.3", &mutil_ip.imr_interface);	//主机地址IP
	
	setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP,&mutil_ip, optlen);	//加入多播组
	
	//3,使能套接字广播属性
	int on = 1;
	setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on));	
	
	//4,往多播组地址发送广播数据
	struct sockaddr_in  dest_addr;		//接收方的IP地址结构体
	socklen_t addrlen = sizeof(dest_addr);
	
	dest_addr.sin_family = AF_INET;					// 地址族 IPV4
	dest_addr.sin_port = htons( atoi(argv[1]) );	//端口号
	dest_addr.sin_addr.s_addr = inet_addr("224.0.0.10");	//组播地址	
	
	char buf[1024];
	while(1)
	{
		memset(buf, 0, sizeof(buf));
		fgets(buf, sizeof(buf), stdin);
		sendto(sockfd, buf, sizeof(buf), 0,(struct sockaddr *)&dest_addr, addrlen);
	}
	
	close(sockfd);
	
	
	
	return 0;
}

组播接收端

#include <stdio.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>

/*组播接收端*/
int main(int argc, char *argv[])
{
	//0,执行程序格式 ./mutil_recv <port>
	if(argc != 2)
	{
		printf("按照格式执行程序:%s <port>\n", argv[0]);
		return 0;
	}	
	
	//1,获取UDP套接字
	int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
	if(-1 == sockfd)
	{
		perror("socket failed");
		return -1;
	}	
	
	//2,bind自己的IP和端口号
	struct sockaddr_in  recv_addr, send_addr;
	socklen_t addrlen = sizeof(recv_addr);
	
	recv_addr.sin_family = AF_INET;
	recv_addr.sin_port =  htons(atoi(argv[1]));	//把字符串的端口转换成网络字节序端口号
	//server_addr.sin_addr.s_addr = inet_addr("192.168.15.3");
	recv_addr.sin_addr.s_addr = htonl(INADDR_ANY);	//本机的IP
	
	if(bind(sockfd, (struct sockaddr *)&recv_addr, addrlen) < 0)
	{
		perror("bind failed");
		return -1;
	}	
	
	//3,把套接字拉入多播组
	struct ip_mreq  mutil_ip;
	socklen_t optlen = sizeof(mutil_ip);
	
	inet_pton(AF_INET, "224.0.0.10", &mutil_ip.imr_multiaddr);		//组播地址IP
	inet_pton(AF_INET, "192.168.15.3", &mutil_ip.imr_interface);	//主机地址IP
	
	setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP,&mutil_ip, optlen);	//加入多播组
		
	//4,循环接收数据
	char ip_addr[20] = {0};
	char buf[1024];
	while(1)
	{
		memset(buf, 0, sizeof(buf));
		recvfrom(sockfd, buf, sizeof(buf), 0, (struct sockaddr *)&send_addr, &addrlen);
		inet_ntop(AF_INET, &(send_addr.sin_addr), ip_addr, sizeof(ip_addr));
		printf("Mutil_Msg IP<%s>:Port<%d>:  %s\n",ip_addr, ntohs(send_addr.sin_port), buf);
	}
	
	//4,关闭UDP套接字	-- close()
	close(sockfd);	
	
	return 0;
}

思考 :一个进程可不可以同时接收单播信息,组播信息,广播信息? 如何区分他们?
==> 可以!
==> 单播,组播,广播其实本质都是UDP数据包,但从接收情况来看,确实不能区分,但是如果想要对他们进行区分,可以自己制定协议,约定单播,组播,广播的端口号, 在接收到数据包的时候,通过判断端口号来确认数据包是属于单播,组播,广播。

网络通信中的四种IO模型

在TCP的server-client模型下,之前的案例都是每次有一个客户端连接服务器就创建一条线程去接收客户端的信息,但是实际项目开发中很少使用这种方案,因为一个服务器可能有成千上万个客户端进行连接,如果每一个客户端都创建一条线程,那么对系统资源会是极大的开销,为了提高资源的使用效率,人们设计的不同的IO模型。
在这里插入图片描述

1、阻塞IO (默认类型)

(1)在Linux下,socket套接字默认是阻塞属性(read/recv/recvfrom : 如果没有数据到达,那么程序会在此阻塞)
(2)绝大部分情况下阻塞类型都是读阻塞(read),极少部分写阻塞(write)

2、非阻塞IO

(1) 在使用非阻塞IO模型时,需要给套接字设置非阻塞类型 --> 当调用 recv/read/recvfrom 函数时,程序不会阻塞,如果有数据到达,那就直接读取出来,如果没有数据到,那就立即返回,不会阻塞。

(2) 在设置非阻塞IO模型的服务器中,如果想要及时获取每客户端的信息,那就需要对每个客户端的会话套接字进行轮询。

3、多路复用

(1) 把多个文件描述符放在同一个文件描述符集合中,设置一个规定的事件对文件描述符集合中所有的文件描述符进行监听,如果在规定的时间内有数据到达文件描述符,集合会保留数据到达的文件描述符 --> 直接读取数据到的这个文件描述符。如果规定的时间内没有数据到达 --> 返回超时。Select()

4、信号驱动 (UDP通信)

(1)文件描述符在有数据到达时会产生一个信号 SIGIO, 设计signal函数去捕捉这个信号,每次捕捉到这个信号就去读取数据,如果没有这个信号,那就不读取数据

(1)非阻塞IO
==> 把服务器端的TCP套接字设置成非阻塞类型, 每次有客户端连接时,就记录会话套接字。
==> 对已连接的会话套接字进行轮询,如果读取到数据,那就把数据打印出来,如果没有读取到数据,那就直接读取下一个会话套接字的内容。

==> 案例:使用非阻塞IO实现服务器端代码(循环接收客户端连接,如果有连接上,那就记录会话ID,如果没人连接,那就去轮询已连接的会话套接字,去读取他们的信息,如此循环)
非阻塞IO模型 – 服务器

#include <stdio.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>

#include <unistd.h>
#include <fcntl.h>

/*非阻塞IO模型 -- 服务器*/
int main(int argc, char *argv[])
{
	//0, 执行程序格式  ./TCP_server <port>
	if(argc != 2)
	{
		printf("按照格式执行程序:%s <port>\n", argv[0]);
		return -1;
	}
	
	//1, 获取TCP套接字
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if(sockfd < 0)
	{
		perror("socket failed");
		return -1;
	}
	
	//2, bind本机IP和端口号
	struct sockaddr_in  server_addr, client_addr;
	socklen_t addrlen = sizeof(server_addr);
	
	server_addr.sin_family = AF_INET;
	server_addr.sin_port =  htons(atoi(argv[1]));	//把字符串的端口转换成网络字节序端口号
	//server_addr.sin_addr.s_addr = inet_addr("192.168.15.3");
	server_addr.sin_addr.s_addr = htonl(INADDR_ANY);	//本机的IP	
	if(bind(sockfd, (struct sockaddr *)&server_addr, addrlen) < 0)
	{
		perror("bind failed");
		return -1;
	}	
	
	//3, 设置监听套接字
	listen(sockfd, 16);
	
	//4,设置监听套接字的非阻塞属性  --> fcntl()
	int state;
	state = fcntl(sockfd, F_GETFL);		//获取套接字默认属性
	state |= O_NONBLOCK;				//默认属性基础上添加非阻塞
	fcntl(sockfd, F_SETFL, state);		//将套接字设置非阻塞
	
	//5,循环接收客户端连接 --> 连接上就记录会话套接字 --> 没有连接上直接返回
	int cli_num = 0;	//记录客户端的个数
	int talkfd[20];		//记录连接的客户端的会话套接字
	
	char ip_addr[20] = {0};
	char buf[1024];
	while(1)
	{
		talkfd[cli_num] = accept(sockfd, (struct sockaddr *)&client_addr, &addrlen);	//不会阻塞
		if(talkfd[cli_num] > 0)	//正确连接
		{
			inet_ntop(AF_INET, &(client_addr.sin_addr), ip_addr, sizeof(ip_addr));
			printf("client ip[%s], port[%d] is connect talkfd[%d]:%d\n", ip_addr, ntohs(client_addr.sin_port), cli_num, talkfd[cli_num]);
						
			//把新连接的客户端的会话套接字设置非阻塞类型
			state = fcntl(talkfd[cli_num], F_GETFL);		//获取套接字默认属性
			state |= O_NONBLOCK;						//默认属性基础上添加非阻塞
			fcntl(talkfd[cli_num], F_SETFL, state);		//将套接字设置非阻塞			
			if(cli_num >= 19)
			{
				printf("客户端已满!\n");
				continue;
			}
			cli_num++;
		}
		//循环接收客户端的信息
		for(int i=0; i<cli_num; i++)
		{
			memset(buf,0, sizeof(buf));
			if(recv(talkfd[i], buf, sizeof(buf), 0) > 0)
			{
				printf("recv[%d]: %s\n",talkfd[i], buf);
			}
			
		}
	}
	
	close(sockfd);
	
	return 0;
}

==> 设置文件描述符非阻塞属性 --> fcntl
SYNOPSIS

   #include <unistd.h>
   #include <fcntl.h>

   int fcntl(int fd, int cmd, ... /* arg */ );

==> fd : 需要设置的文件描述符
==> cmd : 设置的命令
F_SETFL (int) : 设置文件相关属性
F_GETFL (void) :获取文件相关属性
O_APPEND, // 追加属性
O_ASYNC, // 信号触发
O_DIRECT, // 缓冲区写入
O_NOATIME, // 不更新文件修改时间
O_NONBLOCK // 非阻塞
返回值: F_GETFL 参数函数返回 文件描述符的属性
F_SETFL (int) : 成功返回0, 失败返回-1

  网络协议 最新文章
使用Easyswoole 搭建简单的Websoket服务
常见的数据通信方式有哪些?
Openssl 1024bit RSA算法---公私钥获取和处
HTTPS协议的密钥交换流程
《小白WEB安全入门》03. 漏洞篇
HttpRunner4.x 安装与使用
2021-07-04
手写RPC学习笔记
K8S高可用版本部署
mySQL计算IP地址范围
上一篇文章      下一篇文章      查看所有文章
加:2021-08-08 11:53:51  更:2021-08-08 11:54:30 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年12日历 -2024/12/27 14:19:52-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码
数据统计