Linux:基于 UDP Socket 的实战项目——UDP 群聊聊天室

一、项目背景

前两个项目我们分别实现了 UDP 英译汉翻译服务器(单方向请求 - 响应)和 UDP 双向通信程序(一对一实时通信),本次将实现 UDP Socket 编程的综合实战项目 ------UDP 群聊聊天室,也是 UDP 无连接、全双工特性的典型应用

该项目基于 C/S 架构,采用 UDP 协议实现,核心功能是:多个客户端可同时连接服务端,任意一个客户端发送的消息,服务端会广播给所有在线的客户端,实现真正的 "群聊" 效果;同时支持客户端上线通知、下线通知、在线用户管理等功能

二、项目整体设计

1. 架构设计

  • 服务端 :核心是消息广播在线用户管理,监听固定 UDP 端口,维护所有在线客户端的地址信息(IP + 端口);接收任意客户端的消息,若为上线消息则添加客户端到在线列表,若为下线消息则移除,普通消息则广播给所有在线客户端;
  • 客户端 :基于上一个项目的多线程架构(发送线程 + 接收线程),实现用户输入消息发送、实时接收服务端广播的群聊消息,支持上线、下线、发送普通消息。
  • 通信协议:UDP(无连接,无需维护客户端连接状态,适合广播;实时性高,适合群聊场景)

2. 核心设计理念

1. 服务端 "无连接" 的在线用户管理

UDP 是无连接的,服务端无法主动检测客户端的在线状态,因此采用 **"首次发消息即上线"** 的设计:

  • 客户端启动后,首次发送的任意消息(包括普通消息)作为 "上线通知",服务端接收到后,将客户端的地址信息(struct sockaddr_in)添加到在线用户列表(vector);
  • 客户端输入指定指令(如quit)作为 "下线通知",服务端接收到后,将该客户端从在线用户列表中移除,并广播 "XX 客户端已下线";
  • 若客户端异常退出(如直接关闭终端),服务端不会主动移除,可后续扩展 "心跳检测" 机制实现自动下线。
2. 服务端消息广播的实现

广播的核心是:服务端接收到任意客户端的消息后,遍历在线用户列表 ,调用sendto()将消息依次发送给列表中的每一个客户端(排除发送方自身,可选)

因为 UDP 是无连接的,服务端只需知道客户端的 IP 和端口,即可直接发送消息,无需提前建立连接,这也是 UDP 实现广播比 TCP 更简单的核心原因(TCP 广播需要为每个客户端维护连接,资源开销大)

3. 代码高内聚低耦合 ------ 模块化封装

为了提高代码的可维护性和可扩展,采用模块化封装思想,将服务端拆分为三个独立的模块,通过接口交互:

  • Socket 通信模块(UdpServer 类):封装 UDP Socket 的创建、绑定、接收、发送接口,只负责网络通信,不关心业务逻辑;
  • 路由管理模块(Route 类):封装在线用户管理、消息广播、上下线处理等核心业务逻辑,是服务端的 "大脑";
  • 主逻辑模块 :整合 UdpServer 类和 Route 类,通过回调函数将网络通信和业务逻辑解耦,启动服务端。
4. 客户端复用多线程架构

客户端完全复用上一个项目的多线程架构:发送线程负责读取用户输入并发送到服务端,接收线程负责接收服务端广播的群聊消息并实时打印,无需做大的修改,只需适配服务端的消息格式。

3. 核心功能需求

服务端
  1. 绑定固定 UDP 端口,支持多个客户端同时连接;
  2. 维护在线用户列表,支持客户端上线添加、下线移除;
  3. 接收客户端的消息,区分普通消息、下线消息;
  4. 实现消息广播:将普通消息发送给所有在线客户端;
  5. 广播上下线通知:客户端上线 / 下线时,广播通知所有在线客户端;
  6. 处理网络异常,保证服务端稳定运行(不会因单个客户端异常而崩溃)。
客户端
  1. 通过命令行指定服务端 IP 和端口,启动后即可进入群聊;
  2. 多线程实现同时收发:发送线程读取用户输入,接收线程接收广播消息;
  3. 实时打印服务端广播的群聊消息、上下线通知;
  4. 支持输入quit优雅下线,服务端广播下线通知;
  5. 处理网络异常(如服务端关闭、网络中断),优雅退出;
  6. 消息输入友好:避免接收消息覆盖输入提示。

4. 消息格式设计

为了让群聊消息更易读,服务端对广播的消息做统一格式化,格式为:

复制代码
[发送方IP:端口] 时间 # 消息内容

上下线通知格式为:

复制代码
【系统通知】[客户端IP:端口] 已上线/已下线

示例:

复制代码
【系统通知】[127.0.0.1:42356] 已上线
[127.0.0.1:42356] 2024-05-21 15:30:00 # 大家好,我是新用户!
[192.168.1.101:56789] 2024-05-21 15:31:00 # 欢迎!
【系统通知】[127.0.0.1:42356] 已下线

三、核心技术点解析

1. UDP 广播的核心实现

UDP 广播的本质是遍历在线用户列表,逐个发送消息,核心代码逻辑:

复制代码
// 在线用户列表:存储客户端的地址结构
vector<struct sockaddr_in> online_users;
// 广播消息:msg为要广播的消息,sender为发送方地址(可选排除)
void Broadcast(int sockfd, const string& msg, const struct sockaddr_in& sender) {
    for (auto& user : online_users) {
        // 可选:排除发送方自身,避免自己收到自己发送的消息
        if (memcmp(&user, &sender, sizeof(user)) == 0) {
            continue;
        }
        sendto(sockfd, msg.c_str(), msg.size(), 0, (struct sockaddr*)&user, sizeof(user));
    }
}

该方式为应用层广播,区别于网络层的 UDP 广播(如 255.255.255.255),应用层广播更灵活,可精准控制广播的对象(仅在线客户端)

2. 在线用户管理 ------vector + 地址比对

服务端通过std::vector<struct sockaddr_in>存储在线客户端的地址结构,核心操作:

  1. 上线添加 :接收到客户端消息后,先判断该客户端是否在列表中(通过memcmp比对地址结构),若不在则添加,并广播上线通知;
  2. 下线移除 :接收到客户端的quit消息后,遍历列表,找到对应的客户端地址,通过erase移除,并广播下线通知;
  3. 地址比对 :使用memcmp比对两个struct sockaddr_in结构体的内存,完全一致则为同一个客户端,避免 IP / 端口不一致的情况。

3. 回调函数解耦网络层与业务层

服务端的 UdpServer 类(网络层)和 Route 类(业务层)通过C++11 的 std::function 回调函数解耦,UdpServer 类只负责:

  • 创建 Socket、绑定端口;
  • 循环接收客户端消息,获取发送方地址;
  • 调用外部传入的回调函数,将 Socket 描述符、消息、发送方地址传递给业务层。

Route 类则负责处理具体的业务逻辑:在线用户管理、消息格式化、广播消息,无需关心网络通信的细节。这种设计让 UdpServer 类成为通用的 UDP 服务端框架,可适配任意 UDP 业务场景。

4. 客户端多线程与输出优化

客户端完全复用前一个项目的发送线程 + 接收线程架构,核心优化点:

  1. 接收线程打印广播消息时,先换行再打印,避免覆盖 "我:" 的输入提示;
  2. 打印完成后执行fflush(stdout)刷新输出缓冲区,保证输入提示正常显示;
  3. 接收到服务端的系统通知时,用特殊符号(如【】)标记,与普通消息区分。

5. 时间戳格式化 ------ctime

服务端为广播消息添加时间戳 ,使用 C 标准库的ctime()函数将系统时间转换为字符串格式,核心代码:

复制代码
#include <time.h>
// 获取当前时间戳,格式:YYYY-MM-DD HH:MM:SS
string GetTime() {
    time_t now = time(nullptr);
    struct tm* t = localtime(&now);
    char buffer[64] = {0};
    snprintf(buffer, sizeof(buffer), "%04d-%02d-%02d %02d:%02d:%02d",
             t->tm_year + 1900, t->tm_mon + 1, t->tm_mday,
             t->tm_hour, t->tm_min, t->tm_sec);
    return string(buffer);
}

四、完整代码实现(C++)

环境说明

  • 系统:Linux(CentOS/Ubuntu/ 阿里云服务器);
  • 编译器:g++(支持 C++11 及以上,客户端多线程需加-pthread);
  • 编译命令:服务端g++ -o server server.cpp -std=c++11,客户端g++ -o client client.cpp -std=c++11 -pthread
  • 运行方式:先启动服务端,再启动多个客户端,支持本地 / 远程多客户端同时群聊。

1. 服务端代码(udp_chat_server.cpp)

整合 UdpServer 类和 Route 类,实现群聊核心功能:

复制代码
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cerrno>
#include <cstdlib>
#include <vector>
#include <functional>
#include <ctime>

using namespace std;

// 全局常量
const uint16_t DEFAULT_PORT = 8888;
const int BUFF_SIZE = 4096;
const string QUIT_CMD = "quit";
const string SYS_NOTICE_PREFIX = "【系统通知】";

// 工具函数:获取格式化的当前时间戳
string GetCurrentTime() {
    time_t now = time(nullptr);
    struct tm* local_tm = localtime(&now);
    if (local_tm == nullptr) {
        return "未知时间";
    }
    char time_buff[64] = {0};
    snprintf(time_buff, sizeof(time_buff), "%04d-%02d-%02d %02d:%02d:%02d",
             local_tm->tm_year + 1900, local_tm->tm_mon + 1, local_tm->tm_mday,
             local_tm->tm_hour, local_tm->tm_min, local_tm->tm_sec);
    return string(time_buff);
}

// 工具函数:将sockaddr_in转换为IP:端口字符串
string AddrToString(const struct sockaddr_in& addr) {
    char buff[64] = {0};
    snprintf(buff, sizeof(buff), "%s:%d", inet_ntoa(addr.sin_addr), ntohs(addr.sin_port));
    return string(buff);
}

// 路由管理类:封装在线用户管理、消息广播、业务逻辑
class Route {
private:
    vector<struct sockaddr_in> _online_users; // 在线用户列表

    // 判断客户端是否已在线
    bool IsOnline(const struct sockaddr_in& client) {
        for (const auto& user : _online_users) {
            if (memcmp(&user, &client, sizeof(user)) == 0) {
                return true;
            }
        }
        return false;
    }

    // 添加客户端到在线列表
    void AddOnlineUser(const struct sockaddr_in& client) {
        if (!IsOnline(client)) {
            _online_users.push_back(client);
        }
    }

    // 从在线列表移除客户端
    void RemoveOnlineUser(const struct sockaddr_in& client) {
        for (auto iter = _online_users.begin(); iter != _online_users.end(); ++iter) {
            if (memcmp(&(*iter), &client, sizeof(*iter)) == 0) {
                _online_users.erase(iter);
                break;
            }
        }
    }

    // 广播消息:排除发送方
    void BroadcastMsg(int sockfd, const string& msg, const struct sockaddr_in& sender) {
        for (const auto& user : _online_users) {
            if (memcmp(&user, &sender, sizeof(user)) == 0) {
                continue; // 排除发送方,不自己收自己的消息
            }
            sendto(sockfd, msg.c_str(), msg.size(), 0, (struct sockaddr*)&user, sizeof(user));
        }
    }

public:
    // 核心业务处理:处理客户端消息,实现上线、下线、广播
    void HandleMsg(int sockfd, const string& msg, const struct sockaddr_in& client) {
        string client_addr = AddrToString(client);
        // 1. 客户端下线
        if (msg == QUIT_CMD) {
            RemoveOnlineUser(client);
            string notice = SYS_NOTICE_PREFIX + "[" + client_addr + "] 已下线";
            cout << notice << endl;
            BroadcastMsg(sockfd, notice, client);
            return;
        }
        // 2. 客户端上线(首次发消息)
        if (!IsOnline(client)) {
            AddOnlineUser(client);
            string notice = SYS_NOTICE_PREFIX + "[" + client_addr + "] 已上线";
            cout << notice << endl;
            BroadcastMsg(sockfd, notice, client);
        }
        // 3. 普通消息,格式化后广播
        string format_msg = "[" + client_addr + "] " + GetCurrentTime() + " # " + msg;
        cout << format_msg << endl;
        BroadcastMsg(sockfd, format_msg, client);
    }
};

// UDP服务端类:通用网络通信框架,解耦业务逻辑
class UdpServer {
private:
    int _sockfd;
    uint16_t _port;
    function<void(int, const string&, const struct sockaddr_in&)> _handler; // 业务处理回调函数

    // 初始化Socket:创建+绑定
    bool Init() {
        // 创建UDP Socket
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0) {
            cerr << "创建Socket失败:" << strerror(errno) << endl;
            return false;
        }
        // 绑定IP和端口
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = INADDR_ANY;
        if (bind(_sockfd, (struct sockaddr*)&local, sizeof(local)) < 0) {
            cerr << "绑定端口" << _port << "失败:" << strerror(errno) << endl;
            close(_sockfd);
            return false;
        }
        return true;
    }

public:
    // 构造函数:传入端口和业务处理回调函数
    UdpServer(uint16_t port = DEFAULT_PORT, function<void(int, const string&, const struct sockaddr_in&)> handler = nullptr)
        : _port(port), _handler(handler), _sockfd(-1) {}

    // 析构函数:关闭Socket
    ~UdpServer() {
        if (_sockfd >= 0) {
            close(_sockfd);
        }
    }

    // 启动服务端:循环接收消息,调用回调函数处理
    void Start() {
        if (!Init() || _handler == nullptr) {
            cerr << "服务端启动失败" << endl;
            return;
        }
        cout << "UDP群聊聊天室服务端启动成功,监听端口:" << _port << endl;
        cout << "等待客户端连接...(Ctrl+C退出)" << endl;

        char buff[BUFF_SIZE] = {0};
        struct sockaddr_in client;
        socklen_t client_len = sizeof(client);

        while (true) {
            // 接收客户端消息
            memset(buff, 0, sizeof(buff));
            ssize_t recv_len = recvfrom(_sockfd, buff, sizeof(buff)-1, 0, (struct sockaddr*)&client, &client_len);
            if (recv_len < 0) {
                cerr << "接收消息失败:" << strerror(errno) << ",继续等待

c++枚举里面USAGE_ERR是什么意思

在 C++ 枚举中,USAGE_ERR 是一个自定义的枚举常量 ,并非 C++ 语言内置关键字,其核心含义是「用法错误 / 参数使用错误」,通常用于程序的错误码定义,专门标识「用户使用程序的方式不符合要求」(比如命令行参数输入错误)。

一、USAGE_ERR 的核心含义与使用场景

USAGE_ERR 是英文「Usage Error」的缩写,直译就是「用法错误」,最常见的使用场景是:

  • 命令行程序中,用户输入的参数数量、格式、取值不符合程序要求(比如需要 2 个参数却只输了 1 个,端口号输入了非数字等)
  • 函数调用时,传入的参数类型 / 范围 / 顺序不符合函数的使用规范
  • 枚举中定义该常量,用于统一管理错误码,让程序的错误提示更清晰、可维护

二、典型示例:枚举中定义 USAGE_ERR

下面是实际开发中最常见的写法 ------ 用枚举封装所有错误码,USAGE_ERR 作为其中一个枚举值:

复制代码
#include <iostream>
#include <string>
using namespace std;

// 定义错误码枚举:统一管理程序中所有可能的错误类型
enum ErrorCode {
    SUCCESS = 0,        // 成功(错误码通常0表示无错误)
    USAGE_ERR = 1,      // 用法错误(参数数量/格式不对)
    PORT_ERR = 2,       // 端口错误(端口号超出范围)
    IP_ERR = 3,         // IP地址错误(格式非法)
    SOCKET_ERR = 4      // Socket创建错误
};

// 命令行参数校验函数:返回错误码枚举
ErrorCode CheckArgs(int argc, char* argv[]) {
    // 要求参数数量为3(./程序名 IP 端口),否则返回USAGE_ERR
    if (argc != 3) {
        return USAGE_ERR;
相关推荐
cyber_两只龙宝1 小时前
【Docker】搭建Docker私有Registry仓库全流程详解
linux·运维·docker·容器·私有仓库
草莓熊Lotso1 小时前
Linux 进程信号深度解析(上):信号的产生与本质(含完整案例)
android·linux·运维·服务器·数据库·c++·mysql
Wizard7971 小时前
bootable中的伪代码
linux
Yupureki1 小时前
《Linux系统编程》13.Ext系列文件系统
linux·运维·服务器·c语言·开发语言·c++
不是书本的小明1 小时前
负载均衡 Nginx、LVS 和 HAProxy
linux·负载均衡
qq_254674411 小时前
宝利通(Polycom)系统中“一个分会场多路信号能否分别展示
服务器·网络·负载均衡
unityのkiven1 小时前
Hello-Claw 第一章学习笔记
笔记·学习
JACK的服务器笔记1 小时前
《服务器测试百日学习计划——Day11:网卡与链路基础,一张4口RoCE网卡的完整识别方法》
运维·服务器·学习