Linux_UDP聊天服务器

目录

ChatServer.hpp

ServerMain.cc

Route.hpp

InetAddr.hpp

ChatClient.cc

Logger.hpp

Mutex.hpp

Cond.hpp

Thread.hpp

ThreadPool.hpp


ChatServer.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
// 定义基本系统数据类型(如pid_t、size_t等),为socket相关函数提供类型支持
#include <sys/types.h>
// socket核心头文件,提供socket创建、操作的函数(如socket()、bind()、recvfrom())
#include <sys/socket.h>
// 提供IP地址转换函数(如inet_ntoa()、inet_addr())
#include <arpa/inet.h>
// 定义IPv4/IPv6地址结构体(如sockaddr_in)
#include <netinet/in.h>
// 字符串操作函数(如bzero(),用于清空内存)
#include <strings.h>
// 标准库通用工具,提供exit()等函数(用于程序异常退出)
#include <cstdlib>
// 函数对象封装,用于定义回调函数类型
#include <functional>

// 自定义的网络地址封装类(封装IP/端口、sockaddr_in结构体等)
#include "InetAddr.hpp"
// 自定义的日志工具类(用于分级打印日志:FATAL/INFO/DEBUG等)
#include "Logger.hpp"

// 定义回调函数类型别名,简化代码书写
// 回调函数参数说明:
// int sockfd: 服务器的socket文件描述符(用于回发数据)
// std::string message: 接收到的客户端消息
// InetAddr addr: 发送消息的客户端地址(包含IP和端口)
using callback_t = std::function<void (int sockfd, std::string message, InetAddr addr)>;

// 定义静态常量:表示无效的socket文件描述符(初始值)
// 文件描述符通常从0开始,-1是约定俗成的"无效/未初始化"标识
static const int gdefaultsockfd = -1;

// UDP聊天服务器核心类:封装socket创建、绑定、数据接收、回调处理等逻辑
class ChatServer
{
public:
    // 构造函数:初始化服务器端口、回调函数,初始化socket和运行状态
    // 参数:
    // uint16_t port: 服务器要监听的端口号(如8080)
    // callback_t cb: 接收到客户端消息后要执行的回调函数(业务逻辑解耦)
    ChatServer(uint16_t port, callback_t cb)
    : _port(port),          // 初始化服务器监听端口
      _sockfd(gdefaultsockfd),  // 初始化socket fd为无效值
      _isrunning(false),    // 初始化服务器运行状态为"未运行"
      _cb(cb)               // 初始化回调函数(业务逻辑由外部传入)
    {}

    // 服务器初始化函数:完成socket创建、地址绑定核心操作
    void Init()
    {
        // 1. 创建UDP socket文件描述符
        // 参数说明:
        // AF_INET: 使用IPv4协议族
        // SOCK_DGRAM: 数据报套接字(UDP协议,无连接、不可靠、面向报文)
        // 0: 自动选择对应协议(UDP)
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        
        // 错误处理:socket创建失败时打印致命日志并退出程序
        if(_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "create socket error";  // 打印致命错误日志
            exit(1);  // 退出程序,错误码1表示socket创建失败
        }
        // 日志:socket创建成功,打印文件描述符
        LOG(LogLevel::INFO) << "create socket success : " << _sockfd;

        // 2. 封装服务器本地地址(端口号由构造函数传入)
        InetAddr local(_port);

        // 3. 将socket文件描述符与本地地址绑定(必须步骤:让系统知道监听哪个端口)
        // bind参数说明:
        // _sockfd: 要绑定的socket fd
        // local.Addr(): 转换为sockaddr*类型的本地地址结构体
        // local.Length(): 地址结构体的长度
        int n = bind(_sockfd, local.Addr(), local.Length());
        
        // 错误处理:绑定失败时打印致命日志并退出程序
        if(n < 0)
        {
            LOG(LogLevel::FATAL) << "bind socket error";  // 打印致命错误日志
            exit(2);  // 退出程序,错误码2表示bind失败
        }
        // 日志:绑定成功,打印socket fd
        LOG(LogLevel::INFO) << "bind socket success : " << _sockfd;
    }

    // 服务器启动函数:进入循环接收客户端消息,是服务器的核心运行逻辑
    void Start()
    {
        // 标记服务器为"运行中"状态
        _isrunning = true;

        // 服务器主循环:持续接收客户端消息,直到停止
        while(_isrunning)
        {
            // 定义缓冲区:存储接收到的客户端消息(1024字节足够日常聊天消息)
            char buffer[1024];
            buffer[0] = 0;  // 清空缓冲区(避免脏数据)

            // 定义客户端地址结构体:存储发送消息的客户端IP和端口
            struct sockaddr_in peer;
            // 地址结构体长度:必须初始化,且传入recvfrom时为引用(会被内核修改)
            socklen_t len = sizeof(peer);

            // 1. 接收客户端UDP数据报
            // recvfrom参数说明:
            // _sockfd: 服务器socket fd
            // buffer: 存储接收数据的缓冲区
            // sizeof(buffer): 缓冲区最大长度(防止溢出)
            // 0: 接收标志(0表示默认行为)
            // (struct sockaddr*)&peer: 输出参数,存储客户端地址
            // &len: 输入输出参数,传入结构体长度,返回实际地址长度
            ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer), 0,
                (struct sockaddr*)&peer, &len);

            // 2. 数据接收成功(n>0表示接收到n字节数据)
            if(n > 0)
            {
                // 给接收到的字符串末尾加'\0',确保是合法C风格字符串(避免乱码)
                buffer[n] = 0;

                // 封装客户端地址(方便后续获取IP/端口)
                InetAddr clientaddr(peer);

                // 日志:打印客户端信息(IP+端口)和接收到的消息
                LOG(LogLevel::DEBUG) << "get a client info # " 
                    << clientaddr.Ip() << "-" << clientaddr.Port() << ": "
                    << buffer;

                // 将C风格字符串转换为C++ string,方便后续处理
                std::string message = buffer;

                // 核心:调用回调函数,将接收到的消息和客户端信息交给外部业务逻辑处理
                // 解耦设计:服务器只负责接收数据,业务逻辑(如广播、回复)由外部定义
                _cb(_sockfd, message, clientaddr);
            }
            // 注:此处省略了n<0(接收失败)和n==0(客户端关闭)的处理,可根据需求补充
        }

        // 循环退出后,标记服务器为"未运行"状态
        _isrunning = false;
    }

    // 服务器停止函数:修改运行状态,让主循环退出
    void Stop()
    {
        _isrunning = false;
    }

    // 析构函数:此处暂未实现(可补充关闭socket fd的逻辑,避免资源泄漏)
    ~ChatServer(){}

private:
    int _sockfd;          // 服务器socket文件描述符(唯一标识socket)
    uint16_t _port;       // 服务器监听的端口号
    callback_t _cb;       // 消息处理回调函数(解耦业务逻辑)
    bool _isrunning;      // 服务器运行状态标记(true=运行中,false=已停止)
};

ServerMain.cc

cpp 复制代码
// 引入线程池头文件:用于管理并发任务(异步处理消息转发,避免主线程阻塞)
#include "ThreadPool.hpp"
// 引入路由模块头文件:核心功能是实现消息转发(如将客户端消息广播给所有在线客户端)
#include "Route.hpp"
// 引入UDP聊天服务器核心类:封装socket创建、绑定、数据接收等网络操作
#include "ChatServer.hpp"
// 标准输入输出:用于打印程序使用提示、日志等
#include <iostream>
// 智能指针:用于自动管理对象内存(避免内存泄漏,简化资源释放)
#include <memory>

// 程序使用说明函数:当用户传入参数错误时,打印正确的运行方式
// 参数:proc - 程序名(argv[0])
void Usage(std::string proc)
{
    // std::cerr:标准错误输出,专门用于打印错误/提示信息
    std::cerr << "Usage: " << proc << " localport" << std::endl;
}

// 【调试用注释代码】原始的简单聊天回调函数(仅回显消息给发送客户端)
// 作用:测试服务器是否能正常接收/发送消息,实际业务中被线程池+路由替代
// void chat(int sockfd, std::string message, InetAddr addr)
// {
//     // 打印调试日志:socket fd、收到的消息、客户端地址
//     LOG(LogLevel::DEBUG) << "sockfd: " << sockfd;
//     LOG(LogLevel::DEBUG) << "message: " << message;
//     LOG(LogLevel::DEBUG) << "client info: " << addr.ToString();
//     // 回显消息:将收到的消息原封不动发送给客户端(sendto是UDP发送函数)
//     sendto(sockfd, message.c_str(), message.size(), 0, addr.Addr(), addr.Length());
// }

// 定义任务类型别名:封装无参数、无返回值的可调用对象(适配线程池任务队列)
using task_t = std::function<void()>;

// 程序入口函数:
// 运行方式:./udp_server serverport(如./udp_server 8080)
// 核心逻辑:初始化服务器、线程池、路由模块,实现UDP消息的异步广播
int main(int argc, char *argv[])
{
    // 1. 参数检查:确保用户传入「程序名+端口号」两个参数(argc=2)
    // argc:参数个数(argv[0]是程序名,argv[1]是端口号)
    if (argc != 2)
    {
        // 参数错误时,打印使用说明并退出程序
        Usage(argv[0]);
        exit(0);
    }

    // 2. 启用控制台日志策略:让日志输出到终端(而非文件),方便调试
    EnableConsoleLogStrategy();

    // 3. 解析端口号:将命令行传入的字符串端口(如"8080")转换为16位无符号整数
    uint16_t port = std::stoi(argv[1]);

    // 4. 创建消息路由对象
    // Route类核心作用:维护在线客户端列表,实现「收到A客户端消息→广播给所有客户端」
    std::unique_ptr<Route> r = std::make_unique<Route>();

    // 5. 获取线程池单例对象:
    // ThreadPool采用单例模式(GetInstance),避免创建多个线程池
    // 作用:异步处理消息转发任务,防止主线程(接收消息)被阻塞
    auto tp = ThreadPool<task_t>::GetInstance();

    // 6. 创建UDP聊天服务器对象(智能指针管理):
    // 构造参数:
    // - port:服务器监听的端口号
    // - 匿名lambda表达式:服务器收到消息后的回调函数(核心业务逻辑)
    std::unique_ptr<ChatServer> usvr = std::make_unique<ChatServer>(port,
       //捕获路由对象r和线程池tp
       [&r, &tp](int sockfd, std::string message, InetAddr addr){
            // 6.1 封装消息转发任务:
            // std::bind:将Route的RouteMessageToAll函数绑定为无参数的task_t类型
            // r.get():获取智能指针管理的原始指针(bind需要原始指针调用成员函数)
            // 绑定参数:sockfd(服务器socket)、message(客户端消息)、addr(客户端地址)
            task_t task = std::bind(&Route::RouteMessageToAll, r.get(), sockfd, message, addr);
            
            // 6.2 将任务加入线程池队列:
            // 线程池会自动调度空闲线程执行该任务(异步处理,不阻塞主线程接收消息)
            tp->Enqueue(task);
       }
    );

    // 7. 初始化服务器:执行socket创建、地址绑定等核心网络操作
    usvr->Init();

    // 8. 启动服务器:进入死循环,持续接收客户端UDP消息,收到后触发上述lambda回调
    // 注意:Start()是阻塞函数,会一直运行直到调用Stop()
    usvr->Start();

    return 0;
}


/*
核心流程:主函数串联三大模块 ------ChatServer(网络层,收消息)→ ThreadPool(并发层,异步处理)
→ Route(业务层,转发消息),实现 UDP 聊天服务器的消息广播功能。
*/
/*
客户端A输入消息 → sendto发送到服务器 → 服务器recvfrom接收 → 回调函数封装转发任务 →
线程池异步执行 → Route添加A到在线列表 → 
Route将消息(拼接A的IP+端口)广播给所有在线客户端(包括A) →
客户端B/C/...的recver线程recvfrom收到消息 → 打印到终端
*/

Route.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <vector>
// 引入网络地址封装类:每个元素代表一个在线客户端的地址(IP+端口)
#include "InetAddr.hpp"

// Route类:核心功能是「消息路由」+「在线用户管理」
// 1. 管理在线客户端列表(添加/删除/查重)
// 2. 实现UDP消息广播(将客户端消息转发给所有在线用户)
// 3. 处理自定义退出协议(客户端发送"QUIT"则移除出在线列表)
class Route
{
private:
    // 私有函数:检查指定客户端是否已在在线用户列表中
    // 参数:addr - 要检查的客户端地址(IP+端口)
    // 返回值:true=已存在,false=不存在
    // 作用:避免在线列表中出现重复的客户端(同一个IP+端口)
    bool IsExists(const InetAddr &addr)
    {
        // 遍历在线用户列表,逐个比较地址(依赖InetAddr重载的==运算符)
        for (auto &user : _online_user)
        {
            if (user == addr)
            {
                return true;
            }
        }
        return false;
    }

    // 私有函数:添加客户端到在线用户列表(安全添加,避免重复)
    // 参数:addr - 要添加的客户端地址
    void AddUser(const InetAddr &addr)
    {
        // 先检查是否已存在,不存在才添加(防止重复存储)
        if(!IsExists(addr))
            _online_user.push_back(addr);
    }

    // 私有函数:根据自定义协议删除在线客户端
    // 自定义协议:当客户端发送的消息为"QUIT"时,视为退出,从列表中移除
    // 参数:
    // message - 客户端发送的消息(判断是否是退出指令)
    // addr - 要删除的客户端地址
    void DeleteUser(const std::string &message, const InetAddr &addr)
    {
        // 权宜之计:简单的自定义退出协议(实际项目中可设计更完善的协议,如JSON/固定格式)
        if(message == "QUIT")
        {
            // 遍历在线列表,找到匹配的客户端地址
            auto iter = _online_user.begin();
            for(; iter != _online_user.end(); iter++)
            {
                if(*iter == addr)
                {
                    // 从列表中删除该客户端(erase会自动调整容器大小)
                    _online_user.erase(iter);
                    break; 
                }
            }
        }
    }

    // 私有函数:将消息广播给所有在线客户端(核心转发逻辑)
    // 参数:
    // sockfd - 服务器的UDP socket文件描述符(用于发送数据)
    // message - 要转发的客户端消息
    // addr - 发送该消息的客户端地址(用于拼接发送者信息)
    void SendMessageToAll(int sockfd, std::string &message, InetAddr &addr)
    {
        // 遍历所有在线客户端,逐个发送消息
        for(auto &user : _online_user)
        {
            // 调试日志:打印要转发的消息和目标客户端地址
            LOG(LogLevel::DEBUG) << "route [" << message << "] to : " << user.ToString();

            // 拼接发送内容:「发送者地址# 消息内容」(如"192.168.1.100-8080# 你好")
            // 作用:让接收方知道消息是谁发的,区分不同客户端
            std::string info = addr.ToString();
            info += "# ";
            info += message;

            // 发送UDP消息给目标客户端(sendto是UDP核心发送函数)
            // 参数:sockfd | 要发送的数据 | 数据长度 | 标志位(0=默认) | 目标地址 | 地址长度
            sendto(sockfd, info.c_str(), info.size(), 0, user.Addr(), user.Length());
        }
    } 

public:
    // 构造函数:默认构造(无需要初始化的资源,在线列表默认是空vector)
    Route()
    {
    }

    // 公有核心接口:消息路由入口(被线程池调用,处理客户端消息)
    // 参数:
    // sockfd - 服务器UDP socket fd
    // message - 客户端发送的消息
    // addr - 发送消息的客户端地址
    // 执行流程:添加用户→广播消息→检查是否退出(删除用户)
    void RouteMessageToAll(int sockfd, std::string &message, InetAddr &addr)
    {
        // 第一步:将发送消息的客户端加入在线列表(首次发送则添加,重复则忽略)
        AddUser(addr);

        // 第二步:将该客户端的消息广播给所有在线用户(核心功能)
        SendMessageToAll(sockfd, message, addr);

        // 第三步:检查是否是退出消息,若是则从在线列表删除该客户端
        DeleteUser(message, addr);
    }

    // 预留接口:一对一消息路由(私聊功能,暂未实现)
    // void RouteMessageToOne()
    // {}

    // 析构函数:无需手动释放资源(_online_user是vector,自动析构)
    ~Route()
    {
    }

private:
    // ************************ 临界资源说明 ************************
    // _online_user是「临界资源」:多个线程(线程池中的线程)会同时访问/修改它
    // 问题:多线程下直接操作会导致数据竞争(如一个线程添加、一个线程删除,可能导致列表崩溃)
    // 解决方案(注释中给出三种常见思路):

    // 方法1(最常用):加互斥锁(std::mutex)
    // 访问/修改_online_user前加锁,操作完成后解锁,保证同一时间只有一个线程操作
    std::vector<InetAddr> _online_user; // 存储所有在线客户端的地址列表

    // 方法2:锁 + 拷贝(读写分离)
    // 读操作:拷贝一份列表后遍历,不阻塞写操作;写操作:加锁修改原列表
    // std::vector<InetAddr> _send_list;

    // 方法3:消息队列(解耦发送和修改)
    // 将修改列表的指令/消息放入队列,由单独的线程处理,避免多线程直接操作列表
    // std::queue<std::string> _message_queue;
};

InetAddr.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
// 内存操作函数(如memset),用于清空地址结构体
#include <cstring>
// 系统基础类型定义(为socket相关函数提供类型支持)
#include <sys/types.h>
// socket核心头文件(定义sockaddr等结构体)
#include <sys/socket.h>
// IP地址转换函数(inet_ntoa、inet_addr)
#include <arpa/inet.h>
// IPv4地址结构体(sockaddr_in)定义
#include <netinet/in.h>

// 宏定义:类型转换封装,将sockaddr_in*转换为sockaddr*
// 原因:socket API(如bind/recvfrom)要求传入sockaddr*类型,而我们实际用的是IPv4的sockaddr_in
#define Conv(addr) ((struct sockaddr*)&addr)

// InetAddr类:封装网络地址信息(IPv4),核心作用是:
// 1. 处理「网络字节序」和「主机字节序」的转换
// 2. 封装sockaddr_in结构体,提供简洁的IP/端口操作接口
// 3. 适配socket API的参数要求(如返回sockaddr*、地址长度等)
class InetAddr
{
private:
    // 私有成员函数:将「网络字节序」的地址转换为「主机字节序」
    // 背景:网络协议规定使用大端字节序(网络序),而不同主机可能是小端/大端(主机序)
    // 作用:把内核返回的sockaddr_in中的端口/IP转换为主机能直接使用的格式
    void Net2Host()
    {
        // ntohs:network to host short,将网络序的16位端口转换为主机序
        _port = ntohs(_addr.sin_port);
        // inet_ntoa:将网络序的32位IP地址(整数)转换为点分十进制字符串(如"192.168.1.1")
        _ip = inet_ntoa(_addr.sin_addr);
    }

    // 私有成员函数:将「主机字节序」的地址转换为「网络字节序」
    // 作用:把用户传入的主机序IP/端口转换为网络序,存入sockaddr_in供socket API使用
    void Host2Net()
    {
        // 清空sockaddr_in结构体,避免脏数据(比如残留的内存值)
        memset(&_addr, 0, sizeof(_addr));
        // 设置地址族为IPv4(固定值AF_INET)
        _addr.sin_family = AF_INET;
        // htons:host to network short,将主机序的16位端口转换为网络序
        _addr.sin_port = htons(_port);
        // inet_addr:将点分十进制IP字符串转换为网络序的32位整数
        _addr.sin_addr.s_addr = inet_addr(_ip.c_str());
    }

public:
    // 构造函数1:接收内核返回的sockaddr_in(网络序)
    // 场景:比如recvfrom获取到的客户端地址,直接传入即可自动转换为主机序
    // 参数:addr - 网络序的IPv4地址结构体
    InetAddr(const struct sockaddr_in &addr)
        : _addr(addr)  // 初始化网络序的地址结构体
    {
        // 自动将网络序转换为主机序,并存入_ip和_port
        Net2Host();
    }

    // 构造函数2:接收主机序的端口和IP(用户手动指定)
    // 场景:比如服务器绑定地址(0.0.0.0:8080)、主动连接服务器时指定地址
    // 参数:
    // port - 主机序的端口号(如8080)
    // ip - 主机序的IP字符串,默认值"0.0.0.0"(表示监听所有网卡)
    InetAddr(uint16_t port, const std::string &ip = "0.0.0.0")
        : _port(port), _ip(ip)  // 初始化主机序的端口和IP
    {
        // 自动将主机序转换为网络序,并存入_addr
        Host2Net();
    }

    // 公有接口:获取主机序的IP地址字符串(如"127.0.0.1")
    std::string Ip()
    {
        return _ip;
    }

    // 公有接口:获取主机序的端口号(如8080)
    uint16_t Port()
    {
        return _port;
    }

    // 公有接口:返回sockaddr*类型的地址(适配socket API)
    // 场景:bind/recvfrom/sendto等函数需要传入sockaddr*参数
    struct sockaddr* Addr()
    {
        return Conv(_addr);  // 调用宏转换类型
    }

    // 公有接口:返回地址结构体的长度(固定为sizeof(sockaddr_in))
    // 场景:socket API要求传入地址长度(如bind/recvfrom的最后一个参数)
    socklen_t Length()
    {
        return sizeof(_addr);
    }

    // 公有接口:格式化输出地址(IP-端口),方便日志打印/调试
    // 示例:返回"192.168.1.100-8080"
    std::string ToString()
    {
        return _ip + "-" + std::to_string(_port);
    }

    // 重载==运算符:判断两个网络地址是否相同(IP+端口都一致)
    // 场景:比如管理客户端列表时,判断是否是同一个客户端
    bool operator==(const InetAddr &addr)
    {
        // 比较主机序的IP和端口(无需再转换字节序,更高效)
        return (_ip == addr._ip && _port == addr._port);
        // 注释行:仅比较IP(一般不推荐,同一IP不同端口是不同客户端)
        // return (_ip == addr._ip);
    }

    // 析构函数:无需手动释放资源(成员变量都是栈上/字符串,自动回收)
    ~InetAddr()
    {
    }

private:
    // 核心私有成员:
    struct sockaddr_in _addr;  // 存储「网络字节序」的IPv4地址结构体(给socket API用)
    std::string _ip;           // 存储「主机字节序」的IP字符串(给用户用,如"127.0.0.1")
    uint16_t _port;            // 存储「主机字节序」的端口号(给用户用,如8080)
};

ChatClient.cc

cpp 复制代码
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <thread>

void Usage(std::string proc)
{
    std::cerr << "Usage: " << proc << " serverip serverport" << std::endl;
}

int sockfd = -1;
std::string serverip;
uint16_t serverport;

void InitClient(const std::string &serverip, uint16_t serverport)
{
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        std::cout << "create socket errror" << std::endl;
    }
}

void recver()
{
    while (true)
    {
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        char buffer[1024];
        int m = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&temp, &len);
        if (m > 0)
        {
            buffer[m] = 0;
            std::cerr << buffer << std::endl; // 1->2
        }
    }
}

void sender()
{
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    server.sin_addr.s_addr = inet_addr(serverip.c_str());

    while (true)
    {
        std::cout << "Please Enter@ "; //1
        std::string line;
        std::getline(std::cin, line); //0

        // 写
        sendto(sockfd, line.c_str(), line.size(), 0, (struct sockaddr *)&server, sizeof(server));
    }
}

// ./udp_client server_ip server_port
int main(int argc, char *argv[])
{
    std::cerr << "hahhaha " << std::endl;
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(0);
    }

    serverip = argv[1];
    serverport = std::stoi(argv[2]);
    InitClient(serverip, serverport);

    std::thread trecv(recver);
    std::thread tsend(sender);

    trecv.join();
    tsend.join();

    return 0;
}

Logger.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <filesystem> // C++17 文件操作
#include <fstream>
#include <ctime>
#include <unistd.h>
#include <memory>
#include <sstream>
#include "Mutex.hpp"

// 规定出场景的日志等级
enum class LogLevel
{
    DEBUG,
    INFO,
    WARNING,
    ERROR,
    FATAL
};

std::string Level2String(LogLevel level)
{
    switch (level)
    {
    case LogLevel::DEBUG:
        return "Debug";
    case LogLevel::INFO:
        return "Info";
    case LogLevel::WARNING:
        return "Warning";
    case LogLevel::ERROR:
        return "Error";
    case LogLevel::FATAL:
        return "Fatal";
    default:
        return "Unknown";
    }
}

// 20XX-08-04 12:27:03
std::string GetCurrentTime()
{
    // 1. 获取时间戳
    time_t currtime = time(nullptr);

    // 2. 如何把时间戳转换成为20XX-08-04 12:27:03
    struct tm currtm;
    localtime_r(&currtime, &currtm);

    // 3. 转换成为字符串 -- dubug?
    char timebuffer[64];
    snprintf(timebuffer, sizeof(timebuffer), "%4d-%02d-%02d %02d:%02d:%02d",
             currtm.tm_year + 1900,
             currtm.tm_mon + 1,
             currtm.tm_mday,
             currtm.tm_hour,
             currtm.tm_min,
             currtm.tm_sec);

    return timebuffer;
}

///////////////////////////////////////////////////////////////////
// 1. 刷新的问题 -- 假设我们已经有了一条完整的日志,string->设备(显示器,文件)
// 基类方法
class LogStrategy
{
public:
    virtual ~LogStrategy() = default;
    virtual void SyncLog(const std::string &logmessage) = 0;
};

// 显示器刷新
class ConsoleLogStrategy : public LogStrategy
{
public:
    ~ConsoleLogStrategy()
    {
    }
    void SyncLog(const std::string &logmessage) override
    {
        {
            LockGuard lockguard(&_lock);
            std::cout << logmessage << std::endl;
        }
    }

private:
    Mutex _lock;
};

const std::string logdefaultdir = "log";
const static std::string logfilename = "test.log";

// 文件刷新
class FileLogStrategy : public LogStrategy
{
public:
    FileLogStrategy(const std::string &dir = logdefaultdir,
                    const std::string filename = logfilename)
        : _dir_path_name(dir), _filename(filename)
    {
        LockGuard lockguard(&_lock);
        if (std::filesystem::exists(_dir_path_name))
        {
            return;
        }
        try
        {
            std::filesystem::create_directories(_dir_path_name);
        }
        catch (const std::filesystem::filesystem_error &e)
        {
            std::cerr << e.what() << "\r\n";
        }
    }
    void SyncLog(const std::string &logmessage) override
    {
        {
            LockGuard lockguard(&_lock);
            std::string target = _dir_path_name;
            target += "/";
            target += _filename;

            std::ofstream out(target.c_str(), std::ios::app); // append
            if (!out.is_open())
            {
                return;
            }
            out << logmessage << "\n"; // out.write
            out.close();
        }
    }

    ~FileLogStrategy()
    {
    }

private:
    std::string _dir_path_name; // log
    std::string _filename;      // hello.log => log/hello.log
    Mutex _lock;
};

// 网络刷新

////////////////////////////////////////////////////////

// 1. 定制刷新策略
// 2. 构建完整的日志
class Logger
{
public:
    Logger()
    {
    }
    void EnableConsoleLogStrategy()
    {
        _strategy = std::make_unique<ConsoleLogStrategy>();
    }
    void EnableFileLogStrategy()
    {
        _strategy = std::make_unique<FileLogStrategy>();
    }
    // 形成一条完整日志的方式
    class LogMessage
    {
    public:
        LogMessage(LogLevel level, std::string &filename, int line, Logger &logger)
        : _curr_time(GetCurrentTime()),
          _level(level),
          _pid(getpid()),
          _filename(filename),
          _line(line),
          _logger(logger)
        {
            std::stringstream ss;
            ss << "[" << _curr_time << "] "
               << "[" << Level2String(_level) << "] "
               << "[" << _pid << "] "
               << "[" << _filename << "] "
               << "[" << _line << "]"
               <<  " - ";
            _loginfo = ss.str();
        }
        template<typename T>
        LogMessage& operator << (const T &info)
        {
            std::stringstream ss;
            ss << info;
            _loginfo += ss.str();
            return *this;
        }

        ~LogMessage()
        {
            if(_logger._strategy)
            {
                _logger._strategy->SyncLog(_loginfo);
            }
        }
    private:
        std::string _curr_time; // 日志时间
        LogLevel _level; // 日志等级
        pid_t _pid; // 进程pid
        std::string _filename;
        int _line;

        std::string _loginfo; // 一条合并完成的,完整的日志信息
        Logger &_logger; // 提供刷新策略的具体做法
    };
    LogMessage operator()(LogLevel level, std::string filename, int line)
    {
        return LogMessage(level, filename, line, *this);
    } 
    ~Logger()
    {
    }

private:
    std::unique_ptr<LogStrategy> _strategy;
};

Logger logger;

#define LOG(level) logger(level, __FILE__, __LINE__)
#define EnableConsoleLogStrategy() logger.EnableConsoleLogStrategy()
#define EnableFileLogStrategy() logger.EnableFileLogStrategy()

Mutex.hpp

cpp 复制代码
#pragma once
#include <iostream>
// C++标准库互斥锁头文件(本类封装的是POSIX线程库的锁,此处仅作为头文件引入参考)
#include <mutex>
// POSIX线程库核心头文件:提供pthread_mutex_t(互斥锁类型)及相关操作函数
#include <pthread.h>

// Mutex类:对POSIX线程库的原生互斥锁(pthread_mutex_t)进行面向对象封装
// 核心作用:简化互斥锁的使用,封装初始化、加锁、解锁、销毁等底层操作,避免手动调用原生API出错
class Mutex
{
public:
    // 构造函数:初始化互斥锁(资源获取)
    // 调用pthread_mutex_init:创建一个默认属性的互斥锁
    // 参数说明:&_lock - 要初始化的互斥锁对象;nullptr - 使用默认属性(普通互斥锁)
    Mutex()
    {
        pthread_mutex_init(&_lock, nullptr);
    }

    // 公有接口:加锁(阻塞式)
    // 调用pthread_mutex_lock:获取互斥锁,若锁已被其他线程持有,则当前线程阻塞等待
    // 注意:加锁后必须保证解锁,否则会导致死锁
    void Lock()
    {
        pthread_mutex_lock(&_lock);
    }

    // 公有接口:解锁
    // 调用pthread_mutex_unlock:释放持有的互斥锁,唤醒等待该锁的线程
    // 注意:仅能由加锁的线程调用,否则会触发未定义行为(程序崩溃/死锁)
    void Unlock()
    {
        pthread_mutex_unlock(&_lock);
    }

    // 公有接口:获取原生互斥锁指针
    // 作用:适配需要原生pthread_mutex_t*的场景(如配合条件变量pthread_cond_t使用)
    // 返回值:指向当前互斥锁对象的指针
    pthread_mutex_t *Get()
    {
        return &_lock;
    }

    // 析构函数:销毁互斥锁(资源释放)
    // 调用pthread_mutex_destroy:释放互斥锁占用的系统资源
    // 注意:析构时必须保证锁已解锁,否则会导致资源泄漏/系统错误
    ~Mutex()
    {
        pthread_mutex_destroy(&_lock);
    }

private:
    // 私有成员:原生POSIX互斥锁对象(封装底层资源,对外隐藏实现细节)
    pthread_mutex_t _lock;
};

// LockGuard类:基于RAII(资源获取即初始化)设计的「锁守卫」
// 核心作用:自动管理互斥锁的加锁/解锁,避免手动解锁遗漏导致的死锁
// 原理:构造函数加锁,析构函数自动解锁(C++保证析构函数在对象生命周期结束时必执行,即使抛异常/提前return)
class LockGuard
{
public:
    // 构造函数:接收Mutex指针,立即加锁(资源获取)
    // 参数:_mutex - 要管理的Mutex对象指针(不能为空,否则崩溃)
    // 设计思路:创建LockGuard对象的瞬间就完成加锁,无需手动调用Lock()
    LockGuard(Mutex *_mutex):_mutexp(_mutex)
    {
        _mutexp->Lock(); // 构造时自动加锁
    }

    // 析构函数:自动解锁(资源释放)
    // 场景:当LockGuard对象出作用域(如函数结束、异常抛出)时,析构函数执行,自动解锁
    // 优势:彻底避免「忘记解锁」「异常导致解锁代码未执行」等死锁问题
    ~LockGuard()
    {
        _mutexp->Unlock(); // 析构时自动解锁
    }

private:
    // 私有成员:指向要管理的Mutex对象(仅持有指针,不负责销毁Mutex)
    Mutex *_mutexp;

    // 【重要】禁用拷贝构造和赋值运算符(防止锁被意外拷贝,导致重复解锁/死锁)
    // 注:本代码未显式禁用,生产环境中建议添加:
    // LockGuard(const LockGuard&) = delete;
    // LockGuard& operator=(const LockGuard&) = delete;
};

Cond.hpp

cpp 复制代码
#pragma once
#include <iostream>
// POSIX线程库核心头文件:提供pthread_cond_t(条件变量类型)及相关操作函数
#include <pthread.h>
// 引入自定义的Mutex类:条件变量必须与互斥锁配合使用
#include "Mutex.hpp"

// Cond类:对POSIX线程库的原生条件变量(pthread_cond_t)进行面向对象封装
// 核心作用:实现线程间的「条件等待/通知」同步机制(如生产者-消费者模型)
// 关键特性:条件变量必须与互斥锁绑定使用,用于解决「线程等待某个条件满足」的场景
class Cond
{
public:
    Cond()
    {
        pthread_cond_init(&_cond, nullptr);
    }
    // 公有接口:阻塞等待条件变量(核心函数)
    // 参数:lock - 绑定的自定义Mutex对象(条件变量必须与互斥锁配合)
    // 底层逻辑(pthread_cond_wait):
    // 1. 原子操作:释放传入的互斥锁 + 将当前线程加入条件变量等待队列
    // 2. 线程阻塞,直到被NotifyOne/NotifyAll唤醒
    // 3. 被唤醒后:重新获取互斥锁,函数返回(此时锁已重新持有)
    void Wait(Mutex &lock)
    {
        // lock.Get():获取自定义Mutex封装的原生pthread_mutex_t*(适配底层API)
        int n = pthread_cond_wait(&_cond, lock.Get());
        // 注:此处未处理返回值n,生产环境可根据n判断是否调用失败(如EINVAL/EINTR)
    }

    // 公有接口:唤醒一个等待该条件变量的线程(单发通知)
    // 调用pthread_cond_signal:从条件变量的等待队列中唤醒任意一个线程
    // 注意:唤醒后线程需重新竞争互斥锁,不一定立即执行
    void NotifyOne()
    {
        int n = pthread_cond_signal(&_cond);
        (void)n; // 静默丢弃返回值(避免编译器警告,生产环境可加错误处理)
    }

    // 公有接口:唤醒所有等待该条件变量的线程(广播通知)
    // 调用pthread_cond_broadcast:唤醒条件变量等待队列中的所有线程
    // 场景:适用于多个线程等待同一条件,且条件满足后所有线程都需要执行的场景
    void NotifyAll()
    {
        int n = pthread_cond_broadcast(&_cond);
        (void)n; // 静默丢弃返回值(避免编译器警告)
    }

    // 析构函数:销毁条件变量(资源释放)
    // 调用pthread_cond_destroy:释放条件变量占用的系统资源
    // 注意:析构时必须保证无线程在等待该条件变量,否则会导致未定义行为
    ~Cond()
    {
        pthread_cond_destroy(&_cond);
    }

private:
    // 私有成员:原生POSIX条件变量对象(封装底层资源,对外隐藏实现细节)
    pthread_cond_t _cond;
};

Thread.hpp

cpp 复制代码
#ifndef __THREAD_HPP__
#define __THREAD_HPP__

#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
#include <functional>
#include <sys/syscall.h> /* For SYS_xxx definitions */
#include "Logger.hpp"

#define get_lwp_id() syscall(SYS_gettid)

using func_t = std::function<void(const std::string&name)>;
const std::string threadnamedefault = "None-Name";

class Thread
{
public:
    Thread(func_t func, const std::string &name = threadnamedefault)
        : _name(name),
          _func(func),
          _isrunning(false)
    {
        LOG(LogLevel::INFO) << _name << " create thread obj success";
    }
    static void *start_routine(void *args)
    {
        Thread *self = static_cast<Thread *>(args);
        self->_isrunning = true;
        self->_lwpid = get_lwp_id();
        self->_func(self->_name);
        pthread_exit((void *)0);
    }
    void Start()
    {
        int n = pthread_create(&_tid, nullptr, start_routine, this);
        if (n == 0)
        {
            LOG(LogLevel::INFO) << _name << " running success";
        }
    }
    void Stop()
    {
        int n = pthread_cancel(_tid); // 太简单粗暴了
        (void)n;
    }
    // void Die()
    // {
    //     pthread_cancel(_tid);
    // }
    // 检测线程结束并且回收的功能
    void Join()
    {
        if (!_isrunning)
            return;

        int n = pthread_join(_tid, nullptr);
        if (n == 0)
        {
            LOG(LogLevel::INFO) << _name << " pthread_join success";
        }
    }
    ~Thread()
    {
        // LOG(LogLevel::INFO) << _name << " destory thread obj success";
    }

private:
    bool _isrunning;
    pthread_t _tid;
    pid_t _lwpid;
    std::string _name;
    func_t _func;
};

#endif

ThreadPool.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <cstdio>
#include <queue>
#include <vector>
#include <unistd.h>
#include "Mutex.hpp"
#include "Cond.hpp"
#include "Thread.hpp"

// 单例线程池 - 懒汉模式
const static int defaultthreadnum = 3; // for debug

template <class T>
class ThreadPool
{
private:
    bool QueueIsEmpty()
    {
        return _q.empty();
    }
    void Routine(const std::string &name)
    {
        while (true)
        {
            // 把任务从线程获取到线程私有!临界区 -> 私有的栈
            T t;
            {
                LockGuard lockguard(&_lock);
                while (QueueIsEmpty() && _is_running)
                {
                    _wait_thread_num++;
                    _cond.Wait(_lock);
                    _wait_thread_num--;
                }
                if (!_is_running && QueueIsEmpty())
                {
                    LOG(LogLevel::INFO) << " 线程池退出 && 任务队列为空, " << name << " 退出";
                    break;
                }
                // 队列中一定有任务了!, 但是
                // 1. 线程池退出 -- 消耗历史
                // 2. 线程池没有退出 -- 正常工作
                t = _q.front();
                _q.pop();
            }
            t(); // 规定,未来的任务,必须这样处理!,处理任务需要再临界区内部进行吗?1 or 0            
        }
    }
    ThreadPool(int threadnum = defaultthreadnum)
        : _threadnum(threadnum), _is_running(false), _wait_thread_num(0)
    {
        for (int i = 0; i < _threadnum; i++)
        {
            // 方法1:
            // auto f = std::bind(hello, this);
            // 方法2
            std::string name = "thread-" + std::to_string(i + 1);
            _threads.emplace_back([this](const std::string &name)
                                  { this->Routine(name); }, name);

            // Thread t([this](){
            //     this->hello();
            // }, name);
            // _threads.push_back(st::move(t));
        }
        LOG(LogLevel::INFO) << "thread pool obj create success";
    }
    ThreadPool<T> &operator=(const ThreadPool<T> &) = delete;
    ThreadPool(const ThreadPool<T> &) = delete;

public:
    void Start()
    {
        if (_is_running)
            return;
        _is_running = true;
        for (auto &t : _threads)
        {
            t.Start();
        }
        LOG(LogLevel::INFO) << "thread pool running success";
    }

    // 核心思想:我们应该让线程走正常的唤醒逻辑退出
    // 线程池要退出
    // 1. 如果被唤醒 && 任务队列没有任务 = 让线程退出
    // 2. 如果被唤醒 && 任务队列有任务 = 线程不能立即退出,而应该让线程把任务处理完,在退出
    // 3. 线程本身没有被休眠,我们应该让他把他能处理的任务全部处理完成, 在退出
    // 3 || 2 -> 1
    // 如果任务队列有任务,线程是不会休眠的!
    void Stop()
    {
        if (!_is_running)
            return;
        _is_running = false;
        if (_wait_thread_num)
            _cond.NotifyAll();

        // 这种做法不推荐
        // if (!_is_running)
        //     return;
        // _is_running = false;
        // for (auto &t : _threads)
        // {
        //     t.Stop();
        // }
        // LOG(LogLevel::INFO) << "thread pool stop success";
    }
    void Wait()
    {
        for (auto &t : _threads)
        {
            t.Join();
        }
        LOG(LogLevel::INFO) << "thread pool wait success";
    }
    void Enqueue(const T &t)
    {
        if (!_is_running)
            return;
        {
            LockGuard lockguard(&_lock);

            _q.push(t);
            if (_wait_thread_num > 0)
                _cond.NotifyOne();
        }
    }
    // debug
    static std::string ToHex(ThreadPool<T> *addr)
    {
        char buffer[64];
        snprintf(buffer, sizeof(buffer), "%p", addr);
        return buffer;
    }
    // 获取单例 ??
    static ThreadPool<T> *GetInstance()
    {
        // A, B, c
        {
            // 线程安全,提高效率式的获取单例
            if (!_instance)
            {
                LockGuard lockguard(&_singleton_lock);
                if (!_instance)
                {
                    _instance = new ThreadPool<T>();
                    LOG(LogLevel::DEBUG) << "线程池单例首次被使用,创建并初始化, addr: " << ToHex(_instance);

                    _instance->Start();
                }
            }
        }
        // else
        // {
        //     LOG(LogLevel::DEBUG) << "线程池单例已经存在,直接获取, addr: " << ToHex(_instance);
        // }
        return _instance;
    }
    ~ThreadPool()
    {
    }

private:
    // -------------------------- 临界资源(需加锁访问) --------------------------
    std::queue<T> _q;               // 任务队列:存储待执行的任务
    std::vector<Thread> _threads;   // 工作线程数组:存储所有工作线程对象
    int _threadnum;                 // 工作线程数量
    int _wait_thread_num;           // 当前因任务为空而等待的线程数(用于精准唤醒)
    Mutex _lock;                    // 互斥锁:保护所有临界资源
    Cond _cond;                     // 条件变量:线程等待/唤醒的核心
    bool _is_running;               // 线程池运行状态:true=运行中,false=待退出

    // -------------------------- 单例相关静态成员 --------------------------
    static ThreadPool<T> *_instance;        // 单例实例指针(懒汉模式:初始为nullptr)
    static Mutex _singleton_lock;           // 保护单例创建的互斥锁(静态,全局唯一)
};

template <class T>
ThreadPool<T> *ThreadPool<T>::_instance = nullptr;

template <class T>
Mutex ThreadPool<T>::_singleton_lock;
相关推荐
yuanmenghao3 小时前
Linux 性能实战 | 第 19 篇:ftrace 内核跟踪入门 [特殊字符]
linux·python·性能优化
济6173 小时前
ARM Linux 驱动开发篇---TFTP挂载内核设备树,NFS挂载文件系统教程--解决高版本 Ubuntu的nfs挂载系统失败-- Ubuntu20.04
linux·arm开发·驱动开发
RisunJan3 小时前
Linux命令-lsof(列出所有进程打开的所有资源)
linux·服务器
Trouvaille ~3 小时前
【Linux】TCP可靠性与性能优化详解:从确认应答到拥塞控制
linux·运维·服务器·网络·tcp/ip·性能优化·操作系统
之歆10 小时前
Linux文件系统与FHS详解
linux·文件系统
小哥不太逍遥11 小时前
Technical Report 2024
java·服务器·前端
zl_dfq12 小时前
Linux 之 【多线程】(死锁、同步与竞态条件、条件变量、pthread_cond_xxx、POSIX信号量、sem_xxx)
linux
学Linux的语莫12 小时前
k8s常用命令
linux·容器·kubernetes
openKylin12 小时前
《2025年度OpenAtom openKylin社区全景案例集》正式发布
linux