第14章 多播与广播

14.1多播

多播的数据传输是基于UDP的,区别在于UDP以单一目标,多播可以同时向多个主机传输数据。

1.多播的数据传输方式以及流量方面的优点

特点:

  • 多播服务器端针对特定多播组,只发送一次数据
  • 即使只发送一次数据,但该组内的所有客户端都会接收数据
  • 多播组数可在IP地址范围内任意增加
  • 加入特定组即可接收发往该多播组的数据

多播组是D类IP地址(224.0.0.0~239.255.255.255),加入多播组的客户端套接字,就可以接收该多播组的数据。 多播的数据格式和UDP的相同。只是与一般的UDP数据包不同,向网络传递一个多播数据包时,路由器将复制该数据包到多个主机。

有些路由器不支持多多播,或者为了防止网络拥堵故意阻断了多播,这种情况下也会使用隧道技术(这不需要多播程序开发人员考虑)。

2.路由(Routing)和TTL(Time to Live,生存时间),以及加入组的方法

为了传播多播数据包必须设置TTL,这个会决定数据包的传输距离,TTL用整数表示,每经过一个路由器就会减1,当TTL变为0的时候,该数据包就无法再被传递,只能销毁。

发送端如何设置TTL? 通过前面学的套接字可选项完成(setsockopt),TTL相关的协议层是IPPROTO_IP,选项名为IP_MULTICAST_TTL。

int send_sock;
int time_live=64;
...
send_sock=socket(PF_INET,SOCK_DGRAM,0);//设置UDP套接字
setsockopt(send_sock,IPPROTO_IP,IP_MULTICAST_TTL,(void *)&time_live,sizeof(time_live));

接收端如何加入多播组?

int recv_sock;
struct ip_mreq join_adr;
....
recv_sock=socket(PF_INET,SOCK_DGRAM,0);
....
join_adr.imr_multiaddr.s_addr="多播组信息";
join_adr.imr_interface.s_addr="加入多播组的主机地址信息";
setsockopt(recv_sock,IPPROTO_IP,IP_ADD_MEMBERSHIP,(void *)&join_adr,sizeof(join_adr));
...
struct ip_mreq
{
    struct in_addr imr_multiaddr;
    struct in_addr imr_interface;
}

in_addr结构体不需要多介绍。第一个成员写入加入的组IP地址(也就是多播组的IP地址),第二个成员写入要加入多播组的套接字所属主机IP地址(可以用INADDR_ANY)。

3.实现多播Sender和Receiver

Sender:向AAA组发送这多播消息 Receiver:接收发送到AAA组的多播消息 Sender

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define TTL 64
#define BUF_SIZE 30
void error_handling(char *message);

int main(int argc, char *argv[])
{
	int send_sock;
	struct sockaddr_in mul_adr;
	int time_live=TTL;
	FILE *fp;
	char buf[BUF_SIZE];

	if(argc!=3){
		printf("Usage : %s <GroupIP> <PORT>\n", argv[0]);
		exit(1);
	}
  	
	send_sock=socket(PF_INET, SOCK_DGRAM, 0);
	memset(&mul_adr, 0, sizeof(mul_adr));
	mul_adr.sin_family=AF_INET;
	mul_adr.sin_addr.s_addr=inet_addr(argv[1]);  // Multicast IP,要设为多播地址
	mul_adr.sin_port=htons(atoi(argv[2]));       // Multicast Port目标端口
	
	setsockopt(send_sock, IPPROTO_IP, 
		IP_MULTICAST_TTL, (void*)&time_live, sizeof(time_live));//设置生存时间
	
	if((fp=fopen("news.txt", "r"))==NULL)
		error_handling("fopen() error");

	while(!feof(fp)) //没读到文件结束符就循环
	{
		fgets(buf, BUF_SIZE, fp);
		sendto(send_sock, buf, strlen(buf), 
			0, (struct sockaddr*)&mul_adr, sizeof(mul_adr));//用UDP的发送函数发送数据
		sleep(2);
	}
	fclose(fp);
	close(send_sock);
	return 0;
}

void error_handling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

Receiver

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 30
void error_handling(char *message);

int main(int argc, char *argv[])
{
	int recv_sock;
	int str_len;
	char buf[BUF_SIZE];
	struct sockaddr_in adr;
	struct ip_mreq join_adr;
	
	if(argc!=3) {
		printf("Usage : %s <GroupIP> <PORT>\n", argv[0]);
		exit(1);
	 }
  
	recv_sock=socket(PF_INET, SOCK_DGRAM, 0);
 	memset(&adr, 0, sizeof(adr));
	adr.sin_family=AF_INET;
	adr.sin_addr.s_addr=htonl(INADDR_ANY);	//接收套接字主机IP
	adr.sin_port=htons(atoi(argv[2]));//接收套接字端口
	
	if(bind(recv_sock, (struct sockaddr*) &adr, sizeof(adr))==-1)
		error_handling("bind() error");
	
	join_adr.imr_multiaddr.s_addr=inet_addr(argv[1]);//多播组信息,字符串IP转网络字节序IP
	join_adr.imr_interface.s_addr=htonl(INADDR_ANY);//套接字所属主机IP
  	
	setsockopt(recv_sock, IPPROTO_IP, 
		IP_ADD_MEMBERSHIP, (void*)&join_adr, sizeof(join_adr));//加入多播组
  
	while(1)
	{
		str_len=recvfrom(recv_sock, buf, BUF_SIZE-1, 0, NULL, 0);
		if(str_len<0) 
			break;
		buf[str_len]=0;
		fputs(buf, stdout);
	}
	close(recv_sock);
	return 0;
}

void error_handling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

双方填入的端口号应该保持一致

14.2 广播

1.广播的理解及实现方法

广播是向同一网络中的所有主机传输数据的方法,也是基于UDP完成。 种类:

  • 直接广播
  • 本地广播 两者区别主要在IP地址上。

直接广播:除了网络地址外,主机地址全设为1,例如向网络地址为192.12.34中所有主机传输数据,就可以填写192.12.34.255,换而言之,直接广播可以向特定网络发送广播,即使自己并不属于那个网络。 本地广播: 使用的IP地址只能为255.255.255.255,也就是向本网络所有主机发送数据。

默认的套接字会阻止广播,因此需要设置SO_BROADCAST设为1。

int send_sock;
int bcast = 1;//对变量初始化将SO_BROADCAST的选项信息设为1
...
send_sock = (PF_INET,SOCK_DGRAM,0);
...
setSOCKOPT(send_sock,SOL_SOCKET,SO_BROADCAST,(void *)&bcast,sizeof(bcast));
...

Sender

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 30
void error_handling(char *message);

int main(int argc, char *argv[])
{
	int send_sock;
	struct sockaddr_in broad_adr;
	FILE *fp;
	char buf[BUF_SIZE];
	int so_brd=1;
	
	if(argc!=3) {
		printf("Usage : %s <Boradcast IP> <PORT>\n", argv[0]);
		exit(1);
	}
  
	send_sock=socket(PF_INET, SOCK_DGRAM, 0);	
	memset(&broad_adr, 0, sizeof(broad_adr));
	broad_adr.sin_family=AF_INET;
	broad_adr.sin_addr.s_addr=inet_addr(argv[1]);
	broad_adr.sin_port=htons(atoi(argv[2]));
	
	setsockopt(send_sock, SOL_SOCKET, 
		SO_BROADCAST, (void*)&so_brd, sizeof(so_brd));	//允许广播设置
	if((fp=fopen("news.txt", "r"))==NULL)
		error_handling("fopen() error");

	while(!feof(fp))
	{
		fgets(buf, BUF_SIZE, fp);
		sendto(send_sock, buf, strlen(buf), 
			0, (struct sockaddr*)&broad_adr, sizeof(broad_adr));//发送广播数据
		sleep(2);
	}

	close(send_sock);
	return 0;
}

void error_handling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

Receiver

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 30
void error_handling(char *message);

int main(int argc, char *argv[])
{
	int recv_sock;
	struct sockaddr_in adr;
	int str_len;
	char buf[BUF_SIZE];
	
	if(argc!=2) {
		printf("Usage : %s  <PORT>\n", argv[0]);
		exit(1);
	 }
  
	recv_sock=socket(PF_INET, SOCK_DGRAM, 0);
	
	memset(&adr, 0, sizeof(adr));
	adr.sin_family=AF_INET;
	adr.sin_addr.s_addr=htonl(INADDR_ANY);	
	adr.sin_port=htons(atoi(argv[1]));
	
	if(bind(recv_sock, (struct sockaddr*)&adr, sizeof(adr))==-1)//注册接收套接字IP地址和端口号
		error_handling("bind() error");
  
	while(1)
	{
		str_len=recvfrom(recv_sock, buf, BUF_SIZE-1, 0, NULL, 0);
		if(str_len<0) 
			break;
		buf[str_len]=0;
		fputs(buf, stdout);
	}
	
	close(recv_sock);
	return 0;
}

void error_handling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

14.3 基于Windows的实现(多播)

Sender

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
#include <ws2tcpip.h>     // for IP_MULTICAST_TTL option

#define TTL 64
#define BUF_SIZE 30
void ErrorHandling(char *message);

int main(int argc, char *argv[])
{
	WSADATA wsaData;
	SOCKET hSendSock;
	SOCKADDR_IN mulAdr;
	int timeLive=TTL;
	FILE *fp;
	char buf[BUF_SIZE];

	if(argc!=3) {
		printf("Usage : %s <GroupIP> <PORT>\n", argv[0]);
		exit(1);
	}
	if(WSAStartup(MAKEWORD(2, 2), &wsaData)!=0)//注册套接字版本库
		ErrorHandling("WSAStartup() error!"); 
  	
	hSendSock=socket(PF_INET, SOCK_DGRAM, 0);
	memset(&mulAdr, 0, sizeof(mulAdr));
	mulAdr.sin_family=AF_INET;
	mulAdr.sin_addr.s_addr=inet_addr(argv[1]); 
	mulAdr.sin_port=htons(atoi(argv[2]));      
	
	setsockopt(hSendSock, IPPROTO_IP, 
		IP_MULTICAST_TTL, (void*)&timeLive, sizeof(timeLive));//设置TTL
	
	if((fp=fopen("news.txt", "r"))==NULL)
		ErrorHandling("fopen() error");

	while(!feof(fp))
	{
		fgets(buf, BUF_SIZE, fp);
		sendto(hSendSock, buf, strlen(buf), 
			0, (SOCKADDR*)&mulAdr, sizeof(mulAdr));//发送多播数据
		Sleep(2000);
	}
	closesocket(hSendSock);
	WSACleanup();//注销套接字版本库
	return 0;
}

void ErrorHandling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

Receiver

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
#include <ws2tcpip.h>     // for struct ip_mreq

#define BUF_SIZE 30
void ErrorHandling(char *message);

int main(int argc, char *argv[])
{
	WSADATA wsaData;
	SOCKET hRecvSock;
	SOCKADDR_IN adr;
	struct ip_mreq joinAdr;
	char buf[BUF_SIZE];
	int strLen;


	if(argc!=3) {
		printf("Usage : %s <GroupIP> <PORT>\n", argv[0]);
		exit(1);
	}
	if(WSAStartup(MAKEWORD(2, 2), &wsaData)!=0)//注册套接字版本库
		ErrorHandling("WSAStartup() error!"); 
  
	hRecvSock=socket(PF_INET, SOCK_DGRAM, 0);
 	memset(&adr, 0, sizeof(adr));
	adr.sin_family=AF_INET;
	adr.sin_addr.s_addr=htonl(INADDR_ANY);	
	adr.sin_port=htons(atoi(argv[2]));
	
	if(bind(hRecvSock, (SOCKADDR*) &adr, sizeof(adr))==SOCKET_ERROR)//注册接收套接字的IP和端口号
		ErrorHandling("bind() error");
	
	joinAdr.imr_multiaddr.s_addr=inet_addr(argv[1]);//初始化多播组
	joinAdr.imr_interface.s_addr=htonl(INADDR_ANY);//设置本主机的任意IP
  	
	if(setsockopt(hRecvSock, IPPROTO_IP, IP_ADD_MEMBERSHIP, 
			(void*)&joinAdr, sizeof(joinAdr))==SOCKET_ERROR)//加入多播组
		ErrorHandling("setsock() error");
  
	while(1)
	{
		strLen=recvfrom(hRecvSock, buf, BUF_SIZE-1, 0, NULL, 0);
		if(strLen<0) 
			break;
		buf[strLen]=0;
		fputs(buf, stdout);
	}
	closesocket(hRecvSock);
	WSACleanup();//注销套接字版本库
	return 0;
}

void ErrorHandling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

本文章使用limfx的vscode插件快速发布