目录
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;