【网络编程】单播、广播、组播、任播

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. 广播的类型

在计算机网络中,广播有两种主要类型:

  1. 有限广播(Limited Broadcast)
  • 使用特殊的IP地址 255.255.255.255
  • 仅在发送数据包的子网中传播,不能跨越路由器。
  • 例如,DHCP请求使用有限广播,因为客户端不知道服务器的IP地址。
  1. 定向广播(Directed Broadcast)
  • 使用的是子网广播地址,例如在 192.168.1.0/24 子网中的定向广播地址是 192.168.1.255
  • 定向广播包只会被发送到特定的子网,因此可以跨越一些允许定向广播的路由器。
  • 通常用于在特定子网中发送广播信息,而不是整个局域网。

假设有一个网络 192.168.1.0/24,它的广播地址是 192.168.1.255,那么向 192.168.1.255 发送的数据包会被该子网中的所有设备接收。

  • 如果有设备 A192.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. 上面的代码说明和解释
  1. 初始化 Winsock 库
    • 在 Windows 中使用套接字需要先初始化 Winsock 库。
    • WSAStartup 函数用于初始化 Winsock 库,这里使用的版本是 2.2。
WSAStartup(MAKEWORD(2, 2), &wsaData);
  1. 创建 UDP 套接字
    • socket() 函数用于创建一个套接字,这里指定 AF_INET 表示 IPv4,SOCK_DGRAM 表示 UDP 套接字。
    • IPPROTO_UDP 表示使用 UDP 协议。
SOCKET sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
  1. 启用广播选项
    • setsockopt 函数用于配置套接字选项。
    • 其中 SOL_SOCKET 表示套接字层选项,SO_BROADCAST 启用广播。
    • broadcastEnable 设置为 1 表示启用广播。
int broadcastEnable = 1;
setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, (char*)&broadcastEnable, sizeof(broadcastEnable));
  1. 配置广播地址
    • 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);
  1. 发送广播消息

    • sendto 用于发送 UDP 数据。
    • sockfd 是套接字描述符。
    • message 是要发送的消息,strlen(message) 指定消息长度。
    • broadcastAddr 是广播地址,sizeof(broadcastAddr) 指定结构体大小。
  2. 关闭套接字并清理 Winsock

    • closesocket 关闭套接字以释放资源。
    • WSACleanup 用于清理 Winsock 库,释放所有与 Winsock 相关的资源。
closesocket(sockfd);
WSACleanup();
3.5.2.2. 注意事项
  • 防火墙:如果广播消息无法发送或接收,检查系统防火墙是否允许UDP广播包。
  • 管理员权限:在 Windows 中发送广播消息可能需要管理员权限。

3.6. 运行与测试

在这里插入图片描述

抓包后得到:

在这里插入图片描述

4. 组播

4.1. 什么是组播多播?

组播多播实际上是同一个概念,两者只是翻译上的差异。组播(或多播)也是一种网络通信方法,允许数据只发送一次,但可以由多台主机接收。其主要应用于需要多个接收方实时同步数据的场景,比如在线视频直播网络游戏股票行情等。

组播通常用于局域网,因为许多路由器和网络设备(特别是家用和小型网络设备)不支持跨互联网的组播路由。此外,组播的管理和传输在局域网中更为高效和可控。不过,大型企业或互联网服务提供商可能在其内部网络中使用组播,以实现高效的数据分发。

4.2. 组播的原理

具体来说,在一个局域网中的组播过程如下:

  1. 假设局域网内有5台主机:A、B、C、D、E。
  2. 通过分配一个组播地址和端口(比如 239.255.0.1:8888)来定义组。
  3. 现在,如果主机 A、B 和 C 加入了这个组播组(即订阅了 239.255.0.1:8888 的组播消息),那么 D 和 E 不会收到该组播消息,因为它们没有加入该组。
  4. A 发送一条组播消息到 239.255.0.1:8888 时,B 和 C 会接收这条消息,而 D 和 E 不会收到,因为组播只会将数据发给加入了该组播地址的主机。
  5. 组播的IP和端口是程序自己随意选择的(避开常用端口,要大于1024),只要在239.0.0.0 到 239.255.255.255这个范围内就可以

4.3. 组播的实现代码示例

以下是一个在局域网内实现组播的 C++ 代码示例,包含发送端和接收端代码,并附有详细的中文注释

4.3.1. 发送端代码(组播发送 Windows版)
  1. 初始化WSAStartup:启用 Windows Sockets 库,准备使用套接字。
  2. 创建UDP套接字:使用 socket() 创建一个 UDP 套接字(SOCK_DGRAM)。
  3. 设置组播地址和端口:配置 sockaddr_in 结构,设置组播地址 239.255.0.1 和端口 8888
  4. 设置组播TTL(生存时间)IP_MULTICAST_TTL 选项设置组播消息的传输范围,ttl = 1 限制在本地局域网内传输。
  5. 发送消息:通过 sendto 发送组播消息。
  6. 关闭套接字:关闭套接字,清理 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版)
  1. 初始化WSAStartup:启用 Winsock 库。
  2. 创建UDP套接字:使用 socket() 创建一个 UDP 套接字。
  3. 绑定端口:将套接字绑定到端口 8888,接收来自所有网络接口的数据。
  4. 加入组播组:使用 setsockopt() 选项 IP_ADD_MEMBERSHIP 将本机加入到组播地址 239.255.0.1 中。
  5. 接收组播消息:使用 recvfrom() 接收来自组播地址的消息。
  6. 退出组播组:调用 setsockopt() 选项 IP_DROP_MEMBERSHIP,从组播组中退出。
  7. 关闭套接字:关闭套接字并清理 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.0239.255.255.255 是专用组播地址,通常用于本地组播。
  • TTL 设置:通过设置 TTL(Time-To-Live)为 1,可确保组播消息不会被路由器转发到其他网段。
int ttl = 1;
setsockopt(sendSock, IPPROTO_IP, IP_MULTICAST_TTL, (char*)&ttl, sizeof(ttl));
  1. IPPROTO_IP:
    • 这个参数指定了将要设置的选项所属的协议级别。IPPROTO_IP表示这是IPv4协议级别的选项。对于IPv6,相应的常量是IPPROTO_IPV6
  2. 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. 注意事项:
  1. IPv6 地址:IPv6 地址使用十六进制表示,并由冒号分隔。在这个例子中,我们使用了链路本地多播地址 ff02::1,但根据您的需求,您可能需要使用其他类型的 IPv6 多播地址。
  2. inet_pton:这个函数用于将点分十进制的 IPv4 地址或冒分十六进制的 IPv6 地址转换为网络字节序的二进制形式。对于 IPv6,我们使用 AF_INET6 作为第一个参数。
  3. sin6_scope_id:在 IPv6 中,多播地址可能有一个“作用域 ID”(scope ID),它指定了多播消息应该在哪个网络接口或链路上发送。对于大多数应用程序来说,可以将其设置为 0,让系统选择默认接口。但是,如果您有特定的网络接口要求,您可能需要设置这个字段。
  4. 头文件:请确保包含了正确的头文件。在某些系统上,IPv6 相关的函数和类型可能定义在 <netinet/in.h> 或其他头文件中,但 <arpa/inet.h> 通常包含了 inet_ptoninet_ntop。此外,对于 ICMPv6 处理,您可能需要包含 <netinet/icmp6.h>,但在这个简单的发送例子中并不需要。
  5. 错误处理:在实际应用中,您可能希望添加更详细的错误处理逻辑,以便在出现问题时能够更准确地诊断问题所在。

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(开放式最短路径优先协议)等网络路由协议来实现。

以下是最优路由的决策过程:

  1. 地理距离和路径开销:在同一任播IP地址的情况下,路由器根据用户的地理位置选择最短路径或最低延迟的路径,以减少数据传输时间。
  2. 网络延迟:当路径开销类似时,路由器会优先选择网络延迟较低的路径。网络延迟可以通过探测或协议交换获得。
  3. 路径稳定性:路由协议会优先选择路径稳定性较高的节点。路径的不稳定性可能会导致路由频繁更改,从而增加网络开销。
  4. 路由协议的计算与更新:在BGP等动态路由协议中,节点定期交换路由信息,一旦有节点发生变化(如故障、延迟增加),路由表会根据新的路由条件进行更新。
  5. 负载平衡机制:如果多个节点的延迟相似,路由器会分配请求给不同节点,以实现负载平衡。这种策略也用于避免某个节点出现瓶颈。

举个例子:
假设你在中国访问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,监听并接收消息。
  1. 在多个接收方节点(如B、C、D、E)上运行receiver.cpp,并确保网络配置支持任播。
  2. 在发送方节点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、UDPUDPBGP(用于选择最近节点)IGMP、PIM
适用范围适用于需要专用通信的场景局限于局域网广域网和局域网均适用,要求网络支持任播配置局域网和广域网均适用
优势通信明确,安全性高易于实现,无需复杂配置实现负载均衡、提高服务性能节省带宽,优化数据传输效率
缺点不适合大规模接收者的通信消耗带宽,增加不必要的网络流量依赖路由协议支持,配置复杂需要额外的组播路由配置,非所有网络支持

6.1. 简要总结

  • 单播:适合点对点通信,目标唯一,路由简单。
  • 广播:局限于局域网的同一子网,目标是所有接收者。
  • 任播:多个节点共享IP地址,将流量引导到最近或最优的一个接收节点,适合分布式服务。
  • 组播:专为多接收者的通信而设计,节省带宽,但需要额外的路由支持。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值