TCI/IP网络编程(7) 多播与广播

应用场景:
假设服务端需要同时向10000个客户端发送同样的通知消息,如果利用TCP,需要维护10000个套接字连接,如果是基于UDP,也需要进行10000数据传输,向大量客户端发送相同的数据,会对服务器端和网络流量产生负面的影响。

1. 多播

多播方式的数据传输是基于UDP完成的,因此其与UDP客户端/服务端的实现方式,区别在于,UDP传输以单一目标地址进行,而多播模式下,数据会同时传递到所有加入注册组的的主机,即采用多播方式的时候,可以同时向多个主机传输相同的数据。

多播的数据传输特点:

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

多播组是D类IP地址(244.0.0.0~239.255.255.255),因为多播是借助UDP完成的,因此多播数据包的格式与UDP数据包的格式相同。但是在向网络传递一个多播数据包的时候,需要路由器复制多个数据包,并传输到多个主机,也就是说,多播需要借助路由器来完成。

多播路由示意图
标题多播路由示意图

 

通过多播路由示意图可知,因为路由频繁复制同一数据包,也同样会不利于网络流量,但是从另一方面考虑,多播不会向同一区域发送多个相同的数据包。例如,如果通过TCP或者UDP向10000个用户发送相同的数据包,则共需要传递10000次,即使将所有的主机都合到同一个网络,也是如此。如果使用多播,则只需要发送1次数据包,由10000台主机构成的网络中的路由器负责复制文件并传递到主机,由于这种特性,多播主要用于多媒体数据的实时传输。(如在线视频,直播,这是个人理解)。

在实际的应用中,部分路由器不支持多播,或者即使支持多播,也由于网络拥堵的原因而故意阻断多播,因此为了在不支持多播的路由器中完成多播通信,也会采用隧道技术(不做介绍)。

2. 路由和TTL

为了进行多播数据传播,必须设置TTL(Time to live),TTL是决定数据包传输距离的参数,TTL为整数表示的参数,数据包每经过路由器复制一次,TTL的值就会减1,当TTL的值为0时,数据包就会销毁,不再传输。因此TTL的值需要设置的比较合适,过大则会影响影响网络流量,过小则会导致数据包传输不到目标主机,TTL参数的设置通过套接字可选项完成:

// 服务端send socket设置多播模式以及TTL
int sendSocket;
int time_to_live;
sendSocket = socket(PF_INET, SOCK_DGRAM, 0);
// 设置TTL
setsockopt(send_sock, IPPROTO_IP, IP_MULTICAST_TTL, (void*)&time_to_live, sizeof(time_to_live));

加入多播组也需要通过套接字选项完成:

// 客户端recv socket设置加入组播
int recvSocket;
struct ip_mreq join_addr;
recvSocket = socket(PF_INET, SOCK_DGRAM, 0);
join_addr.imr_multiaddr.s_addr = "多播地址"; // D类地址
join_addr.imr_interface.s_addr = "加入多播组的主机地址"
// 设置主机加入多播组
setsockopt(recvSocket, IPPROTO_IP, IP_ADD_MEMBERSHIP, (void*)&join_addr, sizeof(join_addr));

结构体ip_mreq 的定义如下:

struct ip_mreq
{
struct in_addr imr_multiaddr; // 加入的多播组的IP地址
struct in_addr imr_interface; // 加入多播组的套接字所属的主机IP地址,也可以为INADDR_ANY
}

3.多播的代码实现

Sender发送端代码:

// multicast.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <WinSock2.h>
#include <ws2ipdef.h> // 这个定义需要放在WinSock2的后面
#include <stdio.h>
#pragma comment(lib, "Ws2_32.lib")
#define TTL 32
#define BUFF_SIZE 64
#define MULTI_CAST_ADDR "244.1.1.4"
#define MULTI_CAST_PORT 13000
int main()
{
WSADATA wsaData;
char buffer[BUFF_SIZE];
memset(buffer, 0, BUFF_SIZE);
int timeToLive = TTL;
FILE* fp;
SOCKADDR_IN multicastAddr; // 多播地址
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
printf("Failed to wsaStartUp!\n");
return -1;
}
SOCKET sendSock = socket(PF_INET, SOCK_DGRAM, 0);
if (sendSock == SOCKET_ERROR)
{
printf("Failed to init socket.\n");
WSACleanup();
return -1;
}
// 设置多播地址
memset(&multicastAddr, 0, sizeof(multicastAddr));
multicastAddr.sin_family = AF_INET;
multicastAddr.sin_addr.s_addr = inet_addr(MULTI_CAST_ADDR);
multicastAddr.sin_port = htons(MULTI_CAST_PORT);
// 设置套接字可选项
setsockopt(sendSock, IPPROTO_IP, IP_MULTICAST_TTL, (char*)&timeToLive, sizeof(timeToLive));
// 读取文件
if ((fp = fopen("data.txt", "rb")) == NULL)
{
printf("Failed to open data file.\n");
closesocket(sendSock);
WSACleanup();
return -1;
}
while (!feof(fp))
{
memset(buffer, 0, BUFF_SIZE); // 这里必须每次将缓冲区清空,否则会出现读取的内容出现乱码
//fread(buffer, BUFF_SIZE, 1, fp); // fgets读入文件 fread读取和fgets读取的结果是有差距的
fgets(buffer, BUFF_SIZE, fp);
// buffer[BUFF_SIZE - 1] = 0;
int len = strlen(buffer);
if (len > 0)
{
sendto(sendSock, buffer, len, 0, (SOCKADDR*)&multicastAddr, sizeof(multicastAddr));
printf("Send %d bytes data to the multi cast group.\n", len);
printf("Content: %s\n", buffer);
Sleep(1000); // 延时1s
}
}
fclose(fp);
// 关闭socket
closesocket(sendSock);
WSACleanup();
return 0;
}

Recevier接收端代码:

// receiver.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <WinSock2.h>
#include <ws2ipdef.h> // 这个定义需要放在WinSock2的后面
#include <stdio.h>
#define BUFF_SIZE 64
#define MULTI_CAST_ADDR "244.1.1.4"
#define MULTI_CAST_PORT 13000
#pragma comment(lib, "Ws2_32.lib")
int main()
{
WSADATA wsaData;
char buffer[BUFF_SIZE];
FILE* fp;
SOCKADDR_IN addr; // 接收来自任意地址的报文
struct ip_mreq join_addr;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
printf("Failed to wsaStartUp!\n");
return -1;
}
SOCKET recvSock = socket(PF_INET, SOCK_DGRAM, 0);
if (recvSock == SOCKET_ERROR)
{
printf("Failed to init socket.\n");
WSACleanup();
return -1;
}
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(MULTI_CAST_PORT);
join_addr.imr_multiaddr.s_addr = inet_addr(MULTI_CAST_ADDR);
join_addr.imr_interface.s_addr = htonl(INADDR_ANY);
// 设置套接字选项
setsockopt(recvSock, IPPROTO_IP, IP_ADD_MEMBERSHIP, (char*)&join_addr, sizeof(join_addr));
int res = bind(recvSock, (SOCKADDR*)&addr, sizeof(addr));
if (res == SOCKET_ERROR)
{
printf("Failed to bind the socket.\n");
closesocket(recvSock);
WSACleanup();
return -1;
}
fp = fopen("recevie.txt", "a");
while (true)
{
memset(buffer, 0, BUFF_SIZE);
int len = recvfrom(recvSock, buffer, BUFF_SIZE, 0, NULL, NULL); // 不需要最后的fromAddr和fromLen这两数据
if (len < 0)
break;
printf("Receive %d Bytes: %s\n", len, buffer);
fwrite(buffer, len, 1, fp);
printf("write conten to file.\n");
}
// flush(fp);
fclose(fp);
closesocket(recvSock);
WSACleanup();
return 0;
}

4. 广播

广播也用于一次性向多个主机主机发送数据,与组播相比数据的传输范围有区别。多播即使在跨不同网络的情况下,只要加入了多播组,就能够接收数据,而广播只能向同一网络中的多个主机传输数据。按照广播时使用的IP地址的形式,广播可分为两种:

  • 直接广播 (Direct broadcast)
  • 本地广播 (Local broadcast)

直接广播的IP地址中,除了网络地址外,其余主机地址全部设置为1,可以采用直接广播的方式向特定区域内的所有主机传输数据。例如,需要向网络地址192.12.34中的所有主机传输数据的时候,可以将数据传输到192.12.34.255,此时网络192.12.34中的所有主机都会收到数据。

本地广播使用的IP地址限定为255.255.255.255,如果网络192.12.34中的主机向255.255.255.255传输数据时,网络192.12.34中的所有主机将会收到数据。

广播仅仅需要通过修改UD套接字的可选项即可完成,其余内容与普通UDP通讯完全类似。

5.广播的代码实现

Sender代码示例:

// multicast.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <WinSock2.h>
#include <ws2ipdef.h> // 这个定义需要放在WinSock2的后面
#include <stdio.h>
#pragma comment(lib, "Ws2_32.lib")
#define BUFF_SIZE 32
#define BROADCAST_ADDR "255.255.255.255"
#define BROADCAST_PORT 18500
int main()
{
WSADATA wsaData;
char buffer[BUFF_SIZE];
memset(buffer, 0, BUFF_SIZE);
FILE* fp;
SOCKADDR_IN boradAddr;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
printf("Failed to wsaStartUp!\n");
return -1;
}
SOCKET sendSock = socket(PF_INET, SOCK_DGRAM, 0);
if (sendSock == SOCKET_ERROR)
{
printf("Failed to init socket.\n");
WSACleanup();
return -1;
}
// 设置广播地址
memset(&boradAddr, 0, sizeof(boradAddr));
boradAddr.sin_family = AF_INET;
boradAddr.sin_addr.s_addr = inet_addr(BROADCAST_ADDR);
boradAddr.sin_port = htons(BROADCAST_PORT);
// 设置套接字可选项
int enableBoradcast = 1;
setsockopt(sendSock, SOL_SOCKET, SO_BROADCAST, (char*)&enableBoradcast, sizeof(enableBoradcast)); // 使能SO_BROADCAST
// 读取文件
if ((fp = fopen("data.txt", "rb")) == NULL)
{
printf("Failed to open data file.\n");
closesocket(sendSock);
WSACleanup();
return -1;
}
while (!feof(fp))
{
memset(buffer, 0, BUFF_SIZE); // 这里必须每次将缓冲区清空,否则会出现读取的内容出现乱码
//fread(buffer, BUFF_SIZE, 1, fp); // fgets读入文件 fread读取和fgets读取的结果是有差距的
fgets(buffer, BUFF_SIZE, fp); // fread用于读取按字节读取快数据(结构体),fget用于读取文本文件,遇到换行符结束
// fgets实际每次读取BUFF_SIZE-1个字符,最后一个字符自动添加"\0";
// buffer[BUFF_SIZE - 1] = 0;
int len = strlen(buffer);
if (len > 0)
{
sendto(sendSock, buffer, len, 0, (SOCKADDR*)&boradAddr, sizeof(boradAddr)); // 广播消息
printf("Send %d bytes data to the multi cast group.\n", len);
printf("Content: %s\n", buffer);
Sleep(1000); // 延时1s
}
}
fclose(fp);
// 关闭socket
closesocket(sendSock);
WSACleanup();
return 0;
}

Receiver实例代码:

// receiver.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <WinSock2.h>
#include <ws2ipdef.h> // 这个定义需要放在WinSock2的后面
#include <stdio.h>
#define BUFF_SIZE 32
#define MULTI_CAST_PORT 18500
#pragma comment(lib, "Ws2_32.lib")
int main()
{
WSADATA wsaData;
char buffer[BUFF_SIZE];
FILE* fp;
SOCKADDR_IN addr; // 接收来自任意地址的报文
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
printf("Failed to wsaStartUp!\n");
return -1;
}
SOCKET recvSock = socket(PF_INET, SOCK_DGRAM, 0);
if (recvSock == SOCKET_ERROR)
{
printf("Failed to init socket.\n");
WSACleanup();
return -1;
}
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(MULTI_CAST_PORT);
int res = bind(recvSock, (SOCKADDR*)&addr, sizeof(addr));
if (res == SOCKET_ERROR)
{
printf("Failed to bind the socket.\n");
closesocket(recvSock);
WSACleanup();
return -1;
}
fp = fopen("recevie.txt", "a");
while (true)
{
memset(buffer, 0, BUFF_SIZE);
int len = recvfrom(recvSock, buffer, BUFF_SIZE, 0, NULL, NULL); // 不需要最后的fromAddr和fromLen这两数据
if (len < 0)
break;
printf("Receive %d Bytes: %s\n", len, buffer);
// fwrite(buffer, len, 1, fp);
fputs(buffer, fp);
printf("write conten to file.\n");
}
// flush(fp);
fclose(fp);
closesocket(recvSock);
WSACleanup();
return 0;
}

运行结果:

posted @   Alpha205  阅读(97)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· Vue3状态管理终极指南:Pinia保姆级教程
点击右上角即可分享
微信分享提示