多播
多播( Multicast ) 方式的数据传输是基于UDP完成的。因此,与UDP服务器端/客户端的实现方式非常接近。区别在于, UDP数据传输以单一目标进行,而多播数据同时传递到加入(注册)特定组的大量主机。换言之,采用多播方式时,可以同时向多个主机传递数据。
多播的数据传输方式及流量方面的优点
- 多播服务器端针对特定多播组,只发送1次数据。
- 即使只发送1 次数据,但该组内的所有客户端都会接收数据。
- 多播组数可在IP地址范围内任意增加。
- 加入特定组即可接收发往该多播组的数据。
多播是基于UDP完成的,也就是说, 多播数据包的格式与UDP数据包相同。只是与一般的UDP数据包不同,向网络传递1 个多播数据包时,路由器将复制该数据包并传递到多个主机。像这样,多播需要借助路由器完成,如图所示。
虽然理论上可以完成多播通信,但不少路由器并不支持多播,或即便支持也因网络拥堵问题故意阻断多播。因此,为了在不支持多播的路由器中完成多播通信,也会使用隧道( Tunneling) 技术(这并非多播程序开发人员需要考虑的问题)。
路由( Routing) 和TTL (Time to Live, 生存时间)
TTL是Time to Live的简写,是决定“数据包传递距离”的主要因素。TTL用整数表示,并且每经过1 个路由器就减1 。TTL变为0时,该数据包无法再被传递,只能销毁。因此, TTL的值设置过大将影响网络流量。当然,设置过小也会无法传递到目标,需要引起注意。
程序中的TTL设置是通过套接字可选项完成的。与设置TTL相关的协议层为IPPROTO_IP , 选项名为IP_M口JTICAST_TTL。
套接字可选项可以参考:网编(8):套接字的多种可选项
int send_sock;
int time_live=64;//设置TTL
send_ sock=socket(PF_INET, SOCK_DGRAM, 0);
setsockopt(send_sock, IPPROTO_IP, IP_MULTICAST_TTL, (void*) &time_live, sizeof(time_live));
另外,加入多播组也通过设置套接字选项完成。加入多播组相关的协议层为IPPROTO_IP,选项名为IP_ADD_ MEMBERSHIP 。可通过如下代码加入多播组。
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));
ip_mreq结构体
struct ip_mreq
{
struct in_addr imr_multiaddr;
struct in_addr imr_interface;
}
实现多播Sender 和Receiver
- Sender: 向AAA组广播( Broadcasting ) 文件中保存的新闻信息。
- Receiver: 接收传递到AAA组的新闻信息。
发送端:
#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
//设置TTL
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))/*Broadcasting*/
{
fgets(buf, BUF_SIZE, fp);
//发送数据
sendto(send_sock, buf, strlen(buf), 0, (struct sockaddr*)&mul_adr, sizeof(mul_adr));
sleep(2);
}
fclose(fp);
close(send_sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
接收端:
多播Receiver与普通UDP套接字有些不同。为了接收传向任意多播地址的数据, 需要经过加入多播组的过程。除此之外, Receiver同样与UDP套接字程序差不多。接下来给出与上述示例结合使用的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) ;
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]);
join_adr.imr_interface.s_addr=htonl(INADDR_ANY);
//加入组播
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);
}
运行结果:
#发送端
./send 192.168.43.220 9190
#接收端 //要先运行接收端
$ ./rece 192.168.43.220 9190
aaaaaa
bbbbbb
cccccc
dddddd
eeeeee
eeeeee //最后一次数据要发送两次
$ cat new.txt
aaaaaa
bbbbbb
cccccc
dddddd
eeeeee
广播
本节介绍的广播( Broadcast ) 在“一次性向多个主机发送数据“这一点上与多播类似,但传输数据的范围有区别。多播即使在跨越不同网络的情况下,只要加入多播组就能接收数据。相反,广播只能向同一网络中的主机传输数据。
广播是向同一网络中的所有主机传输数据的方法。与多播相同,广播也是基于UDP完成的。根据传输数据时使用的IP地址的形式,广播分为如下2种。
- 直接广播( Directed Broadcast )
- 本地广播( Local Broadcast )
二者在代码实现上的差别主要在于IP地址。
直接广播的IP地址中除了网络地址外,其余主机地址全部设置为1 。例如,希望向网络地址192.12 .34 中的所有主机传输数据时,可以向
192.12.34.255传输。换言之,可以采用直接广播的方式向特定区域内所有主机传输数据。
本地广播中使用的IP地址限定为255.255.255.255 。例如,192.32.24 网络中的主机向255.255.255.255传输数据时,数据将传递到192.32.24网络中的所有主机。
int send_sock;
int beast= 1; //对变量进行初始化以将SO_BROADCAST 选项信息改为1。
send_sock = socket(PF_INET, SOCK_ DGRAM, 0);
setsockopt(send_sock, SOL_SOCKET, SO_BROADCAST, (void*) & beast, sizeof(bcast));
调用setsockopt函数,将SO_BROADCAST选项设置为beast变量中的值1 。这意味着可以进行数据广播。当然,上述套接字选项只需在Sender 中更改, Receiver的实现不需要该过程。
发送端:
#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 broad_adr;
int so_brd=1;
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(&broad_adr, 0, sizeof(broad_adr));
broad_adr.sin_family=AF_INET;
broad_adr.sin_addr.s_addr=inet_addr(argv[1]); // Multicast IP
broad_adr.sin_port=htons(atoi(argv[2]));//Multicast Port
//设置TTL
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))/*Broadcasting*/
{
fgets(buf, BUF_SIZE, fp);
//发送数据
sendto(send_sock, buf, strlen(buf), 0, (struct sockaddr*)&broad_adr, sizeof(broad_adr));
sleep(2);
}
fclose(fp);
close(send_sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
接收端:
#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;
if(argc!=2) {
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) ;
adr.sin_port=htons(atoi(argv[1]));
//绑定
if(bind(recv_sock, (struct sockaddr*) &adr, sizeof(adr))==-1)
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);
}
运行结果:
#发送端
$ ./send 192.168.43.220 9190 //或者 ./send 255.255.255.255 9190
#接收端 //要先运行接收端
$ ./rece 9190
aaaaaa
bbbbbb
cccccc
dddddd
eeeeee
eeeeee //最后一次数据要发送两次
$ cat new.txt
aaaaaa
bbbbbb
cccccc
dddddd
eeeeee