广播与组播
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>
#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[])
{
if(argc != 2)
{
printf("按照格式执行程序:%s <port>\n", argv[0]);
return -1;
}
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(-1 == sockfd)
{
perror("socket failed");
return -1;
}
int on = 1;
setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on));
struct sockaddr_in dest_addr;
socklen_t addrlen = sizeof(dest_addr);
dest_addr.sin_family = AF_INET;
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>
#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[])
{
if(argc != 2)
{
printf("按照格式执行程序:%s <端口号>\n", argv[0]);
return -1;
}
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(-1 == sockfd)
{
perror("socket failed");
return -1;
}
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 = htonl(INADDR_ANY);
if(bind(sockfd, (struct sockaddr *)&recv_addr, addrlen) < 0)
{
perror("bind failed");
return -1;
}
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);
}
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>
#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[])
{
if(argc != 2)
{
printf("按照格式执行程序:%s <port>\n", argv[0]);
return 0;
}
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(-1 == sockfd)
{
perror("socket failed");
return -1;
}
struct ip_mreq mutil_ip;
socklen_t optlen = sizeof(mutil_ip);
inet_pton(AF_INET, "224.0.0.10", &mutil_ip.imr_multiaddr);
inet_pton(AF_INET, "192.168.15.3", &mutil_ip.imr_interface);
setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP,&mutil_ip, optlen);
int on = 1;
setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on));
struct sockaddr_in dest_addr;
socklen_t addrlen = sizeof(dest_addr);
dest_addr.sin_family = AF_INET;
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>
#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[])
{
if(argc != 2)
{
printf("按照格式执行程序:%s <port>\n", argv[0]);
return 0;
}
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(-1 == sockfd)
{
perror("socket failed");
return -1;
}
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 = htonl(INADDR_ANY);
if(bind(sockfd, (struct sockaddr *)&recv_addr, addrlen) < 0)
{
perror("bind failed");
return -1;
}
struct ip_mreq mutil_ip;
socklen_t optlen = sizeof(mutil_ip);
inet_pton(AF_INET, "224.0.0.10", &mutil_ip.imr_multiaddr);
inet_pton(AF_INET, "192.168.15.3", &mutil_ip.imr_interface);
setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP,&mutil_ip, optlen);
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);
}
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>
#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>
int main(int argc, char *argv[])
{
if(argc != 2)
{
printf("按照格式执行程序:%s <port>\n", argv[0]);
return -1;
}
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd < 0)
{
perror("socket failed");
return -1;
}
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 = htonl(INADDR_ANY);
if(bind(sockfd, (struct sockaddr *)&server_addr, addrlen) < 0)
{
perror("bind failed");
return -1;
}
listen(sockfd, 16);
int state;
state = fcntl(sockfd, F_GETFL);
state |= O_NONBLOCK;
fcntl(sockfd, F_SETFL, state);
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
|