1 多播
&emsp 多播(Multicast )方式的数据传输是基于UDP完成的。 因此,与UDP服务器端/客户端的实现方式非常接近。 区别在于, UDP数据传输以单一目标进行,而多播数据同时传递到加入(注册)特定组的大量主机。 换言之,采用多播方式时,可以同时向多个主机传递数据。
1.1 多播的数据传输方式及流量方面的优点
多播的数据传输特点如下:
- 多播服务端针对特定多播组,只发送一次数据。
- 即使只发送一次数据,但该组内的所有客户端都会接收数据。
- 多播组数可在IP地址范围内任意增加。
- 加入特定组即可接收发往该多播的数据。
多播是D类IP地址(224.0.0.0~239.255.255.255),"加入多播组"可以理解为在D类IP地址中,我希望接收发往目标239.234.218.234的多播数据。
多播是基于UDP完成的,也就是说,多播数据包的格式与UDP数据包相同。不同的是,向网络发送一个多播数据包时,路由器将复制该数据包并传递到多个主机。多播需要借助路由器完成,如下图所示:
不要觉得频繁复制同一数据包,会不利于网络流量,因为不会向同一区域发送多个相同的数据包。相当于羊村的100之羊,从很远的地方定了一批相同的订单,使用TCP/UDP需要送100次,但是使用多播,只需要送一次到羊村门口,然后村里驿站复制100份然后给小羊就行。基于这种特性,多播主要用于“多媒体数据的实时传输”。
注意:虽然理论上可以完成多播通信,但不少路由器并不支持多播,或即便支持也因网络拥堵问题故意阻断多播。因此为了在不支持多播的路由器中完成多播通信,也会使用隧道技术。
1.2 路由和TTL,以及加入组的方法
为了传递多播数据包,必须设置TTL(Time to Live),这是决定“数据包传递距离”的主要因素。TTL用整数表示,并且每经过一个路由器就减1,TTL变为0时,该数据包将无法传递被销毁。因此,TTL值设置过大将影响网络流量,设置国小会无法传递到目标,注意。
TTL设置方法是通过此节:套接字可选项完成的。与TTL相关的协议层为IPPROTO_IP
,选项名为IP_MULTICAST_TTL
。具体设置如下:
int send_sock;
int time_live = 64;
...
send_sock = socket(PF_INET, SOCK_DGRAM, 0);
setsockopt(send_sock, IPPROTO, 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(FP_INET, SOCK_DGRAM, 0);
...
join_adr.imr_multiaddr.saddr = "多播组地址信息";
join_adr.imr_interface.saddr = "加入多播组的主机地址信息";
setsockopt(recv_sock, IPPROTO, IP_ADD_MEMBERSHIP, (void*)&join_adr, sizeof(join_adr));
ip_mreq
的结构体定义如下:
struct ip_mreq
{struct in_addr imr_multiaddr;struct in_addr imr_interface;
}
1.3 实现多播Sender 和 Receiver
多播中使用“发送者”和“接收者”替代服务端和客户端,此处Sender是多播数据的发送主体,Receiver是多播数据的发送主体,Receiver需要加入多播组接收数据。下面给出示例,示例背景如下:
- Sender:向AAA组广播文件中保存的新闻消息
- Receiver:接收传递到AAA组的新闻信息
news_sender.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>#define TTL 64
#define BUF_SIZE 30void ErrorHandler(char* message) {fputs(message, stderr);fputc('\n', stderr);exit(1);
}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 <IP><PORT>\n", argv[0]);exit(1);}//多播通信基于UDPsend_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]);mul_adr.sin_port = htons(atoi(argv[2]));//设置TTL信息setsockopt(send_sock, IPPROTO_IP, IP_MULTICAST_TTL, (void*)&time_live, sizeof(time_live));if (fp = fopen("news.txt", "r") == NULL) {ErrorHandler("fopen() error");}//实际传输数据的区域。while (!feof(fp)) {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;
}
news_receiver.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <netinet/in.h>#define BUF_SIZE 30void ErrorHandler(char* message) {fputs(message, stderr);fputc('\n', stderr);exit(1);
}int main(int argc, char* argv[]) {if (argc != 3) {printf("Usage : %s <GroupIP><PORT> \n", argv[0]);exit(1);}int recv_sock;int str_len;char buf[BUF_SIZE];struct sockaddr_in adr;struct ip_mreq join_adr;recv_sock = socket(PF_INET, SOCK_DGRAM, 0);memset(&adr, 0, sizeof(adr));adr.sin_family = AF_INET;adr.sin_addr.s_addr = inet_addr(INADDR_ANY);adr.sin_port = htons(atoi(argv[2]));if (bind(recv_sock, (struct sockaddr*)&adr, sizeof(adr)) == -1) {ErrorHandler("bind error");}//初始化结构体ip_mreqjoin_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) {//不知道传输数据的主机地址信息,所以传0str_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;
}
2 广播
广播与多播都是一次性向多个主机发送数据,但是传输数据的范围有区别。多播即使在跨越不同网络的情况下,只要加入多播组就能接收数据。相反,广播只能向同一网络中的主机传输数据。
2.1 广播的理解及实现方法
广播是向同一网络中所有主机传输数据的方法。广播也是基于UDP完成的,根据传输数据时使用的IP地址的形式,广播分为如下2种。
- 直接广播
- 本地广播
二者在代码上的差别主要在于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网络中的所有主机。
如何实现Sender和Receiver呢?实际上,如果不仔细观察广播示例中通信时使用的IP地址,则很难与UDP示例进行区分。也就是说,数据通信中使用的IP地址是与UDP示例的唯一区别。默认生成的套接字会阻止广播,因此只需要通过如下代码更改默认设置。
int send_sock;
int bcast = 1;
....
send_sock = socket(PF_INET, SOCK_DGRAM, 0);
....
setsockopt(send_sock, SOL_SOCKET, SO_BROADCAST, (void*)&bcast, sizeof(bcast));
....
2.2 实现广播数据的Sender 和 Receiver
new_sender_brd.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>#define BUFF_SIZE 30void ErrorHandler(char* message) {fputs(message, stderr);fputc('\n', stderr);exit(1);
}int main(int argc, char* argv[]) {if (argc != 3) {printf("Usage: %s <Boardcast IP> <PORT>\n", argv[0]);exit(1);}int send_sock = socket(PF_INET, SOCK_DGRAM, 0);struct sockaddr_in broad_addr;memset(&broad_addr, 0, sizeof(broad_addr));broad_addr.sin_family = AF_INET;broad_addr.sin_addr.s_addr = inet_addr(argv[1]);broad_addr.sin_port = htons(atoi(argv[2]));int bcast = 1;setsockopt(send_sock, SOL_SOCKET, SO_BROADCAST, (void*)&bcast, sizeof(bcast));FILE* fp;if((fp = fopen("news.txt", "r")) == NULL) {ErrorHandler("fopen() error");}char buf[BUFF_SIZE];while(!feof(fp)) {fgets(buf, BUFF_SIZE, fp);sendto(send_sock, buf, strlen(buf), 0, (struct sockaddr*)&broad_addr, sizeof(broad_addr));sleep(2);}close(send_sock);return 0;
}
news_receiver_brd.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>#define BUFF_SIZE 30void ErrorHandler(char* message) {fputs(message, stderr);fputc('\n', stderr);exit(1);
}int main(int argc, char* argv[]) {if (argc != 2) {printf("Usage : %s <PORT> \n", argv[0]);exit(1);}int recv_sock;struct sockaddr_in adr;int str_len;char buf[BUFF_SIZE];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) {ErrorHandler("bind error");}while (1) {str_len = recvfrom(recv_sock, buf, BUFF_SIZE-1, 0, NULL, 0);if (str_len < 0) {break;}buf[str_len] = 0;fputs(buf, stdout);}close(recv_sock);return 0;
}
```