1. IP
1.1. IP地址
1.2. 网络号和主机号
1.3. IP的分类
IP地址根据网络规模和用途被划分为五类,主要是A、B、C、D、E类 。
特殊说明
- 如127.0.0.1 等效于localhost或本机IP。 一般用于测试使用。
- 每一个字节都为0的地址(0.0.0.0)对应当前主机。
- IP地址中的每一个字节都为1的IP地址(255.255.255.255)是当前子网的广播地址。
- IP地址中凡是以11110开头的E类IP的地址,都保留用于将来和实验使用。
- 网络号的第一个8位不能全为0。
- IP地址不能以127为开头,该类地址中数字127.0.0.1~127.255.255.254用于回路测试
1.4. 私有IP地址
除了公共IP地址外,还有一些地址被保留用于私有网络:
- A类私有地址:10.0.0.0 到 10.255.255.255
- B类私有地址:172.16.0.0 到 172.31.255.255
- C类私有地址:192.168.0.0 到 192.168.255.255
这些私有IP地址在公共互联网上不可路由,适用于家庭、公司等私有网络
1.5. 子网掩码
- 定义:子网掩码用于将IP地址划分为网络号和主机号的部分。它是一个32位的数字,与IP地址采用相同的格式。
- 例子:对于IP地址
192.168.1.1
,常用的子网掩码为255.255.255.0
,这表示网络号为192.168.1
,主机号为1
。
2. 单播
单播(Unicast) 是一种网络通信方式,它指的是在网络中从一个源节点到一个单一目标节点的传输模式。单播传输时,数据包从发送端直接发送到指定接收端,而不是广播给网络中的所有节点或组播给多个指定节点。单播是最常见的网络通信方式,主要应用于日常的互联网通信,如访问网页、发送电子邮件、文件下载等。
举个栗子:
- 当用户访问一个网页时,用户的设备会通过单播请求服务器的网页内容,服务器再单播传输响应给用户设备。
- 发送电子邮件也是一个单播过程:邮件从发送者的邮件服务器传输到接收者的邮件服务器。
2.1 服务端
这里的服务端在Linux下,使用TCP协议
#include <iostream>
#include <string>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <thread>
#include <map>
#define PORT 8080
#define BUFFER_SIZE 1024
std::map<int, int> client_map; // 存储客户端的配对关系
// 处理客户端通信
void handle_client(int client_socket) {
char buffer[BUFFER_SIZE];
int target_client = client_map[client_socket]; // 获取配对的客户端
while (true) {
memset(buffer, 0, sizeof(buffer));
ssize_t bytes_received = recv(client_socket, buffer, BUFFER_SIZE, 0);
if (bytes_received <= 0) {
std::cout << "客户端断开连接。" << std::endl;
close(client_socket);
return;
}
std::cout << "收到消息: " << buffer << std::endl;
// 将消息转发给目标客户端
if (client_map.find(target_client) != client_map.end()) {
send(target_client, buffer, strlen(buffer), 0);
} else {
std::cout << "目标客户端未连接。" << std::endl;
}
}
}
int main() {
int server_socket;
struct sockaddr_in server_addr;
// 创建服务器端套接字
server_socket = socket(AF_INET, SOCK_STREAM, 0);
if (server_socket == 0) {
std::cerr << "套接字创建失败。" << std::endl;
return -1;
}
// 初始化地址结构
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(PORT);
// 绑定地址到套接字
if (bind(server_socket, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
std::cerr << "绑定失败。" << std::endl;
return -1;
}
// 监听客户端连接
if (listen(server_socket, 3) < 0) {
std::cerr << "监听失败。" << std::endl;
return -1;
}
std::cout << "等待客户端连接..." << std::endl;
while (true) {
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
int new_socket = accept(server_socket, (struct sockaddr*)&client_addr, &addr_len);
if (new_socket < 0) {
std::cerr << "连接失败。" << std::endl;
continue;
}
std::cout << "新客户端连接: " << inet_ntoa(client_addr.sin_addr) << std::endl;
// 这里为了简化,我们直接假设是两客户端配对,client_map 存储配对关系
if (client_map.empty()) {
client_map[new_socket] = -1; // 第一个客户端,暂时没有配对
} else {
for (auto& pair : client_map) {
if (pair.second == -1) {
client_map[new_socket] = pair.first; // 第二个客户端与第一个配对
client_map[pair.first] = new_socket;
break;
}
}
}
// 创建线程处理新客户端
std::thread client_thread(handle_client, new_socket);
client_thread.detach(); // 分离线程以便独立运行
}
close(server_socket);
return 0;
}
2.2 客户端
客户端在Windows下,也是TCP协议
#include <iostream> // 引入标准输入输出流库
#include <string> // 引入字符串库
#include <winsock2.h> // 引入Winsock2库,用于网络编程
#include <ws2tcpip.h> // 引入Windows Sockets 2 TCP/IP协议的扩展库
#pragma comment(lib, "ws2_32.lib") // 链接ws2_32.lib库,这是使用Winsock2进行网络编程必需的
int main() {
WSADATA wsaData; // 用于WSAStartup的返回状态信息
SOCKET sock = INVALID_SOCKET; // 声明一个SOCKET对象,并初始化为无效套接字
sockaddr_in server; // 用于存储服务器地址信息的结构体
int result; // 用于存储WSAStartup的返回值
// 初始化Winsock库
result = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (result != 0) {
std::cerr << "WSAStartup失败,错误码: " << result << std::endl;
return 1; // 初始化失败,返回1
}
// 创建一个流式套接字(TCP)
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock == INVALID_SOCKET) {
std::cerr << "套接字创建失败,错误码: " << WSAGetLastError() << std::endl;
WSACleanup(); // 清理Winsock资源
return 1; // 创建失败,返回1
}
// 设置服务器的地址和端口
server.sin_family = AF_INET; // 使用IPv4地址
server.sin_port = htons(8080); // 设置端口号为8081,htons函数用于主机字节序到网络字节序的转换
// 使用inet_pton函数将文本形式的IP地址转换为二进制形式
if (inet_pton(AF_INET, "127.0.0.1", &server.sin_addr) <= 0) {
std::cerr << "无效的地址/地址不受支持" << std::endl;
closesocket(sock); // 关闭套接字
WSACleanup(); // 清理Winsock资源
return 1; // 转换失败,返回1
}
// 连接到服务器
if (connect(sock, (struct sockaddr*)&server, sizeof(server)) < 0) {
std::cerr << "连接失败,错误码: " << WSAGetLastError() << std::endl;
closesocket(sock); // 关闭套接字
WSACleanup(); // 清理Winsock资源
return 1; // 连接失败,返回1
}
// 发送消息到服务器
std::string message = "Hello from client";
send(sock, message.c_str(), message.size(), 0); // 发送字符串到服务器
// 接收来自服务器的响应
char buffer[1024] = { 0 }; // 声明一个缓冲区用于接收数据
int bytesRead = recv(sock, buffer, sizeof(buffer) - 1, 0); // 接收数据
if (bytesRead > 0) {
std::cout << "服务器: " << buffer << std::endl; // 输出接收到的数据
}
else if (bytesRead == 0) {
std::cout << "连接被对方关闭。" << std::endl; // 连接被对方关闭
}
else {
std::cerr << "接收失败,错误码: " << WSAGetLastError() << std::endl; // 接收失败,输出错误码
}
// 关闭套接字
closesocket(sock);
WSACleanup(); // 清理Winsock资源
return 0; // 程序正常结束
}
3. 广播
3.1. 什么是广播
-
-广播(Broadcast) 这种网络通信方式,用于将数据从一个节点传输到同一网络中的所有其他节点。广播数据包会被发送到一个特殊的广播地址,网络中的所有设备都会接收到这个数据包,不论设备是否需要该信息。这种通信方式一般用于局域网(LAN)中的信息共享 。
-
广播并不是所有的网络都支持,通常 广播只是在局域网中,IPv6也不支持广播。在以太网中使用全1的地址,来代表广播地址。例如:255.255.255.255。在广播中,发送端并不指定特定的接收方,而是将数据包发送到该网络中的所有设备。由于广播会广泛传播,在网络中广播的数据通常是诸如网络探测和广告等,这种方式也常被黑客用来进行入侵和攻击。
3.2. 广播的类型
在计算机网络中,广播有两种主要类型:
- 有限广播(Limited Broadcast):
- 使用特殊的IP地址 255.255.255.255。
- 仅在发送数据包的子网中传播,不能跨越路由器。
- 例如,DHCP请求使用有限广播,因为客户端不知道服务器的IP地址。
- 定向广播(Directed Broadcast):
- 使用的是子网广播地址,例如在
192.168.1.0/24
子网中的定向广播地址是192.168.1.255
。 - 定向广播包只会被发送到特定的子网,因此可以跨越一些允许定向广播的路由器。
- 通常用于在特定子网中发送广播信息,而不是整个局域网。
假设有一个网络 192.168.1.0/24
,它的广播地址是 192.168.1.255
,那么向 192.168.1.255
发送的数据包会被该子网中的所有设备接收。
- 如果有设备
A
向192.168.1.255
发送广播包,那么192.168.1.0/24
子网内的所有设备都会接收到该数据包。 - 使用
ping 255.255.255.255
命令(有限广播),可以测试网络中是否有设备响应,常用于网络调试。
3.3. 广播的应用场景
3.4. 广播在不同协议中的使用
- IPv4协议:IPv4中支持广播,有专门的广播地址。
- IPv6协议:IPv6取消了广播功能,代之以组播(Multicast)和任播(Anycast)通信方式,组播比广播更高效且对资源利用更优化。
3.5. 代码示例
下面是一个用C++实现UDP广播的示例代码。这个程序将创建一个UDP套接字,并向局域网内的广播地址发送消息。为了简化示例,代码将消息广播到默认的局域网广播地址(如 255.255.255.255
或某个特定的子网广播地址,比如 192.168.1.255
)。
注意:运行广播代码时,请确保防火墙允许广播包的发送,否则可能会被拦截。另外,这段代码需要在支持BSD套接字的环境中运行,比如Linux或Windows。
#include <iostream>
#include <winsock2.h> // Windows 套接字库
#include <Ws2tcpip.h> // 包含 Ws2tcpip.h 以使用 inet_pton
#pragma comment(lib, "ws2_32.lib") // 自动链接 ws2_32.lib 库
#define BROADCAST_PORT 8888 // 广播端口
#define BROADCAST_IP "255.255.255.255" // 广播地址
int main() {
// 初始化 Winsock 库
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
std::cerr << "Winsock 初始化失败,错误代码:" << WSAGetLastError() << std::endl;
return -1;
}
// 创建 UDP 套接字
SOCKET sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (sockfd == INVALID_SOCKET) {
std::cerr << "创建套接字失败,错误代码:" << WSAGetLastError() << std::endl;
WSACleanup(); // 清理 Winsock 库
return -1;
}
// 设置广播选项
int broadcastEnable = 1; // 启用广播
if (setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, (char*)&broadcastEnable, sizeof(broadcastEnable)) == SOCKET_ERROR) {
std::cerr << "启用广播选项失败,错误代码:" << WSAGetLastError() << std::endl;
closesocket(sockfd);
WSACleanup();
return -1;
}
// 配置广播地址
sockaddr_in broadcastAddr;
memset(&broadcastAddr, 0, sizeof(broadcastAddr)); // 清空结构体
broadcastAddr.sin_family = AF_INET; // 使用 IPv4 地址族
broadcastAddr.sin_port = htons(BROADCAST_PORT); // 设置端口号,使用网络字节序
//broadcastAddr.sin_addr.s_addr = inet_addr(BROADCAST_IP); // 设置广播地址 使用下面的
if (inet_pton(AF_INET, BROADCAST_IP, &broadcastAddr.sin_addr) <= 0) {
std::cerr << "IP 地址转换失败" << std::endl;
closesocket(sockfd);
WSACleanup();
return -1;
}
// 要发送的广播消息
const char* message = "Hello, this is a broadcast message!";
// 发送广播消息
int sendResult = sendto(sockfd, message, strlen(message), 0, (sockaddr*)&broadcastAddr, sizeof(broadcastAddr));
if (sendResult == SOCKET_ERROR) {
std::cerr << "广播消息发送失败,错误代码:" << WSAGetLastError() << std::endl;
} else {
std::cout << "广播消息已发送: " << message << std::endl;
}
// 关闭套接字
closesocket(sockfd);
// 清理 Winsock 库
WSACleanup();
return 0;
}
3.5.2.1. 上面的代码说明和解释
- 初始化 Winsock 库:
- 在 Windows 中使用套接字需要先初始化 Winsock 库。
WSAStartup
函数用于初始化 Winsock 库,这里使用的版本是 2.2。
WSAStartup(MAKEWORD(2, 2), &wsaData);
- 创建 UDP 套接字:
socket()
函数用于创建一个套接字,这里指定AF_INET
表示 IPv4,SOCK_DGRAM
表示 UDP 套接字。IPPROTO_UDP
表示使用 UDP 协议。
SOCKET sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
- 启用广播选项:
setsockopt
函数用于配置套接字选项。- 其中
SOL_SOCKET
表示套接字层选项,SO_BROADCAST
启用广播。 broadcastEnable
设置为 1 表示启用广播。
int broadcastEnable = 1;
setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, (char*)&broadcastEnable, sizeof(broadcastEnable));
- 配置广播地址:
broadcastAddr
是存储目标地址的结构体。sin_family
设置为AF_INET
,表示使用 IPv4。sin_port
使用htons
将端口号转换为网络字节序。sin_addr.s_addr
设置广播地址,使用inet_addr
将字符串形式的 IP 地址转换为网络字节序。
sockaddr_in broadcastAddr;
broadcastAddr.sin_family = AF_INET;
broadcastAddr.sin_port = htons(BROADCAST_PORT);
broadcastAddr.sin_addr.s_addr = inet_addr(BROADCAST_IP);
-
发送广播消息:
sendto
用于发送 UDP 数据。sockfd
是套接字描述符。message
是要发送的消息,strlen(message)
指定消息长度。broadcastAddr
是广播地址,sizeof(broadcastAddr)
指定结构体大小。
-
关闭套接字并清理 Winsock:
closesocket
关闭套接字以释放资源。WSACleanup
用于清理 Winsock 库,释放所有与 Winsock 相关的资源。
closesocket(sockfd);
WSACleanup();
3.5.2.2. 注意事项
- 防火墙:如果广播消息无法发送或接收,检查系统防火墙是否允许UDP广播包。
- 管理员权限:在 Windows 中发送广播消息可能需要管理员权限。
3.6. 运行与测试
抓包后得到:
4. 组播
4.1. 什么是组播多播?
组播和多播实际上是同一个概念,两者只是翻译上的差异。组播(或多播)也是一种网络通信方法,允许数据只发送一次,但可以由多台主机接收。其主要应用于需要多个接收方实时同步数据的场景,比如在线视频直播、网络游戏、股票行情等。
组播通常用于局域网,因为许多路由器和网络设备(特别是家用和小型网络设备)不支持跨互联网的组播路由。此外,组播的管理和传输在局域网中更为高效和可控。不过,大型企业或互联网服务提供商可能在其内部网络中使用组播,以实现高效的数据分发。
4.2. 组播的原理
具体来说,在一个局域网中的组播过程如下:
- 假设局域网内有5台主机:A、B、C、D、E。
- 通过分配一个组播地址和端口(比如
239.255.0.1:8888
)来定义组。 - 现在,如果主机 A、B 和 C 加入了这个组播组(即订阅了
239.255.0.1:8888
的组播消息),那么 D 和 E 不会收到该组播消息,因为它们没有加入该组。 - 当 A 发送一条组播消息到
239.255.0.1:8888
时,B 和 C 会接收这条消息,而 D 和 E 不会收到,因为组播只会将数据发给加入了该组播地址的主机。 - 组播的IP和端口是程序自己随意选择的(避开常用端口,要大于1024),只要在239.0.0.0 到 239.255.255.255这个范围内就可以
4.3. 组播的实现代码示例
以下是一个在局域网内实现组播的 C++ 代码示例,包含发送端和接收端代码,并附有详细的中文注释
4.3.1. 发送端代码(组播发送 Windows版)
- 初始化
WSAStartup
:启用 Windows Sockets 库,准备使用套接字。 - 创建UDP套接字:使用
socket()
创建一个 UDP 套接字(SOCK_DGRAM)。 - 设置组播地址和端口:配置
sockaddr_in
结构,设置组播地址239.255.0.1
和端口8888
。 - 设置组播TTL(生存时间):
IP_MULTICAST_TTL
选项设置组播消息的传输范围,ttl = 1
限制在本地局域网内传输。 - 发送消息:通过
sendto
发送组播消息。 - 关闭套接字:关闭套接字,清理 Winsock 环境。
#include <iostream>
#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
int main() {
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
std::cerr << "WSAStartup failed." << std::endl;
return -1;
}
// 创建UDP套接字
//第三个参数可以直接写0。
SOCKET sendSock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (sendSock == INVALID_SOCKET) {
std::cerr << "Socket creation failed." << std::endl;
WSACleanup();
return -1;
}
// 设置组播地址和端口
sockaddr_in multicastAddr;
multicastAddr.sin_family = AF_INET;
multicastAddr.sin_addr.s_addr = inet_addr("239.255.0.1"); // 组播地址
multicastAddr.sin_port = htons(8888); // 组播端口
// 设置套接字为可广播,以确保数据可以通过组播地址发送
int ttl = 1;
setsockopt(sendSock, IPPROTO_IP, IP_MULTICAST_TTL, (char*)&ttl, sizeof(ttl));
// 发送组播消息
const char* message = "Hello, multicast!";
int sendResult = sendto(sendSock, message, strlen(message), 0, (sockaddr*)&multicastAddr, sizeof(multicastAddr));
if (sendResult == SOCKET_ERROR) {
std::cerr << "Failed to send message." << std::endl;
closesocket(sendSock);
WSACleanup();
return -1;
}
std::cout << "组播消息已发送: " << message << std::endl;
// 关闭套接字
closesocket(sendSock);
WSACleanup();
return 0;
}
4.3.2. 接收端代码(组播接收 Windows版)
- 初始化
WSAStartup
:启用 Winsock 库。 - 创建UDP套接字:使用
socket()
创建一个 UDP 套接字。 - 绑定端口:将套接字绑定到端口
8888
,接收来自所有网络接口的数据。 - 加入组播组:使用
setsockopt()
选项IP_ADD_MEMBERSHIP
将本机加入到组播地址239.255.0.1
中。 - 接收组播消息:使用
recvfrom()
接收来自组播地址的消息。 - 退出组播组:调用
setsockopt()
选项IP_DROP_MEMBERSHIP
,从组播组中退出。 - 关闭套接字:关闭套接字并清理 Winsock。
#include <iostream>
#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
int main() {
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
std::cerr << "WSAStartup failed." << std::endl;
return -1;
}
// 创建UDP套接字
SOCKET recvSock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (recvSock == INVALID_SOCKET) {
std::cerr << "Socket creation failed." << std::endl;
WSACleanup();
return -1;
}
// 绑定到组播端口
sockaddr_in recvAddr;
recvAddr.sin_family = AF_INET;
recvAddr.sin_addr.s_addr = INADDR_ANY; // 监听所有本地接口
recvAddr.sin_port = htons(8888); // 组播端口
if (bind(recvSock, (sockaddr*)&recvAddr, sizeof(recvAddr)) == SOCKET_ERROR) {
std::cerr << "Binding failed." << std::endl;
closesocket(recvSock);
WSACleanup();
return -1;
}
// 加入组播组
ip_mreq multicastRequest;
multicastRequest.imr_multiaddr.s_addr = inet_addr("239.255.0.1"); // 组播地址
multicastRequest.imr_interface.s_addr = INADDR_ANY; // 本地任意接口
if (setsockopt(recvSock, IPPROTO_IP, IP_ADD_MEMBERSHIP, (char*)&multicastRequest, sizeof(multicastRequest)) < 0) {
std::cerr << "Failed to join multicast group." << std::endl;
closesocket(recvSock);
WSACleanup();
return -1;
}
// 接收组播消息
char recvBuffer[1024];
sockaddr_in senderAddr;
int senderAddrSize = sizeof(senderAddr);
int recvLen = recvfrom(recvSock, recvBuffer, sizeof(recvBuffer) - 1, 0, (sockaddr*)&senderAddr, &senderAddrSize);
if (recvLen > 0) {
recvBuffer[recvLen] = '\0';
std::cout << "收到组播消息: " << recvBuffer << std::endl;
} else {
std::cerr << "接收失败" << std::endl;
}
// 退出组播组
setsockopt(recvSock, IPPROTO_IP, IP_DROP_MEMBERSHIP, (char*)&multicastRequest, sizeof(multicastRequest));
// 关闭套接字
closesocket(recvSock);
WSACleanup();
return 0;
}
4.3.3. 注意事项和重点函数参数说明
- 组播地址选择:在局域网中,
239.0.0.0
到239.255.255.255
是专用组播地址,通常用于本地组播。 - TTL 设置:通过设置 TTL(Time-To-Live)为 1,可确保组播消息不会被路由器转发到其他网段。
int ttl = 1;
setsockopt(sendSock, IPPROTO_IP, IP_MULTICAST_TTL, (char*)&ttl, sizeof(ttl));
IPPROTO_IP
:- 这个参数指定了将要设置的选项所属的协议级别。
IPPROTO_IP
表示这是IPv4协议级别的选项。对于IPv6,相应的常量是IPPROTO_IPV6
。
- 这个参数指定了将要设置的选项所属的协议级别。
IP_MULTICAST_TTL
:- 这是要设置的选项名。
IP_MULTICAST_TTL
用于指定多播数据包的生存时间(TTL)。TTL定义了数据包在网络中可以经过的最大路由器数(或跳数)。每当数据包经过一个路由器时,其TTL值减1,当TTL值减到0时,数据包被丢弃。
- 这是要设置的选项名。
if (setsockopt(sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, &multicast_request, sizeof(multicast_request)) < 0) {
std::cerr << "无法加入组播组" << std::endl;
close(sock);
return -1;
}
- sock:套接字描述符(socket descriptor),是一个指向已打开套接字的引用,用于网络通信。
- IPPROTO_IP:指定选项所在的协议层。在这个例子中,它指定了IPv4协议层,因为我们在处理多播组,这是基于IP层的特性。
- IP_ADD_MEMBERSHIP:这是要设置的选项的名称。它告诉系统,我们想要将一个套接字加入到一个多播组中。
- &multicast_request:指向一个ip_mreq结构的指针,该结构包含了多播组的地址以及本地接口的地址(或者是一个特殊的值来表示所有接口)。这个结构定义了套接字将要加入的多播组。
- sizeof(multicast_request):指定multicast_request结构的大小,以字节为单位。
ip_mreq结构通常定义如下(在<netinet/in.h>或类似头文件中)
struct ip_mreq {
struct in_addr imr_multiaddr; // 多播组的IP地址
struct in_addr imr_interface; // 本地接口的IP地址,或INADDR_ANY表示所有接口
};
4.3.4. 发送方代码 (组播发送 Linux 版本)
发送方将消息发送到指定的组播 IP 地址和端口。
#include <iostream>
#include <cstring> // 用于 memset
#include <arpa/inet.h> // 用于 socket 相关函数
#include <unistd.h> // 用于 close
int main() {
// 组播地址和端口
const char* multicast_ip = "239.0.0.1";
const int multicast_port = 12345;
// 创建 UDP socket
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0) {
std::cerr << "无法创建 socket" << std::endl;
return -1;
}
// 设置组播地址结构体
struct sockaddr_in multicast_addr;
memset(&multicast_addr, 0, sizeof(multicast_addr));
multicast_addr.sin_family = AF_INET;
multicast_addr.sin_addr.s_addr = inet_addr(multicast_ip); // 设置组播 IP 地址
multicast_addr.sin_port = htons(multicast_port); // 设置组播端口
// 要发送的消息
const char* message = "这是一个组播消息";
// 发送消息到组播地址
if (sendto(sock, message, strlen(message), 0,
(struct sockaddr*)&multicast_addr, sizeof(multicast_addr)) < 0) {
std::cerr << "发送失败" << std::endl;
close(sock);
return -1;
}
std::cout << "组播消息已发送: " << message << std::endl;
// 关闭 socket
close(sock);
return 0;
}
4.3.5. 接收方代码 (组播接收 Linux版本)
接收方加入组播组,接收来自指定组播地址和端口的消息。
#include <iostream>
#include <cstring> // 用于 memset
#include <arpa/inet.h> // 用于 socket 相关函数
#include <unistd.h> // 用于 close
#include <netinet/in.h> // 用于 IPPROTO_IP 和 ip_mreq
int main() {
// 组播地址和端口
const char* multicast_ip = "239.0.0.1";
const int multicast_port = 12345;
// 创建 UDP socket
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0) {
std::cerr << "无法创建 socket" << std::endl;
return -1;
}
// 绑定接收地址和端口
struct sockaddr_in local_addr;
memset(&local_addr, 0, sizeof(local_addr));
local_addr.sin_family = AF_INET;
local_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定到所有本地地址
local_addr.sin_port = htons(multicast_port); // 指定组播端口
if (bind(sock, (struct sockaddr*)&local_addr, sizeof(local_addr)) < 0) {
std::cerr << "绑定失败" << std::endl;
close(sock);
return -1;
}
// 设置组播组
struct ip_mreq multicast_request;
multicast_request.imr_multiaddr.s_addr = inet_addr(multicast_ip); // 组播地址
multicast_request.imr_interface.s_addr = htonl(INADDR_ANY); // 使用默认网络接口
// 加入组播组
if (setsockopt(sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, &multicast_request, sizeof(multicast_request)) < 0) {
std::cerr << "无法加入组播组" << std::endl;
close(sock);
return -1;
}
// 接收消息
char buffer[256];
while (true) {
memset(buffer, 0, sizeof(buffer));
int len = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, nullptr, nullptr);
if (len < 0) {
std::cerr << "接收失败" << std::endl;
break;
}
buffer[len] = '\0';
std::cout << "收到组播消息: " << buffer << std::endl;
}
// 离开组播组
setsockopt(sock, IPPROTO_IP, IP_DROP_MEMBERSHIP, &multicast_request, sizeof(multicast_request));
// 关闭 socket
close(sock);
return 0;
}
为了将您的代码适配到IPv6环境,我们需要对几个关键部分进行修改。这包括创建套接字时使用的地址族(AF_INET6
替代 AF_INET
)、套接字地址结构(sockaddr_in6
替代 sockaddr_in
)、以及设置多播地址的方式。以下是修改后的代码:
#include <iostream>
#include <cstring> // 用于 memset
#include <arpa/inet.h> // 用于 inet_pton, inet_ntop 等函数(注意:某些系统上可能是 <netinet/in.h>)
#include <sys/socket.h> // 用于 socket, sendto 等函数
#include <unistd.h> // 用于 close
#include <netinet/icmp6.h> // 如果需要处理 ICMPv6,但本例中不需要
int main() {
// 组播地址和端口(IPv6 地址使用十六进制表示,并用冒号分隔)
const char* multicast_ip = "ff02::1"; // IPv6 的链路本地多播地址
const int multicast_port = 12345;
// 创建 UDP socket(使用 IPv6)
int sock = socket(AF_INET6, SOCK_DGRAM, 0);
if (sock < 0) {
std::cerr << "无法创建 socket" << std::endl;
return -1;
}
// 设置组播地址结构体(IPv6)
struct sockaddr_in6 multicast_addr;
memset(&multicast_addr, 0, sizeof(multicast_addr));
multicast_addr.sin6_family = AF_INET6;
// 使用 inet_pton 将 IPv6 地址从文本转换为二进制形式
if (inet_pton(AF_INET6, multicast_ip, &multicast_addr.sin6_addr) <= 0) {
std::cerr << "无效的 IPv6 地址" << std::endl;
close(sock);
return -1;
}
multicast_addr.sin6_port = htons(multicast_port); // 设置组播端口
// 对于 IPv6 多播,通常不需要设置 sin6_scope_id,除非你有特定的网络接口要求
// 要发送的消息
const char* message = "这是一个 IPv6 组播消息";
// 发送消息到组播地址
if (sendto(sock, message, strlen(message), 0,
(struct sockaddr*)&multicast_addr, sizeof(multicast_addr)) < 0) {
std::cerr << "发送失败" << std::endl;
close(sock);
return -1;
}
std::cout << "IPv6 组播消息已发送: " << message << std::endl;
// 关闭 socket
close(sock);
return 0;
}
4.4.1. 注意事项:
- IPv6 地址:IPv6 地址使用十六进制表示,并由冒号分隔。在这个例子中,我们使用了链路本地多播地址
ff02::1
,但根据您的需求,您可能需要使用其他类型的 IPv6 多播地址。 - inet_pton:这个函数用于将点分十进制的 IPv4 地址或冒分十六进制的 IPv6 地址转换为网络字节序的二进制形式。对于 IPv6,我们使用
AF_INET6
作为第一个参数。 - sin6_scope_id:在 IPv6 中,多播地址可能有一个“作用域 ID”(scope ID),它指定了多播消息应该在哪个网络接口或链路上发送。对于大多数应用程序来说,可以将其设置为 0,让系统选择默认接口。但是,如果您有特定的网络接口要求,您可能需要设置这个字段。
- 头文件:请确保包含了正确的头文件。在某些系统上,IPv6 相关的函数和类型可能定义在
<netinet/in.h>
或其他头文件中,但<arpa/inet.h>
通常包含了inet_pton
和inet_ntop
。此外,对于 ICMPv6 处理,您可能需要包含<netinet/icmp6.h>
,但在这个简单的发送例子中并不需要。 - 错误处理:在实际应用中,您可能希望添加更详细的错误处理逻辑,以便在出现问题时能够更准确地诊断问题所在。
5. 任播
5.1. 什么是任播
任播(Anycast)是指多个服务器或节点共享一个IP地址,通过网络协议中的路由机制,用户的请求自动选择距离最近或最优的服务器进行响应。与传统的单播(Unicast)和组播(Multicast)不同,任播的核心在于“发送到最近的一个”。
任播与组播的区别:在组播中,消息会被发送到一组所有订阅该IP地址的节点。但在任播中,消息只会到达同一IP地址下的一个最优节点。
- 任播可以使用任何可路由的IP地址,通常是常规的单播地址(A类、B类或C类IP地址),而不是D类地址。
- 任播实现的关键在于多个主机共享同一单播IP地址,通过路由协议(例如BGP或OSPF)来决定最近或最佳路径,而不是通过使用特殊的IP地址类别来实现。
5.2. 任播的原理
5.3. 任播最优路由的决策过程
任播的最优路由通常由BGP(边界网关协议)或OSPF(开放式最短路径优先协议)等网络路由协议来实现。
以下是最优路由的决策过程:
- 地理距离和路径开销:在同一任播IP地址的情况下,路由器根据用户的地理位置选择最短路径或最低延迟的路径,以减少数据传输时间。
- 网络延迟:当路径开销类似时,路由器会优先选择网络延迟较低的路径。网络延迟可以通过探测或协议交换获得。
- 路径稳定性:路由协议会优先选择路径稳定性较高的节点。路径的不稳定性可能会导致路由频繁更改,从而增加网络开销。
- 路由协议的计算与更新:在BGP等动态路由协议中,节点定期交换路由信息,一旦有节点发生变化(如故障、延迟增加),路由表会根据新的路由条件进行更新。
- 负载平衡机制:如果多个节点的延迟相似,路由器会分配请求给不同节点,以实现负载平衡。这种策略也用于避免某个节点出现瓶颈。
举个例子:
假设你在中国访问Google的公共DNS服务器(如IP地址 8.8.8.8
)。Google在全球多个地点(美国、欧洲、亚洲等)部署了多个共享这个IP地址的DNS服务器,这些服务器都配置为任播节点。
- 当你请求访问
8.8.8.8
时,路由器会根据你的地理位置和当前的网络状况,选择距离最近或响应最快的服务器节点来处理请求。例如,你的请求会被引导到位于亚洲的一个Google DNS服务器,而不是到美国或欧洲的服务器。 - 这样做的好处是降低了请求延迟,用户可以以更快的速度获得DNS解析结果,同时,如果亚洲的服务器出现故障,请求会自动转向其他位置的服务器,确保服务的高可用性。
也就是说:在互联网中的任播应用,通过全局BGP协议实现,将全球用户的请求引导至最近的服务器节点
再举一个例子:
- 主机A在北京,而主机B在石家庄、C和D在上海、E在硅谷。所有节点BCDE都监听同一个任播IP地址
239.255.255.250
和端口8080
。 - 当A发送UDP消息到任播地址
239.255.255.250:8080
时,路由协议会选择一个最优路径,将消息转发给其中距离最近的一个节点,而不是多个节点。
5.4. 代码示例
5.4.1. 场景设定
- 假设网络已经配置支持任播。
- 发送方主机A位于北京,向任播地址
192.168.2.1
发送消息,端口为8080
。 - 接收方主机(B、C、D、E)都监听在`192.168.2.1:8080上。
我们可以使用UDP套接字实现该功能,并且需要网络配置支持任播路径选择,以确保消息能正确到达距离最近的节点。
5.4.2. 发送方代码(主机A) - sender.cpp
发送方会向任播IP地址192.168.2.1
的端口8080
发送UDP消息。
#include <iostream>
#include <cstring>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
#define ANYCAST_IP "192.168.2.1" // 任播IP地址
#define PORT 8080 // 端口号
int main() {
int sockfd;
struct sockaddr_in servaddr;
// 创建UDP套接字
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("Socket creation failed");
return -1;
}
// 设置目标地址
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(PORT);
if (inet_pton(AF_INET, ANYCAST_IP, &servaddr.sin_addr) <= 0) {
perror("Invalid address/ Address not supported");
close(sockfd);
return -1;
}
// 准备消息内容
const char *message = "Hello from A (Beijing)";
int msg_len = strlen(message);
// 发送消息
if (sendto(sockfd, message, msg_len, 0, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) {
perror("Send failed");
close(sockfd);
return -1;
}
std::cout << "Message sent to " << ANYCAST_IP << ":" << PORT << std::endl;
// 关闭套接字
close(sockfd);
return 0;
}
5.4.3. 接收方代码(主机B/C/D/E) - receiver.cpp
接收方监听在任播IP地址192.168.2.1
和端口8080
上,等待来自发送方的UDP消息
#include <iostream>
#include <cstring>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#define ANYCAST_IP "192.168.2.1" // 任播IP地址
#define PORT 8080 // 端口号
int main() {
int sockfd;
struct sockaddr_in servaddr, clientaddr;
// 创建UDP套接字
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("Socket creation failed");
return -1;
}
// 设置地址复用选项,允许多个节点绑定到相同的端口
int opt = 1;
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {
perror("Setting SO_REUSEADDR failed");
close(sockfd);
return -1;
}
// 设置服务端地址
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(PORT);
servaddr.sin_addr.s_addr = inet_addr(ANYCAST_IP);
// 绑定套接字到任播地址
if (bind(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) {
perror("Bind failed");
close(sockfd);
return -1;
}
// 接收数据
char buffer[1024];
socklen_t len = sizeof(clientaddr);
std::cout << "Receiver listening on " << ANYCAST_IP << ":" << PORT << std::endl;
while (true) {
int n = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&clientaddr, &len);
if (n < 0) {
perror("Receive failed");
break;
}
buffer[n] = '\0';
std::cout << "Received message: " << buffer << " from "
<< inet_ntoa(clientaddr.sin_addr) << ":" << ntohs(clientaddr.sin_port) << std::endl;
}
// 关闭套接字
close(sockfd);
return 0;
}
5.4.4. 代码说明 和重要函数参数
- 发送方:通过UDP套接字向任播地址
192.168.2.1:8080
发送消息。 - 接收方:通过UDP套接字绑定到相同的任播地址和端口号
192.168.2.1:8080
,监听并接收消息。
- 在多个接收方节点(如B、C、D、E)上运行
receiver.cpp
,并确保网络配置支持任播。 - 在发送方节点A上运行
sender.cpp
发送消息。
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {
perror("Setting SO_REUSEADDR failed");
close(sockfd);
return -1;
}
难点代码
- sockfd:这是套接字文件描述符,socket()函数返回的值。setsockopt将对这个套接字应用配置选项。
- SOL_SOCKET:这是套接字层的选项类型,用于指定选项在套接字级别(例如,SO_REUSEADDR、SO_KEEPALIVE等)。当您想要设置通用的套接字选项(而不是协议特定的选项)时,会将此参数设置为SOL_SOCKET。
- SO_REUSEADDR:这是具体的选项标志,表示地址重用。它允许一个本地端口可以被多个套接字绑定,特别是在以下场景中使用:
- 服务器快速重启:如果服务器进程使用某个端口并意外终止,那么在短时间内重新启动该进程时,端口可能还被操作系统标记为"正在使用",SO_REUSEADDR可以使端口立即可用。
- 多播和任播:多个进程可以监听同一个端口,这对于多播和任播应用非常重要,因为多个服务器实例可能同时监听同一端口来接收数据。
- 端口共享:允许多个套接字同时绑定到同一IP地址和端口,但其中只有一个会接收传入数据。
- &opt:这是指向opt变量的指针,表示SO_REUSEADDR的值。我们将其设置为1(即opt = 1;),表示启用端口重用。若将其设置为0,则表示禁用端口重用。
- sizeof(opt):指定opt的大小,用于告诉setsockopt函数opt变量的长度。
5.4.5. 预期结果
在正确配置任播的情况下,只有一个接收方节点(例如距离A最近的B节点)会接收到消息,并在控制台上打印出发送方A的消息内容。这是因为任播路由的设计会自动将消息引导到最优路径的单一节点。
6. 种播的比较
以下是单播、广播、任播和组播的详细比较表:
特性 | 单播 (Unicast) | 广播 (Broadcast) | 任播 (Anycast) | 组播 (Multicast) |
---|---|---|---|---|
地址类型 | A、B、C类IP地址 | D类地址(以255.255.255.255结尾) | A、B、C类IP地址 | D类地址 (224.0.0.0 - 239.255.255.255) |
数据传输范围 | 一个发送者到一个接收者 | 一个发送者到同一网络内的所有接收者 | 一个发送者到多个节点之一,选择最近的一个节点 | 一个发送者到指定组内的多个接收者 |
数据传输效率 | 效率低,需要多个单独的传输 | 效率低,所有设备都接收,无论是否需要 | 效率较高,只发送到最优接收者 | 效率较高,只发送到订阅的接收者 |
应用场景 | 点对点通信,例如客户端-服务器模型 | 局域网中的广播(如ARP请求) | CDN、DNS等分布式服务,将流量引导到最近节点 | 视频会议、IPTV、在线直播 |
IP地址范围 | 常规单播IP地址(如A类、B类、C类) | 子网广播(如192.168.1.255),或全网广播 | 常规单播IP地址(如A类、B类、C类) | D类地址范围 (224.0.0.0 - 239.255.255.255) |
路由实现 | 常规路由,根据目标单个IP地址 | 不路由,一般局限于局域网内 | 通过路由协议选择最近或最优的路径 | 依靠多播路由协议(如PIM) |
典型协议 | TCP、UDP | UDP | BGP(用于选择最近节点) | IGMP、PIM |
适用范围 | 适用于需要专用通信的场景 | 局限于局域网 | 广域网和局域网均适用,要求网络支持任播配置 | 局域网和广域网均适用 |
优势 | 通信明确,安全性高 | 易于实现,无需复杂配置 | 实现负载均衡、提高服务性能 | 节省带宽,优化数据传输效率 |
缺点 | 不适合大规模接收者的通信 | 消耗带宽,增加不必要的网络流量 | 依赖路由协议支持,配置复杂 | 需要额外的组播路由配置,非所有网络支持 |
6.1. 简要总结
- 单播:适合点对点通信,目标唯一,路由简单。
- 广播:局限于局域网的同一子网,目标是所有接收者。
- 任播:多个节点共享IP地址,将流量引导到最近或最优的一个接收节点,适合分布式服务。
- 组播:专为多接收者的通信而设计,节省带宽,但需要额外的路由支持。