TCI/IP网络编程(7) 多播与广播
应用场景:
假设服务端需要同时向10000个客户端发送同样的通知消息,如果利用TCP,需要维护10000个套接字连接,如果是基于UDP,也需要进行10000数据传输,向大量客户端发送相同的数据,会对服务器端和网络流量产生负面的影响。
1. 多播
多播方式的数据传输是基于UDP完成的,因此其与UDP客户端/服务端的实现方式,区别在于,UDP传输以单一目标地址进行,而多播模式下,数据会同时传递到所有加入注册组的的主机,即采用多播方式的时候,可以同时向多个主机传输相同的数据。
多播的数据传输特点:
- 多播服务器端,对特定的多播组,仅发送1次数据
- 即使只发送一次数据,多播组内的所有客户端都会接收到数据
- 多播组可在IP地址范围内任意增加
- 加入特定组即可接收到发往该多播组的数据
多播组是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; }
运行结果:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· Vue3状态管理终极指南:Pinia保姆级教程