Linux_C++网络编程四种CS模型

第一种:Linux服务器+Windows客户端

UDP-L-W------------------------------------

1. UDP 服务器(Linux 端)

udp_server_linux.cpp

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 8080       // 监听端口
#define BUF_SIZE 1024   // 缓冲区大小

int main() {
    // 1. 创建 UDP 套接字(SOCK_DGRAM = UDP)
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        perror("socket 创建失败");
        return -1;
    }

    // 2. 绑定 IP + 端口(服务器必须 bind)
    struct sockaddr_in serv_addr, cli_addr;
    socklen_t cli_len = sizeof(cli_addr);
    memset(&serv_addr, 0, sizeof(serv_addr));

    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);       // 端口转网络字节序
    serv_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有网卡

    if (bind(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
        perror("bind 绑定失败");
        close(sockfd);
        return -1;
    }

    std::cout << "UDP 服务器(Linux)已启动,监听端口 " << PORT << "..." << std::endl;

    char buffer[BUF_SIZE];
    while (true) {
        // 3. 阻塞接收客户端数据
        ssize_t recv_len = recvfrom(
            sockfd, buffer, BUF_SIZE, 0,
            (struct sockaddr*)&cli_addr, &cli_len
        );

        if (recv_len < 0) {
            perror("recvfrom 接收失败");
            continue;
        }
        buffer[recv_len] = '\0';

        // 打印客户端信息
        std::cout << "【收到】来自 " 
                  << inet_ntoa(cli_addr.sin_addr) 
                  << ":" << ntohs(cli_addr.sin_port) 
                  << " 的消息:" << buffer << std::endl;

        // 4. 回复客户端
        const char* reply = "Linux 服务器已收到你的 UDP 消息!";
        sendto(
            sockfd, reply, strlen(reply), 0,
            (struct sockaddr*)&cli_addr, cli_len
        );
        std::cout << "【回复】已发送给客户端" << std::endl << std::endl;
    }

    close(sockfd);
    return 0;
}

2. UDP 客户端(Windows 端)

udp_client_windows.cpp

#include <iostream>
#include <cstring>
// Windows 专用 Winsock 头文件
#include <winsock2.h>
#include <ws2tcpip.h>
// 链接 Winsock 库(MinGW 编译需加 -lws2_32,VS 自动识别)
#pragma comment(lib, "ws2_32.lib")

#define SERVER_IP "192.168.1.100"  // 替换为你的 Linux 服务器实际 IP!
#define SERVER_PORT 8080          // 服务器端口(与服务器一致)
#define BUF_SIZE 1024

int main() {
    // 1. 初始化 Winsock 库(Windows 必须)
    WSADATA wsaData;
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
        std::cerr << "Winsock 初始化失败,错误码:" << WSAGetLastError() << std::endl;
        return -1;
    }

    // 2. 创建 UDP 套接字(SOCK_DGRAM = UDP)
    SOCKET sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd == INVALID_SOCKET) {
        std::cerr << "socket 创建失败,错误码:" << WSAGetLastError() << std::endl;
        WSACleanup();
        return -1;
    }

    // 3. 填充服务器地址
    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(SERVER_PORT);
    // IP 地址转换(Windows 与 Linux 用法一致)
    if (inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr) <= 0) {
        std::cerr << "IP 地址转换失败" << std::endl;
        closesocket(sockfd);
        WSACleanup();
        return -1;
    }

    // 4. 向服务器发送 UDP 消息
    const char* send_msg = "Hello Linux UDP Server! 我是 Windows 客户端~";
    int send_len = sendto(
        sockfd, send_msg, (int)strlen(send_msg), 0,
        (struct sockaddr*)&serv_addr, sizeof(serv_addr)
    );
    if (send_len == SOCKET_ERROR) {
        std::cerr << "sendto 发送失败,错误码:" << WSAGetLastError() << std::endl;
        closesocket(sockfd);
        WSACleanup();
        return -1;
    }
    std::cout << "【发送】给 Linux 服务器:" << send_msg << std::endl;

    // 5. 接收服务器回复
    char buffer[BUF_SIZE];
    int recv_len = recvfrom(
        sockfd, buffer, BUF_SIZE, 0, NULL, NULL
    );
    if (recv_len == SOCKET_ERROR) {
        std::cerr << "recvfrom 接收失败,错误码:" << WSAGetLastError() << std::endl;
        closesocket(sockfd);
        WSACleanup();
        return -1;
    }
    buffer[recv_len] = '\0';
    std::cout << "【收到】Linux 服务器回复:" << buffer << std::endl;

    // 6. 清理资源(Windows 必须)
    closesocket(sockfd);
    WSACleanup();
    return 0;
}

3. 编译 & 运行步骤

1. 编译 Linux 服务器

# 进入服务器代码目录
g++ udp_server_linux.cpp -o udp_server
# 运行服务器
./udp_server

2. 编译 Windows 客户端

方式 1:MinGW 编译(推荐)
# 进入客户端代码目录
g++ udp_client_windows.cpp -o udp_client.exe -lws2_32
# 运行客户端
udp_client.exe
方式 2:Visual Studio 编译
  1. 新建「空项目」,添加 udp_client_windows.cpp
  2. 直接编译运行(#pragma comment(lib, "ws2_32.lib") 会自动链接库)

4. 关键注意事项(必看)

  1. 服务器 IP 配置

    • Windows 客户端的 SERVER_IP 必须填 Linux 服务器的实际局域网 IP(如 192.168.1.100),不能用 127.0.0.1(除非 Linux 服务器在 WSL 中,且网络模式为桥接)。
    • 查看 Linux 服务器 IP:ifconfigip addr(找 eth0/wlan0inet 地址)。
  2. 防火墙放行

    • Linux 服务器:放行 UDP 8080 端口

      bash

      运行

      sudo ufw allow 8080/udp  # Ubuntu/Debian
      sudo firewall-cmd --add-port=8080/udp --permanent  # CentOS/RHEL
      sudo firewall-cmd --reload
      
    • Windows 客户端:允许程序通过防火墙(首次运行时弹窗选择「允许」)。
  3. 跨平台 API 差异

    功能LinuxWindows
    头文件<sys/socket.h> <netinet/in.h><winsock2.h> <ws2tcpip.h>
    套接字类型intSOCKET
    关闭套接字close()closesocket()
    库初始化无需WSAStartup()
    库清理无需WSACleanup()
    错误处理perror()WSAGetLastError()

5. 运行效果示例

Linux 服务器终端

UDP 服务器(Linux)已启动,监听端口 8080...
【收到】来自 192.168.1.105:54321 的消息:Hello Linux UDP Server! 我是 Windows 客户端~
【回复】已发送给客户端

Windows 客户端终端

【发送】给 Linux 服务器:Hello Linux UDP Server! 我是 Windows 客户端~
【收到】Linux 服务器回复:Linux 服务器已收到你的 UDP 消息!

TCP-L-W--------------------------------------

1. TCP 服务器(Linux 端)

tcp_server_linux.cpp

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 8080       // 监听端口
#define BUF_SIZE 1024   // 缓冲区大小

int main() {
    // 1. 创建 TCP 套接字(SOCK_STREAM = TCP)
    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd < 0) {
        perror("socket 创建失败");
        return -1;
    }

    // 2. 设置套接字选项(避免端口占用后重启报错)
    int opt = 1;
    if (setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
        perror("setsockopt 失败");
        close(listen_fd);
        return -1;
    }

    // 3. 绑定 IP + 端口
    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);
    serv_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有网卡

    if (bind(listen_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
        perror("bind 绑定失败");
        close(listen_fd);
        return -1;
    }

    // 4. 开启监听(转为被动套接字)
    if (listen(listen_fd, 5) < 0) { // 监听队列长度 5
        perror("listen 监听失败");
        close(listen_fd);
        return -1;
    }

    std::cout << "TCP 服务器(Linux)已启动,监听端口 " << PORT << "..." << std::endl;

    while (true) {
        // 5. 阻塞等待客户端连接(创建新的通信套接字)
        struct sockaddr_in cli_addr;
        socklen_t cli_len = sizeof(cli_addr);
        int conn_fd = accept(listen_fd, (struct sockaddr*)&cli_addr, &cli_len);
        if (conn_fd < 0) {
            perror("accept 接受连接失败");
            continue;
        }

        // 打印客户端信息
        std::cout << "\n【新连接】客户端 " 
                  << inet_ntoa(cli_addr.sin_addr) 
                  << ":" << ntohs(cli_addr.sin_port) 
                  << " 已连接" << std::endl;

        // 6. 与客户端通信(循环接收/发送数据)
        char buffer[BUF_SIZE];
        while (true) {
            // 清空缓冲区
            memset(buffer, 0, BUF_SIZE);
            // 接收客户端数据(阻塞)
            ssize_t recv_len = read(conn_fd, buffer, BUF_SIZE);
            
            // 客户端断开连接(recv_len = 0)或接收失败
            if (recv_len <= 0) {
                if (recv_len == 0) {
                    std::cout << "【断开连接】客户端 " 
                              << inet_ntoa(cli_addr.sin_addr) 
                              << ":" << ntohs(cli_addr.sin_port) 
                              << " 主动断开" << std::endl;
                } else {
                    perror("read 接收数据失败");
                }
                close(conn_fd); // 关闭通信套接字
                break;
            }

            // 打印客户端消息
            std::cout << "【收到】客户端消息:" << buffer << std::endl;

            // 回复客户端
            std::string reply = "Linux 服务器已收到:" + std::string(buffer);
            write(conn_fd, reply.c_str(), reply.length());
            std::cout << "【回复】已发送:" << reply << std::endl;
        }
    }

    close(listen_fd); // 理论上服务器不会走到这里
    return 0;
}

2. TCP 客户端(Windows 端)

tcp_client_windows.cpp

#include <iostream>
#include <cstring>
// Windows 专用 Winsock 头文件
#include <winsock2.h>
#include <ws2tcpip.h>
// 链接 Winsock 库(VS 自动识别,MinGW 编译需加 -lws2_32)
#pragma comment(lib, "ws2_32.lib")

#define SERVER_IP "192.168.1.100"  // 替换为你的 Linux 服务器实际 IP!
#define SERVER_PORT 8080          // 与服务器端口一致
#define BUF_SIZE 1024

int main() {
    // 1. 初始化 Winsock 2.2 库(Windows 必须)
    WSADATA wsaData;
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
        std::cerr << "Winsock 初始化失败,错误码:" << WSAGetLastError() << std::endl;
        return -1;
    }

    // 2. 创建 TCP 套接字(SOCK_STREAM = TCP)
    SOCKET sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (sock_fd == INVALID_SOCKET) {
        std::cerr << "socket 创建失败,错误码:" << WSAGetLastError() << std::endl;
        WSACleanup();
        return -1;
    }

    // 3. 填充服务器地址信息
    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(SERVER_PORT);
    // 转换服务器 IP 为网络字节序
    if (inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr) <= 0) {
        std::cerr << "IP 地址转换失败(请检查服务器 IP 是否正确)" << std::endl;
        closesocket(sock_fd);
        WSACleanup();
        return -1;
    }

    // 4. 连接 Linux 服务器(TCP 三次握手)
    if (connect(sock_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == SOCKET_ERROR) {
        std::cerr << "连接服务器失败,错误码:" << WSAGetLastError() << std::endl;
        closesocket(sock_fd);
        WSACleanup();
        return -1;
    }
    std::cout << "已成功连接到 Linux TCP 服务器(" << SERVER_IP << ":" << SERVER_PORT << ")" << std::endl;

    // 5. 与服务器通信(发送消息 + 接收回复)
    char send_buf[BUF_SIZE], recv_buf[BUF_SIZE];
    while (true) {
        // 输入要发送的消息
        std::cout << "\n请输入要发送的消息(输入 exit 退出):";
        std::cin.getline(send_buf, BUF_SIZE);

        // 退出条件
        if (strcmp(send_buf, "exit") == 0) {
            std::cout << "主动断开与服务器的连接" << std::endl;
            break;
        }

        // 发送消息到服务器
        int send_len = send(sock_fd, send_buf, (int)strlen(send_buf), 0);
        if (send_len == SOCKET_ERROR) {
            std::cerr << "send 发送失败,错误码:" << WSAGetLastError() << std::endl;
            break;
        }
        std::cout << "【发送】已发送:" << send_buf << std::endl;

        // 接收服务器回复
        memset(recv_buf, 0, BUF_SIZE);
        int recv_len = recv(sock_fd, recv_buf, BUF_SIZE, 0);
        if (recv_len == SOCKET_ERROR) {
            std::cerr << "recv 接收失败,错误码:" << WSAGetLastError() << std::endl;
            break;
        } else if (recv_len == 0) {
            std::cerr << "服务器已断开连接" << std::endl;
            break;
        }
        std::cout << "【收到】服务器回复:" << recv_buf << std::endl;
    }

    // 6. 清理资源(Windows 必须)
    closesocket(sock_fd);
    WSACleanup();
    return 0;
}

3. 编译 & 运行步骤

1. 编译 Linux 服务器

# 进入服务器代码目录
g++ tcp_server_linux.cpp -o tcp_server
# 运行服务器(后台运行可加 &:./tcp_server &)
./tcp_server

2. 编译 Windows 客户端

方式 1:MinGW 编译(推荐,轻量)
# 进入客户端代码目录
g++ tcp_client_windows.cpp -o tcp_client.exe -lws2_32
# 运行客户端
tcp_client.exe
方式 2:Visual Studio 编译
  1. 新建「空项目」,将 tcp_client_windows.cpp 添加到项目;
  2. 直接点击「运行」(#pragma comment(lib, "ws2_32.lib") 会自动链接 Winsock 库)。

4. 关键注意事项(必看)

1. 服务器 IP 配置

  • Windows 客户端的 SERVER_IP 必须替换为 Linux 服务器的实际局域网 IP(如 192.168.1.100),绝对不能用 127.0.0.1(仅本机回环,跨机器无法通信);
  • 查看 Linux 服务器 IP:执行 ifconfigip addr,找到 eth0(有线)/ wlan0(无线)的 inet 字段。

2. 防火墙放行

  • Linux 服务器:放行 TCP 8080 端口
    # Ubuntu/Debian 系统
    sudo ufw allow 8080/tcp
    # CentOS/RHEL 系统
    sudo firewall-cmd --add-port=8080/tcp --permanent
    sudo firewall-cmd --reload
    
  • Windows 客户端:首次运行客户端时,防火墙弹窗选择「允许访问」(专用网络 + 公用网络)。

3. 跨平台 API 核心差异

功能Linux 系统Windows 系统
核心头文件<sys/socket.h><winsock2.h> <ws2tcpip.h>
套接字类型intSOCKET(unsigned int)
无效套接字标识-1INVALID_SOCKET
关闭套接字close()closesocket()
库初始化 / 清理无需WSAStartup() / WSACleanup()
错误码获取perror() / errnoWSAGetLastError()
数据收发read() / write()recv() / send()

5. 运行效果示例

Linux 服务器终端

TCP 服务器(Linux)已启动,监听端口 8080...

【新连接】客户端 192.168.1.105:54321 已连接
【收到】客户端消息:Hello Linux TCP Server!
【回复】已发送:Linux 服务器已收到:Hello Linux TCP Server!
【收到】客户端消息:This is Windows Client
【回复】已发送:Linux 服务器已收到:This is Windows Client
【断开连接】客户端 192.168.1.105:54321 主动断开

Windows 客户端终端

已成功连接到 Linux TCP 服务器(192.168.1.100:8080)

请输入要发送的消息(输入 exit 退出):Hello Linux TCP Server!
【发送】已发送:Hello Linux TCP Server!
【收到】服务器回复:Linux 服务器已收到:Hello Linux TCP Server!

请输入要发送的消息(输入 exit 退出):This is Windows Client
【发送】已发送:This is Windows Client
【收到】服务器回复:Linux 服务器已收到:This is Windows Client

请输入要发送的消息(输入 exit 退出):exit
主动断开与服务器的连接

第二种:Linux服务器+Linux客户端

UDP-L-L------------------------------------

一、UDP 服务器(Linux 端)

服务端是要写的代码主要是:
初始化阶段:
1.创建一个套接字
2.为服务器定义并且配置地址结构体, 必须转成网络序, 这是协议的规定, 配置内容是主机的某个网卡的IP/所有网卡的IP, 某一个端口;通常我们会选择监听所有的网卡的IP: INADDR_ANY
3.绑定套接字和其监听的地址结构体
业务逻辑代码:
1.持续获取接受来自外部网络的发送过来的数据(包含了外部网络的地址结构体)
2.处理数据, (利用外部网络的地址结构体)向外部网络回复反馈

udp_server_linux.cp

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>// 套接字核心头文件:提供 socket()、bind()、recvfrom()、sendto() 等网络函数
#include <netinet/in.h>// 网络地址结构体头文件:定义 sockaddr_in(IPv4 地址结构体)
#include <arpa/inet.h>// IP 地址转换函数:提供 inet_ntoa()(将网络字节序 IP 转字符串)

#define PORT 8080       // 服务器监听的端口号(自定义,建议 1024 以上避免系统端口)
#define BUF_SIZE 1024   // 数据缓冲区大小:单次最多接收/发送 1024 字节数据

int main() {
    // ===================== 步骤1:创建 UDP 套接字(socket 描述符) =====================
    // socket() 函数作用:创建一个用于网络通信的"文件描述符"(Linux 一切皆文件)
    // 参数说明:
    //   AF_INET:地址族,指定使用 IPv4 协议
    //   SOCK_DGRAM:套接字类型,DGRAM = Datagram(数据报),表示 UDP 协议(无连接、不可靠)
    //   0:协议编号,0 表示根据前两个参数自动匹配(UDP 对应 IPPROTO_UDP)
    int sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock_fd < 0) {
        perror("socket 创建失败");
        return -1;// 程序异常退出
    }

    // ===================== 步骤2:设置套接字选项(端口复用) =====================
    // 问题背景:服务器重启时,若端口还处于 TIME_WAIT 状态,直接绑定会报错 "Address already in use"
    // 解决方案:设置 SO_REUSEADDR + SO_REUSEPORT 选项,允许端口复用
    int opt = 1; // 选项值:1 表示启用该选项
    // setsockopt() 函数:设置套接字的属性
    // 参数说明:
    //   sock_fd:要设置的套接字描述符
    //   SOL_SOCKET:级别,SOL_SOCKET 表示操作套接字本身的通用选项
    //   SO_REUSEADDR | SO_REUSEPORT:要启用的选项(按位或表示同时启用)
    //   &opt:选项值的地址(输入参数)
    //   sizeof(opt):选项值的长度
    if (setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt)))             {
        perror("setsockopt 失败");
        close(sock_fd); // 失败时必须关闭已创建的套接字,避免资源泄漏
        return -1;
    }

    // ===================== 步骤3:填充服务器地址结构体 =====================
    // 定义两个地址结构体:
    //   serv_addr:服务器自身的地址(用于绑定)
    struct sockaddr_in serv_addr;
    // cli_addr:客户端的地址(接收数据时自动填充)
    struct sockaddr_in cli_addr;
    // cli_len:客户端地址结构体的长度(recvfrom 的输入输出参数,必须初始化)
    socklen_t cli_len = sizeof(cli_addr);
    // memset:将 serv_addr 内存全部置 0,避免脏数据影响
    memset(&serv_addr, 0, sizeof(serv_addr));
    
    // 填充 serv_addr 字段(IPv4 地址结构体固定格式)
    serv_addr.sin_family = AF_INET; // 地址族:必须和 socket() 的 AF_INET 一致(IPv4)
    // htons():Host to Network Short(主机字节序转网络字节序,短整型)
    // 原因:不同系统主机字节序可能不同(大端/小端),网络协议强制使用大端序,必须转换
    serv_addr.sin_port = htons(PORT);         
    // INADDR_ANY:表示监听本机所有网卡的 IP(如 127.0.0.1、内网 IP、公网 IP)
    // 等价于 inet_addr("0.0.0.0"),无需手动指定具体 IP
    serv_addr.sin_addr.s_addr = INADDR_ANY;   

    // ===================== 步骤4:绑定套接字到 IP + 端口 =====================
    // bind() 函数:将套接字与指定的 IP 地址和端口绑定(UDP 服务器必须绑定,客户端无需)
    // 参数说明:
    //   sock_fd:套接字描述符
    //   (struct sockaddr*)&serv_addr:通用地址结构体(强制类型转换,兼容 IPv4/IPv6)
    //   sizeof(serv_addr):地址结构体长度
    if (bind(sock_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
        perror("bind 绑定失败");
        close(sock_fd); // 释放资源
        return -1;
    }

    // 打印启动信息,告知用户服务器已就绪
    std::cout << "=== UDP 服务器(Linux)已启动 ===" << std::endl;
    std::cout << "监听端口:" << PORT << ",等待客户端消息..." << std::endl;

    // ===================== 步骤5:循环接收客户端数据并回复 =====================
    char buffer[BUF_SIZE]; // 定义数据缓冲区,用于存储接收的客户端数据
    while (true) { // 无限循环:服务器持续运行,直到手动终止(Ctrl+C)
        memset(buffer, 0, BUF_SIZE); // 每次接收前清空缓冲区,避免残留上一次的数据

        // recvfrom():UDP 专用接收函数(阻塞式,直到收到数据才返回)
        // 核心特点:无需提前建立连接,接收数据时,自动获取客户端的 IP + 端口
        ssize_t recv_len = recvfrom(
            sock_fd,          // 套接字描述符
            buffer,           // 接收数据的缓冲区(输出)
            BUF_SIZE,         // 缓冲区最大长度(避免溢出)
            0,                // 标志位(0 表示无特殊行为)
            (struct sockaddr*)&cli_addr, // 输出参数,存储发送方(客户端)的地址信息
            &cli_len          // 输入输出参数,输入时是地址结构体长度,输出时是实际长度
        );
        // 错误处理:接收失败时打印错误,继续循环(不退出服务器)
        if (recv_len < 0) {
            perror("recvfrom 接收数据失败");
            continue;
        }

        // 打印客户端信息 + 收到的消息
        std::cout << "\n【收到】来自 " 
                  << inet_ntoa(cli_addr.sin_addr)  
                  // inet_ntoa:将网络字节序的 IP 转字符串(如 192.168.1.1)
                  << ":" << ntohs(cli_addr.sin_port)  
                  // ntohs:Network to Host Short(网络字节序转主机字节序)
                  << " 的消息:" << buffer << std::endl;

        // 构造回复消息:拼接客户端发送的内容,告知已收到
        std::string reply = "服务器已收到:" + std::string(buffer);
        // sendto():UDP 专用发送函数(无连接,需指定目标地址)
        sendto(
            sock_fd,                // 套接字描述符
            reply.c_str(),          // 要发送的字符串(转 C 风格字符串)
            reply.length(),         // 发送数据的长度
            0,                      // 标志位
            (struct sockaddr*)&cli_addr, // 目标地址(客户端地址)
            cli_len                 // 目标地址长度
        );
        std::cout << "【回复】已发送:" << reply << std::endl;
    }

    // ===================== 步骤6:释放资源(理论上不会执行) =====================
    // 因为服务器是无限循环,只有手动终止(Ctrl+C)才会退出,所以这里代码不会执行
    close(sock_fd); // 关闭套接字,释放文件描述符资源
    return 0;
}

sendto() 发送字符串时不需要转换字节序,本质是因为字节序问题只存在于多字节数值类型(如 int/short),而字符串是单字节序列,不存在字节序差异。
数值类型主机序(小端)内存存储网络序(大端)内存存储转换函数8888(端口)16 位0x38 0x220x22 0x38htons()1024(整数)32 位0x00 0x04 0x00 0x000x00 0x00 0x04 0x00htonl()

二、UDP 客户端(Linux 端)

代码功能:
初始化逻辑:
1. 创建套接字

2. 为服务器创建地址结构体, 并且填充目标服务器的ip和端口
业务逻辑:
 

udp_client_linux.cpp

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define SERVER_IP "127.0.0.1"  // 服务器 IP 地址:127.0.0.1 是本机回环地址(仅本机测试用),跨机器测试需改为服务器实际 IP
#define SERVER_PORT 8080       // 服务器监听端口:必须与 UDP 服务器的 PORT 保持一致,否则无法通信
#define BUF_SIZE 1024          // 数据缓冲区大小:单次最多发送/接收 1024 字节数据

int main() {
    // ===================== 步骤1:创建 UDP 套接字(socket 描述符) =====================
    // socket() 函数:创建用于 UDP 通信的套接字描述符
    // 参数说明:
    //   AF_INET:地址族,指定使用 IPv4 协议(与服务器保持一致)
    //   SOCK_DGRAM:套接字类型,DGRAM = Datagram(数据报),表示 UDP 协议(无连接、不可靠)
    //   0:协议编号,0 表示根据前两个参数自动匹配 UDP 协议(等价于 IPPROTO_UDP)
    int sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock_fd < 0) {
        perror("socket 创建失败");
        return -1;
    }

    // ===================== 步骤2:填充服务器地址结构体 =====================
    // 定义服务器地址结构体:用于指定消息发送的目标(服务器)地址
    struct sockaddr_in serv_addr;
    // memset:将 serv_addr 内存全部置 0,避免未初始化的脏数据影响通信
    memset(&serv_addr, 0, sizeof(serv_addr));
    
    // 填充服务器地址结构体的核心字段(IPv4 固定格式)
    serv_addr.sin_family = AF_INET;           // 地址族:必须为 AF_INET(IPv4),与服务器一致
    serv_addr.sin_port = htons(SERVER_PORT);  // 服务器端口:转换为网络字节序(大端),UDP 协议要求
    // inet_pton():将字符串格式的 IP 地址(如 "127.0.0.1")转换为网络字节序的二进制格式
    // 参数说明:
    //   AF_INET:地址族(IPv4)
    //   SERVER_IP:待转换的字符串 IP
    //   &serv_addr.sin_addr:输出参数,存储转换后的二进制 IP
    // 返回值:成功返回 1;无效 IP 返回 0;失败返回 -1
    if (inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr) <= 0) {
        perror("IP 地址转换失败(请检查服务器 IP 是否正确)");
        close(sock_fd); // 失败时关闭已创建的套接字,避免资源泄漏
        return -1;
    }

    // 打印客户端启动信息,告知用户服务器地址和端口
    std::cout << "=== UDP 客户端(Linux)已启动 ===" << std::endl;
    std::cout << "服务器地址:" << SERVER_IP << ":" << SERVER_PORT << std::endl;

    // ===================== 步骤3:循环发送消息到服务器并接收回复 =====================
    // 定义两个缓冲区:send_buf 存储用户输入的待发送消息,recv_buf 存储服务器的回复
    char send_buf[BUF_SIZE], recv_buf[BUF_SIZE];
    // 无限循环:直到用户输入 "exit" 才退出
    while (true) {
        // 提示用户输入要发送的消息
        std::cout << "\n请输入要发送的消息(输入 exit 退出):";
        // cin.getline():读取用户输入的一行字符串(包含空格),存入 send_buf,最多读取 BUF_SIZE 字节
        // 区别于 cin >> send_buf:cin >> 会以空格/换行为分隔符,getline 能读取整行
        std::cin.getline(send_buf, BUF_SIZE);

        // 退出逻辑:若用户输入 "exit",则终止循环并退出客户端
        // strcmp():比较两个字符串,相等返回 0,不等返回非 0
        if (strcmp(send_buf, "exit") == 0) {
            std::cout << "客户端退出" << std::endl;
            break; // 跳出 while 循环,执行后续的资源释放逻辑
        }


        // ===================== 发送消息到服务器 =====================
        // sendto():UDP 专用发送函数(无连接,需明确指定目标服务器地址)
        ssize_t send_len = sendto(
            sock_fd,                // 套接字描述符
            send_buf,               // 待发送的消息缓冲区
            strlen(send_buf),       // 发送数据的实际长度(strlen 不计算字符串末尾的 '\0')
            0,                      // 标志位
            (struct sockaddr*)&serv_addr, // 目标:目标地址(服务器的 IPv4 地址结构体,强制类型转换)
            sizeof(serv_addr)       // 目标地址结构体的长度
        );
        // 错误处理:发送失败时打印错误,跳出循环(退出客户端)
        if (send_len < 0) {
            perror("sendto 发送数据失败");
            break;
        }
        // 打印发送成功的日志,确认消息已发出
        std::cout << "【发送】已发送:" << send_buf << std::endl;

        // ===================== 接收服务器回复 =====================
        // 接收前清空回复缓冲区,避免残留上一次的回复数据
        memset(recv_buf, 0, BUF_SIZE);
        // recvfrom():UDP 专用接收函数(阻塞式,直到收到服务器回复才返回)
        ssize_t recv_len = recvfrom(
            sock_fd,    // 套接字描述符
            recv_buf,   // 存储服务器回复的缓冲区
            BUF_SIZE,   // 缓冲区最大长度(避免溢出)
            0,          // 标志位,0 表示阻塞接收
            NULL,       // 无需获取发送方(服务器)的地址,填 NULL 即可(客户端已知服务器地址)
            NULL        // 地址长度参数也填 NULL(与上一个参数对应)
        );
        // 错误处理:接收回复失败时打印错误,跳出循环(退出客户端)
        if (recv_len < 0) {
            perror("recvfrom 接收回复失败");
            break;
        }
        // 打印服务器的回复,告知用户
        std::cout << "【收到】服务器回复:" << recv_buf << std::endl;
    }

    // ===================== 步骤4:释放资源 =====================
    // 关闭套接字描述符,释放系统分配的网络资源(即使异常退出也会执行)
    close(sock_fd);
    return 0; // 程序正常退出
}

由于客户端有很多个, 且是动态的, 系统在客户端发送信息时会自动附加客户端的动态IP和端口;
不建议你为客户端绑死一个IP和端口, 这样可能会导致冲突, 因为对于同设备同IP, 不同应用的客户端必须使用不同端口, 系统会自动分配空闲的端口;

服务端必须要固定端口和IP, 并且客户端会直接内置服务端的IP和端口号, 不能随便改变, 否则客户端无法正确访问服务端;
 

你想知道为什么 UDP 客户端调用sendto()时不用显式发送自己的 IP / 端口,而服务端却能准确知道客户端地址 —— 核心原因是:UDP 数据包在网络传输时,底层协议栈(操作系统内核)会自动填充客户端的源 IP 和源端口,服务端通过recvfrom()的参数就能从数据包中提取这些信息,无需客户端手动传递

UDP 客户端 默认完全不需要手动bind() —— 操作系统会在你第一次调用sendto()时,自动帮你完成「本地 IP + 端口」的绑定,这是 UDP 无连接特性的核心设计之一。

bind()的本质是「显式指定套接字关联的本地 IP + 端口」,而 UDP 客户端的核心诉求是「给服务器发消息」,不是「让别人找自己」,所以操作系统会替你做 3 件事:

表格

操作时机操作系统自动行为咱们聊天客户端的例子
首次调用sendto()1. 选本地 IP:根据路由表选「能到达服务器的出口网卡 IP」(比如连本地服务器选 127.0.0.1,连公网服务器选网卡公网 IP)2. 选本地端口:从系统临时端口范围(如 32768-61000)随机选一个未被占用的端口3. 隐式绑定:把这个 IP + 端口和客户端套接字绑定你客户端代码里InitClient只调用了socket(),没bind(),但第一次执行sendto(服务器IP:8080)时,系统会自动给客户端绑定「127.0.0.1 + 随机端口(比如 54321)」
后续收发客户端所有的recvfrom()都会监听这个「自动绑定的 IP + 端口」,服务器转发的消息也会精准发到这个地址服务器广播消息时,目标地址就是客户端这个随机端口,所以客户端recver线程能收到

你客户端的InitClient函数只有一行创建 socket 的代码:

void InitClient(const std::string &serverip, uint16_t serverport)
{
    sockfd = socket(AF_INET, SOCK_DGRAM, 0); // 只创建socket,无bind
    if (sockfd < 0) { /* 错误处理 */ }
}

但你执行sendto(sockfd, line.c_str(), ..., 服务器地址)时,系统会:

  1. 自动给sockfd绑定「IP(比如192.168.1.100)+ 随机端口(比如 56789)」;
  2. 把消息从这个 IP + 端口发给服务器的 8080 端口;
  3. 服务器收到后,能从recvfrompeer参数拿到这个「192.168.1.100:56789」,所以广播消息时能精准发回给客户端。

三、编译 & 运行步骤

1. 编译代码

打开 Linux 终端,分别编译服务器和客户端:

# 编译服务器
g++ udp_server_linux.cpp -o udp_server

# 编译客户端
g++ udp_client_linux.cpp -o udp_client

2. 运行程序

第一步:启动服务器(先运行)
./udp_server

服务器启动后输出:

=== UDP 服务器(Linux)已启动 ===
监听端口:8080,等待客户端消息...
第二步:启动客户端(新开一个终端)
./udp_client

客户端启动后输出:

=== UDP 客户端(Linux)已启动 ===
服务器地址:127.0.0.1:8080

四、关键注意事项

1. 跨机器通信配置

如果服务器和客户端不在同一台 Linux 机器,需要:

  • 修改客户端代码中的 SERVER_IP服务器的实际局域网 IP(如 192.168.1.100);
  • 服务器放行 8080 端口(UDP):
    # Ubuntu/Debian 系统
    sudo ufw allow 8080/udp
    # CentOS/RHEL 系统
    sudo firewall-cmd --add-port=8080/udp --permanent
    sudo firewall-cmd --reload
    

2. 端口占用问题

如果启动服务器时报 bind 绑定失败: Address already in use

  • 查看占用 8080 端口的进程:lsof -i :8080
  • 杀死该进程:kill -9 进程ID
  • 或修改代码中的 PORT 为其他未占用端口(如 8081)。

3. UDP 核心特点(与 TCP 对比)

特性UDPTCP
连接性无连接(无需建立 / 断开连接)面向连接(三次握手 / 四次挥手)
可靠性不可靠(数据可能丢失 / 乱序)可靠(重传、确认、有序)
核心 APIrecvfrom()/sendto()read()/write()/accept()
服务器绑定必须 bind()必须 bind()
客户端绑定无需(系统自动分配临时端口)无需(系统自动分配临时端口)

五、运行效果示例

服务器终端输出

=== UDP 服务器(Linux)已启动 ===
监听端口:8080,等待客户端消息...

【收到】来自 127.0.0.1:54321 的消息:Hello UDP Server!
【回复】已发送:服务器已收到:Hello UDP Server!

【收到】来自 127.0.0.1:54321 的消息:This is Linux Client
【回复】已发送:服务器已收到:This is Linux Client

客户端终端输出

=== UDP 客户端(Linux)已启动 ===
服务器地址:127.0.0.1:8080

请输入要发送的消息(输入 exit 退出):Hello UDP Server!
【发送】已发送:Hello UDP Server!
【收到】服务器回复:服务器已收到:Hello UDP Server!

请输入要发送的消息(输入 exit 退出):This is Linux Client
【发送】已发送:This is Linux Client
【收到】服务器回复:服务器已收到:This is Linux Client

请输入要发送的消息(输入 exit 退出):exit
客户端退出

封装后的代码(UDPServer 类)

#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdexcept>  // 用于异常处理

// 定义 UDP 服务器类:封装所有属性和行为
class UDPServer {
public:
    // 构造函数:初始化端口和缓冲区大小(提供默认值,增强灵活性)
    // 入参:port - 监听端口,buf_size - 数据缓冲区大小
    explicit UDPServer(int port = 8080, int buf_size = 1024)
        : port_(port), buf_size_(buf_size), sock_fd_(-1) {
        // 初始化服务器地址结构体(清零)
        memset(&serv_addr_, 0, sizeof(serv_addr_));
    }

    // 析构函数:自动释放资源(核心优势:无需手动 close,防止泄漏)
    ~UDPServer() {
        closeServer();
        std::cout << "=== UDP 服务器资源已释放 ===" << std::endl;
    }

    // 禁用拷贝构造和赋值运算符(避免套接字描述符重复释放)
    UDPServer(const UDPServer&) = delete;
    UDPServer& operator=(const UDPServer&) = delete;

    // 初始化服务器:创建套接字 + 设置端口复用 + 绑定端口
    // 返回值:true-成功,false-失败
    bool init() {
        // 1. 创建 UDP 套接字
        sock_fd_ = socket(AF_INET, SOCK_DGRAM, 0);
        if (sock_fd_ < 0) {
            std::cerr << "错误:创建套接字失败 - " << strerror(errno) << std::endl;
            return false;
        }

        // 2. 设置端口复用选项
        int opt = 1;
        if (setsockopt(sock_fd_, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, 
                       &opt, sizeof(opt)) < 0) {
            std::cerr << "错误:设置套接字选项失败 - " << strerror(errno) << std::endl;
            closeServer();
            return false;
        }

        // 3. 填充服务器地址结构体
        serv_addr_.sin_family = AF_INET;         // IPv4 协议族
        serv_addr_.sin_port = htons(port_);      // 端口转网络字节序
        serv_addr_.sin_addr.s_addr = INADDR_ANY; // 监听所有网卡

        // 4. 绑定套接字到端口
        if (bind(sock_fd_, (struct sockaddr*)&serv_addr_, sizeof(serv_addr_)) < 0) {
            std::cerr << "错误:绑定端口失败 - " << strerror(errno) << std::endl;
            closeServer();
            return false;
        }

        std::cout << "=== UDP 服务器初始化成功 ===" << std::endl;
        std::cout << "监听端口:" << port_ << ",缓冲区大小:" << buf_size_ << "字节" << std::endl;
        return true;
    }

    // 运行服务器:循环接收客户端数据并回复(阻塞式)
    void run() {
        if (sock_fd_ < 0) {
            std::cerr << "错误:服务器未初始化,请先调用 init()!" << std::endl;
            return;
        }

        std::cout << "\n=== UDP 服务器已启动,等待客户端消息 ===" << std::endl;
        char* buffer = new char[buf_size_]; // 动态分配缓冲区(避免栈溢出)

        while (true) {
            // 清空缓冲区
            memset(buffer, 0, buf_size_);

            // 客户端地址信息(接收数据时自动填充)
            struct sockaddr_in cli_addr;
            socklen_t cli_len = sizeof(cli_addr);

            // 接收客户端数据
            ssize_t recv_len = recvfrom(
                sock_fd_, buffer, buf_size_, 0,
                (struct sockaddr*)&cli_addr, &cli_len
            );

            // 接收失败:打印错误,继续循环
            if (recv_len < 0) {
                std::cerr << "错误:接收数据失败 - " << strerror(errno) << std::endl;
                continue;
            }

            // 解析客户端信息
            std::string client_ip = inet_ntoa(cli_addr.sin_addr);
            int client_port = ntohs(cli_addr.sin_port);
            std::string recv_msg(buffer, recv_len); // 转换为 C++ 字符串

            // 打印接收信息
            std::cout << "\n【收到】来自 " << client_ip << ":" << client_port 
                      << " 的消息:" << recv_msg << std::endl;

            // 构造回复消息并发送
            std::string reply_msg = "服务器已收到:" + recv_msg;
            sendTo(reply_msg, client_ip, client_port);
            std::cout << "【回复】已发送:" << reply_msg << std::endl;
        }

        // 释放缓冲区(理论上不会执行,因无限循环)
        delete[] buffer;
    }

    // 主动发送数据给指定客户端(对外暴露的接口,可单独调用)
    // 入参:msg-发送内容,client_ip-客户端IP,client_port-客户端端口
    // 返回值:发送的字节数(-1 表示失败)
    ssize_t sendTo(const std::string& msg, const std::string& client_ip, int client_port) {
        if (sock_fd_ < 0) {
            std::cerr << "错误:套接字未初始化!" << std::endl;
            return -1;
        }

        // 填充客户端地址结构体
        struct sockaddr_in cli_addr;
        memset(&cli_addr, 0, sizeof(cli_addr));
        cli_addr.sin_family = AF_INET;
        cli_addr.sin_port = htons(client_port);

        // 转换客户端 IP 为网络字节序
        if (inet_pton(AF_INET, client_ip.c_str(), &cli_addr.sin_addr) <= 0) {
            std::cerr << "错误:客户端 IP 格式错误 - " << client_ip << std::endl;
            return -1;
        }

        // 发送数据
        ssize_t send_len = sendto(
            sock_fd_, msg.c_str(), msg.length(), 0,
            (struct sockaddr*)&cli_addr, sizeof(cli_addr)
        );

        if (send_len < 0) {
            std::cerr << "错误:发送数据失败 - " << strerror(errno) << std::endl;
        }
        return send_len;
    }

    // 关闭服务器:释放套接字资源
    void closeServer() {
        if (sock_fd_ >= 0) {
            close(sock_fd_);
            sock_fd_ = -1; // 标记为已关闭,防止重复关闭
        }
    }

private:
    // 私有成员变量:对外隐藏,仅类内部访问(封装核心)
    int sock_fd_;               // 套接字描述符(核心句柄)
    int port_;                  // 监听端口
    int buf_size_;              // 数据缓冲区大小
    struct sockaddr_in serv_addr_; // 服务器地址结构体
};

// 测试主函数:演示如何使用 UDPServer 类
int main() {
    try {
        // 1. 创建 UDP 服务器对象(指定端口 8080,缓冲区 1024)
        UDPServer server(8080, 1024);

        // 2. 初始化服务器
        if (!server.init()) {
            std::cerr << "服务器初始化失败,程序退出!" << std::endl;
            return -1;
        }

        // 3. 运行服务器(阻塞式,直到手动 Ctrl+C 终止)
        server.run();

    } catch (const std::exception& e) {
        // 异常捕获:增强程序健壮性
        std::cerr << "服务器运行异常:" << e.what() << std::endl;
        return -1;
    }

    return 0;
}

封装后的代码(UDPClient 类)

#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cerrno>   // 用于获取错误码描述
#include <stdexcept>

// UDP 客户端类:封装所有属性和行为,对外暴露简洁接口
class UDPClient {
public:
    // 构造函数:初始化服务器地址、端口、缓冲区大小(提供默认值,增强灵活性)
    // 入参:server_ip - 服务器IP,server_port - 服务器端口,buf_size - 缓冲区大小
    UDPClient(const std::string& server_ip = "127.0.0.1", 
              int server_port = 8080, 
              int buf_size = 1024)
        : server_ip_(server_ip), 
          server_port_(server_port), 
          buf_size_(buf_size), 
          sock_fd_(-1) {
        // 初始化服务器地址结构体(清零,避免脏数据)
        memset(&serv_addr_, 0, sizeof(serv_addr_));
    }

    // 析构函数:自动释放套接字资源(RAII 核心,防止资源泄漏)
    ~UDPClient() {
        closeClient();
        std::cout << "=== UDP 客户端资源已释放 ===" << std::endl;
    }

    // 禁用拷贝构造和赋值运算符(避免套接字描述符重复释放/操作)
    UDPClient(const UDPClient&) = delete;
    UDPClient& operator=(const UDPClient&) = delete;

    // 初始化客户端:创建套接字 + 填充服务器地址结构体
    // 返回值:true-成功,false-失败
    bool init() {
        // 1. 创建 UDP 套接字
        sock_fd_ = socket(AF_INET, SOCK_DGRAM, 0);
        if (sock_fd_ < 0) {
            std::cerr << "错误:创建套接字失败 - " << strerror(errno) << std::endl;
            return false;
        }

        // 2. 填充服务器地址结构体
        serv_addr_.sin_family = AF_INET; // IPv4 协议族
        serv_addr_.sin_port = htons(server_port_); // 端口转网络字节序

        // 转换服务器 IP 为网络字节序(替代原 inet_pton,增加错误提示)
        if (inet_pton(AF_INET, server_ip_.c_str(), &serv_addr_.sin_addr) <= 0) {
            std::cerr << "错误:服务器 IP 格式错误(" << server_ip_ << ")- " 
                      << strerror(errno) << std::endl;
            closeClient();
            return false;
        }

        std::cout << "=== UDP 客户端初始化成功 ===" << std::endl;
        std::cout << "服务器地址:" << server_ip_ << ":" << server_port_ << std::endl;
        return true;
    }

    // 运行客户端:循环读取用户输入 → 发送消息 → 接收回复(阻塞式)
    void run() {
        if (sock_fd_ < 0) {
            std::cerr << "错误:客户端未初始化,请先调用 init()!" << std::endl;
            return;
        }

        std::cout << "\n=== UDP 客户端已启动(输入 exit 退出)===" << std::endl;
        std::string input_msg; // 用 C++ string 替代字符数组,更安全

        while (true) {
            // 提示用户输入消息
            std::cout << "\n请输入要发送的消息:";
            std::getline(std::cin, input_msg); // 读取整行输入(支持空格)

            // 退出逻辑:输入 exit 终止循环
            if (input_msg == "exit") {
                std::cout << "客户端准备退出..." << std::endl;
                break;
            }

            // 空消息过滤:避免发送空字符串
            if (input_msg.empty()) {
                std::cout << "提示:消息不能为空,请重新输入!" << std::endl;
                continue;
            }

            // 1. 发送消息到服务器
            ssize_t send_len = sendMessage(input_msg);
            if (send_len < 0) {
                std::cerr << "发送失败,退出客户端..." << std::endl;
                break;
            }
            std::cout << "【发送】已发送:" << input_msg << std::endl;

            // 2. 接收服务器回复
            std::string reply_msg = receiveReply();
            if (reply_msg.empty()) {
                std::cerr << "接收回复失败,退出客户端..." << std::endl;
                break;
            }
            std::cout << "【收到】服务器回复:" << reply_msg << std::endl;
        }
    }

    // 发送消息到服务器(对外暴露的独立接口,可单独调用)
    // 入参:msg - 要发送的字符串
    // 返回值:发送的字节数(-1 表示失败)
    ssize_t sendMessage(const std::string& msg) {
        if (sock_fd_ < 0) {
            std::cerr << "错误:套接字未初始化!" << std::endl;
            return -1;
        }

        ssize_t send_len = sendto(
            sock_fd_,                // 套接字描述符
            msg.c_str(),             // 待发送数据(C风格字符串)
            msg.length(),            // 数据长度(不含 '\0')
            0,                       // 标志位
            (struct sockaddr*)&serv_addr_, // 目标服务器地址
            sizeof(serv_addr_)       // 地址结构体长度
        );

        if (send_len < 0) {
            std::cerr << "错误:发送消息失败 - " << strerror(errno) << std::endl;
        }
        return send_len;
    }

    // 接收服务器回复(对外暴露的独立接口,可单独调用)
    // 返回值:服务器回复的字符串(空字符串表示失败)
    std::string receiveReply() {
        if (sock_fd_ < 0) {
            std::cerr << "错误:套接字未初始化!" << std::endl;
            return "";
        }

        // 动态分配缓冲区(避免栈溢出,适配自定义 buf_size_)
        char* recv_buf = new char[buf_size_];
        memset(recv_buf, 0, buf_size_); // 清空缓冲区

        // 接收回复(无需获取服务器地址,填 NULL)
        ssize_t recv_len = recvfrom(
            sock_fd_,    // 套接字描述符
            recv_buf,    // 接收缓冲区
            buf_size_,   // 缓冲区大小
            0,           // 阻塞接收
            NULL,        // 不获取发送方地址
            NULL         // 地址长度参数置空
        );

        std::string reply;
        if (recv_len > 0) {
            reply = std::string(recv_buf, recv_len); // 转换为 C++ string
        } else if (recv_len < 0) {
            std::cerr << "错误:接收回复失败 - " << strerror(errno) << std::endl;
        }

        delete[] recv_buf; // 释放缓冲区
        return reply;
    }

    // 关闭客户端:释放套接字资源
    void closeClient() {
        if (sock_fd_ >= 0) {
            close(sock_fd_);
            sock_fd_ = -1; // 标记为已关闭,防止重复关闭
        }
    }

private:
    // 私有成员变量:对外隐藏,仅类内部访问(封装核心)
    int sock_fd_;               // 套接字描述符(核心句柄)
    std::string server_ip_;     // 服务器 IP 地址(C++ string 更安全)
    int server_port_;           // 服务器端口
    int buf_size_;              // 数据缓冲区大小
    struct sockaddr_in serv_addr_; // 服务器地址结构体
};

// 测试主函数:演示 UDPClient 类的使用
int main() {
    try {
        // 1. 创建 UDP 客户端对象(指定服务器 IP、端口、缓冲区)
        // 跨机器测试时,将 "127.0.0.1" 改为服务器实际 IP
        UDPClient client("127.0.0.1", 8080, 1024);

        // 2. 初始化客户端
        if (!client.init()) {
            std::cerr << "客户端初始化失败,程序退出!" << std::endl;
            return -1;
        }

        // 3. 运行客户端(循环交互,直到输入 exit)
        client.run();

    } catch (const std::exception& e) {
        // 异常捕获:增强程序健壮性
        std::cerr << "客户端运行异常:" << e.what() << std::endl;
        return -1;
    }

    return 0;
}

二、封装思路与核心设计亮点

TCL-L-L----------------------------------

一、TCP 服务器(Linux 端)

tcp_server_linux.cpp

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 8080       // 监听端口
#define BUF_SIZE 1024   // 数据缓冲区大小

int main() {
    // 1. 创建 TCP 套接字(SOCK_STREAM 表示 TCP 协议)
    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd < 0) {
        perror("socket 创建失败");
        return -1;
    }

    // 2. 设置套接字选项:允许端口复用(避免重启服务器时端口占用报错)
    int opt = 1;
    if (setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
        perror("setsockopt 失败");
        close(listen_fd);
        return -1;
    }

    // 3. 填充服务器地址结构体(IPv4)
    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr)); // 清空结构体
    serv_addr.sin_family = AF_INET;           // 地址族:IPv4
    serv_addr.sin_port = htons(PORT);         // 端口号:转网络字节序(大端)
    serv_addr.sin_addr.s_addr = INADDR_ANY;   // 监听本机所有网卡 IP

    // 4. 绑定套接字到指定 IP + 端口
    if (bind(listen_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
        perror("bind 绑定失败");
        close(listen_fd);
        return -1;
    }

    // 5. 开启监听(转为被动套接字,等待客户端连接)
    // 第二个参数:监听队列长度(最多同时等待 5 个连接)
    if (listen(listen_fd, 5) < 0) {
        perror("listen 监听失败");
        close(listen_fd);
        return -1;
    }

    std::cout << "=== TCP 服务器(Linux)已启动 ===" << std::endl;
    std::cout << "监听端口:" << PORT << ",等待客户端连接..." << std::endl;

    // 循环监听客户端连接(支持多客户端依次连接)
    while (true) {
        // 6. 阻塞等待客户端连接,成功后返回新的通信套接字
        struct sockaddr_in cli_addr;
        socklen_t cli_len = sizeof(cli_addr);
        int conn_fd = accept(listen_fd, (struct sockaddr*)&cli_addr, &cli_len);
        if (conn_fd < 0) {
            perror("accept 接受连接失败");
            continue;
        }

        // 打印客户端连接信息(IP + 端口)
        std::cout << "\n【新连接】客户端 " 
                  << inet_ntoa(cli_addr.sin_addr)  // 客户端 IP(转字符串)
                  << ":" << ntohs(cli_addr.sin_port)  // 客户端端口(转主机字节序)
                  << " 已连接" << std::endl;

        // 7. 与当前客户端循环通信
        char buffer[BUF_SIZE];
        while (true) {
            memset(buffer, 0, BUF_SIZE); // 清空缓冲区
            // 接收客户端数据(阻塞,直到收到数据/客户端断开)
            ssize_t recv_len = read(conn_fd, buffer, BUF_SIZE);

            // 处理接收结果:客户端断开(recv_len=0)或接收失败(recv_len<0)
            if (recv_len <= 0) {
                if (recv_len == 0) {
                    std::cout << "【断开连接】客户端 " 
                              << inet_ntoa(cli_addr.sin_addr) << ":" << ntohs(cli_addr.sin_port)
                              << " 主动断开" << std::endl;
                } else {
                    perror("read 接收数据失败");
                }
                close(conn_fd); // 关闭与该客户端的通信套接字
                break; // 回到外层循环,等待新客户端连接
            }

            // 打印客户端发送的消息
            std::cout << "【收到】客户端消息:" << buffer << std::endl;

            // 回复客户端(拼接收到的消息)
            std::string reply = "服务器已收到:" + std::string(buffer);
            write(conn_fd, reply.c_str(), reply.length());
            std::cout << "【回复】已发送:" << reply << std::endl;
        }
    }

    // 理论上服务器不会执行到这里(无限循环监听)
    close(listen_fd);
    return 0;
}

二、TCP 客户端(Linux 端)

tcp_client_linux.cpp

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define SERVER_IP "127.0.0.1"  // 服务器 IP(本机测试用回环地址,跨机器改实际 IP)
#define SERVER_PORT 8080       // 服务器端口(与服务器保持一致)
#define BUF_SIZE 1024          // 数据缓冲区大小

int main() {
    // 1. 创建 TCP 套接字
    int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (sock_fd < 0) {
        perror("socket 创建失败");
        return -1;
    }

    // 2. 填充服务器地址结构体
    struct sockaddr_in serv_addr;
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(SERVER_PORT);

    // 将服务器 IP 字符串转为网络字节序的二进制格式
    if (inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr) <= 0) {
        perror("IP 地址转换失败(请检查服务器 IP 是否正确)");
        close(sock_fd);
        return -1;
    }

    // 3. 连接服务器(TCP 三次握手)
    if (connect(sock_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
        perror("连接服务器失败");
        close(sock_fd);
        return -1;
    }

    std::cout << "=== TCP 客户端(Linux)已启动 ===" << std::endl;
    std::cout << "已成功连接到服务器:" << SERVER_IP << ":" << SERVER_PORT << std::endl;

    // 4. 与服务器循环通信
    char send_buf[BUF_SIZE], recv_buf[BUF_SIZE];
    while (true) {
        // 输入要发送的消息
        std::cout << "\n请输入要发送的消息(输入 exit 退出):";
        std::cin.getline(send_buf, BUF_SIZE);

        // 输入 exit 则主动断开连接
        if (strcmp(send_buf, "exit") == 0) {
            std::cout << "主动断开与服务器的连接" << std::endl;
            break;
        }

        // 发送消息到服务器
        ssize_t send_len = write(sock_fd, send_buf, strlen(send_buf));
        if (send_len < 0) {
            perror("write 发送数据失败");
            break;
        }
        std::cout << "【发送】已发送:" << send_buf << std::endl;

        // 接收服务器回复
        memset(recv_buf, 0, BUF_SIZE);
        ssize_t recv_len = read(sock_fd, recv_buf, BUF_SIZE);
        if (recv_len <= 0) {
            if (recv_len == 0) {
                std::cerr << "服务器已断开连接" << std::endl;
            } else {
                perror("read 接收回复失败");
            }
            break;
        }
        std::cout << "【收到】服务器回复:" << recv_buf << std::endl;
    }

    // 5. 关闭套接字,释放资源
    close(sock_fd);
    std::cout << "客户端已退出" << std::endl;
    return 0;
}

三、编译 & 运行步骤

1. 编译代码

打开 Linux 终端,分别编译服务器和客户端:

# 编译服务器
g++ tcp_server_linux.cpp -o tcp_server

# 编译客户端
g++ tcp_client_linux.cpp -o tcp_client

2. 运行程序

第一步:启动服务器(先运行)
./tcp_server

服务器启动后会输出:

=== TCP 服务器(Linux)已启动 ===
监听端口:8080,等待客户端连接...
第二步:启动客户端(新开一个终端)
./tcp_client

客户端启动后会输出:

=== TCP 客户端(Linux)已启动 ===
已成功连接到服务器:127.0.0.1:8080

四、关键注意事项

1. 跨机器通信配置

如果服务器和客户端不在同一台 Linux 机器,需要:

  • 修改客户端代码中的 SERVER_IP服务器的实际局域网 IP(如 192.168.1.100);
  • 服务器放行 8080 端口(TCP):
    # Ubuntu/Debian 系统
    sudo ufw allow 8080/tcp
    # CentOS/RHEL 系统
    sudo firewall-cmd --add-port=8080/tcp --permanent
    sudo firewall-cmd --reload
    

2. 端口占用问题

如果启动服务器时报 bind 绑定失败: Address already in use

  • 查看占用 8080 端口的进程:lsof -i :8080
  • 杀死该进程:kill -9 进程ID
  • 或修改代码中的 PORT 为其他未占用端口(如 8081)。

3. 核心 API 说明

API 函数作用
socket()创建套接字(通信句柄)
setsockopt()设置套接字选项(如端口复用)
bind()绑定 IP + 端口(服务器专用)
listen()开启监听(TCP 服务器专用)
accept()接受客户端连接(TCP 服务器专用)
connect()连接服务器(TCP 客户端专用)
read()/write()收发数据(替代 recv()/send()

五、运行效果示例

服务器终端输出

=== TCP 服务器(Linux)已启动 ===
监听端口:8080,等待客户端连接...

【新连接】客户端 127.0.0.1:54321 已连接
【收到】客户端消息:Hello TCP Server!
【回复】已发送:服务器已收到:Hello TCP Server!
【收到】客户端消息:This is Linux Client
【回复】已发送:服务器已收到:This is Linux Client
【断开连接】客户端 127.0.0.1:54321 主动断开

客户端终端输出

=== TCP 客户端(Linux)已启动 ===
已成功连接到服务器:127.0.0.1:8080

请输入要发送的消息(输入 exit 退出):Hello TCP Server!
【发送】已发送:Hello TCP Server!
【收到】服务器回复:服务器已收到:Hello TCP Server!

请输入要发送的消息(输入 exit 退出):This is Linux Client
【发送】已发送:This is Linux Client
【收到】服务器回复:服务器已收到:This is Linux Client

请输入要发送的消息(输入 exit 退出):exit
主动断开与服务器的连接
客户端已退出

第三种:Windows服务器+Linux客户端

第四种:Windows服务器+Windows客户端

这是一门linuxc++通讯架构实战课程,针对c/c++语言已经掌握的很熟并希望进一步深造以将来用c++linux下从事网络通讯领域/网络服务器的开发和架构工作。这门课程学习难度颇高但也有着极其优渥的薪水(最少30K月薪,最高可达60-80K月薪),这门课程,会先从nginx源码的分析和讲解开始,逐步开始书写属于自己的高性能服务器框架代码,完善个人代码库,这些,将会是您日后能取得高薪的重要筹码。本课程原计划带着大家逐行写代码,但因为代码实在过于复杂和精细,带着写代码可能会造成每节课至少要4~5小时的超长时间,所以老师会在课前先写好代码,主要的时间花费在逐行讲解这些代码上,这一点望同学们周知。如果你觉得非要老师领着写代码才行的话,老师会觉得你当前可能学习本门课程会比较吃力,请不要购买本课程,以免听不懂课程并给老师差评,差评也会非常影响老师课程的销售并造成其他同学的误解。 这门课程要求您具备下面的技能:(1)对c/c++语言掌握的非常熟练,语言本身已经不是继续学习的障碍,并不要求您一定熟悉网络或者linux;(2)对网络通讯架构领域有兴趣、勇于挑战这个高难度的开发领域并期望用大量的付出换取高薪;在这门课程中,实现了一个完整的项目,其中包括通讯框架和业务逻辑框架,浓缩总结起来包括如下几点:(1)项目本身是一个极完整的多线程高并发的服务器程序;(2)按照包头包体格式正确的接收客户端发送过来的数据包, 完美解决收包时的数据粘包问题;(3)根据收到的包的不同来执行不同的业务处理逻辑;(4)把业务处理产生的结果数据包正确返回给客户端;本项目用到的主要开发技术和特色包括:(1)epoll高并发通讯技术,用到的触发模式是epoll中的水平触发模式【LT】;(2)自己写了一套线程池来处理业务逻辑,调用适当的业务逻辑处理函数处理业务并返回给客户端处理结果;(3)线程之间的同步技术包括互斥量,信号量等等;(4)连接池中连接的延迟回收技术,这是整个项目中的精华技术,极大程度上消除诸多导致服务器程序工作不稳定的因素;(5)专门处理数据发送的一整套数据发送逻辑以及对应的发送线程;(6)其他次要技术,包括信号、日志打印、fork()子进程、守护进程等等;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值