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

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

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

1. UDP 服务器(Linux 端)

udp_server_linux.cpp

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

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 服务器

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

2. 编译 Windows 客户端

方式 1:MinGW 编译(推荐)
bash 复制代码
# 进入客户端代码目录
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 差异

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

5. 运行效果示例

Linux 服务器终端

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

Windows 客户端终端

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

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

1. TCP 服务器(Linux 端)

tcp_server_linux.cpp

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

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 服务器

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

2. 编译 Windows 客户端

方式 1:MinGW 编译(推荐,轻量)
bash 复制代码
# 进入客户端代码目录
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 端口

    bash 复制代码
    # 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>
套接字类型 int SOCKET(unsigned int)
无效套接字标识 -1 INVALID_SOCKET
关闭套接字 close() closesocket()
库初始化 / 清理 无需 WSAStartup() / WSACleanup()
错误码获取 perror() / errno WSAGetLastError()
数据收发 read() / write() recv() / send()

5. 运行效果示例

Linux 服务器终端

bash 复制代码
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 客户端终端

bash 复制代码
已成功连接到 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 端)

udp_server_linux.cp

cpp 复制代码
#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:服务器自身的地址(用于绑定)
    //   cli_addr:客户端的地址(接收数据时自动填充)
    struct sockaddr_in serv_addr, 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;
}

二、UDP 客户端(Linux 端)

udp_client_linux.cpp

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和端口号, 不能随便改变, 否则客户端无法正确访问服务端;

三、编译 & 运行步骤

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):

    bash 复制代码
    # 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 对比)

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

五、运行效果示例

服务器终端输出

bash 复制代码
=== 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

客户端终端输出

bash 复制代码
=== 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
客户端退出

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

一、TCP 服务器(Linux 端)

tcp_server_linux.cpp

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

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 终端,分别编译服务器和客户端:

bash 复制代码
# 编译服务器
g++ tcp_server_linux.cpp -o tcp_server

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

2. 运行程序

第一步:启动服务器(先运行)
bash 复制代码
./tcp_server

服务器启动后会输出:

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

客户端启动后会输出:

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

四、关键注意事项

1. 跨机器通信配置

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

  • 修改客户端代码中的 SERVER_IP服务器的实际局域网 IP (如 192.168.1.100);

  • 服务器放行 8080 端口(TCP):

    bash 复制代码
    # 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()

五、运行效果示例

服务器终端输出

bash 复制代码
=== 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 主动断开

客户端终端输出

bash 复制代码
=== 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客户端

相关推荐
vortex52 小时前
如何快速删除 Linux 中的海量小文件:告别rm命令的缓慢困境
linux·运维·服务器
General_G2 小时前
ROS2资源汇总
linux·机器人·ros2
学习3人组3 小时前
Docker 从本地Label-studio导入 tar 镜像包
运维·docker·容器
羑悻的小杀马特3 小时前
Docker-Android 容器化 + cpolar 穿透,完善异地调试
android·运维·docker·容器·cpolar
RisunJan4 小时前
Linux命令-ldd(查看可执行程序或共享库所依赖的动态链接库)
linux·运维·服务器
实心儿儿4 小时前
Linux —— 进程概念 - 进程运行、阻塞、挂起状态
linux·运维·服务器
观音山保我别报错4 小时前
消息队列项目基础知识总结
linux·服务器·数据库
历程里程碑4 小时前
Linux 5 目录权限与粘滞位详解
linux·运维·服务器·数据结构·python·算法·tornado