C++ 网络编程(12)服务器逻辑层单例模式设计
更新时间:2025年6月15日
️ 标签:C++ | Boost.Asio | 网络编程 | 单例模式 | 并发 | 加锁
前文第11章我们完善了消息节点,使用了tlv协议的格式去发送消息,
今天我们来实现用单例模式
来实现逻辑类
单例模式(Singleton Pattern) 是一种创建型设计模式
,它的核心目的是:
✅ 保证一个类在整个程序中只能有一个实例
✅ 并且提供一个全局访问点来获取这个实例
通俗理解
你可以把“单例”理解成:
程序里的 “唯一老大”,这个类你只能创建一次。
再创建?不行!得给你原来的那个!
就像你电脑系统里的「任务管理器」只能打开一个
接下来我们实现一个单例模板类,因为服务器的逻辑处理需要单例模式,后期可能还会有一些模块的设计也需要单例模式,所以先实现一个单例模板类,然后其他想实现单例类只需要继承这个模板类即可
#pragma once
#include
#include
#include
using namespace std;
template<typename T>
class Singleton
{
protected:
Singleton() = default;
Singleton(const Singleton<T>&) = delete;
Singleton& operator= (const Singleton<T>& st) = delete;
static std::shared_ptr<T> _instance;
public:
static std::shared_ptr<T> GetInstance()
{
static std::once_flag s_flag;
std::call_once(s_flag, [&]()
{
_instance = shared_ptr<T>(new T);
//_instance=std::make_shared();//错误!!!
}
)
return _instance;
}
//打印地址
void PrintAddress()
{
cout << _instance.get() << endl;
}
~Singleton()
{
cout << "this is Singleton destruct" << endl;
}
};
template<typename T>
std::shared_ptr<T> Singleton<T>::_instance = nullptr;//外部定义
为什么要将
默认构造和拷贝构造和拷贝赋值写在protected权限下
因为我们要做的是单例模式
,这样可以禁止外部随意创建或复制对象
Singleton(const Singleton<T>&) = delete
可以禁止下面这种情况发生
Singleton<MyClass> s2 = s1;
然后这样写
Singleton& operator=(const Singleton<T>&) = delete
禁止别人写 s2 = s1;
拷贝赋值同样可能让多个对象共享不一致状态,也破坏了单例性质
默认构造函数的写法问题
下面两种写法区别在哪?
Singleton() = default;
Singleton(){};
虽然都是默认构造函数而且什么都没有,但第一个是默认,第二个是用户自己写的
前者真正调用编译器生成的默认实现
保留了默认构造函数的所有特性(比如更高的性能、更强的编译器优化)
Singleton() = default
; 是在明确告诉编译器:“我就是要用默认构造函数,不加任何逻辑”
Singleton() {}
虽然也“能用”,但从语义上看不清楚你是故意要自定义、还是写了个空的
为什么成员变量智能指针对象 _instance和 成员函数GetInstance都要用static
静态变量
核心:
因为 单例模式的目标 是:
“类只存在一个全局唯一对象,且不需要实例化这个类就能访问它”
而 static
恰好实现了这个目标!
下面这个写法
保证了这个类全局只有一份实例指针
static std::shared_ptr<T> _instance;
然后后面定义的函数也是静态的,因为一开始我们没有实例化对象也可以直接调用
static std::shared_ptr<T> GetInstance()
为什么要用 std::once_flag
和 std::call_once
? 这两个是什么 ?
这两个东西是 C++11 引入的用于线程安全保证代码只执行一次的标准工具。特别适合像单例模式
这种 “只创建一个实例” 的场景,防止多线程同时初始化造成的问题
std::once_flag
是一个轻量级的标记,用来标识某段代码是否已经执行过。
它是一个不可复制的对象,只能和 std::call_once 配合使用
你可以把它想象成“这段代码是否执行过”的一个开关
std::call_once
是一个函数模板,接受两个参数:
一个 std::once_flag 变量(标记)
一个函数或可调用对象(lambda、函数指针等)
std::call_once 会保证传入的函数在多线程中只执行一次
也就是说,不管多少线程同时调用 call_once,只有第一个线程会执行函数体,其他线程会等待或者跳过,确保代码只执行一次
我们结合代码来总结这里
static std::once_flag s_flag; // 定义标记变量,只会有一份
std::call_once(s_flag, [&]() {
_instance = shared_ptr<T>(new T); // 只执行一次的初始化代码
});
当多个线程同时调用 GetInstance()
,所有线程都会尝试执行 call_once
。
call_once
看到 s_flag
还没被“打开”,第一个线程会执行 lambda 里的代码,创建 _instance。
其他线程看到 s_flag 已经“打开”,就不会再重复创建 _instance,直接跳过。
这样避免了竞态条件和重复创建
看到这里大家可能会有一个问题
为什么不直接用互斥锁 std::mutex???????
因为互斥锁每次都会锁住和解锁,可能带来额外开销。
call_once 内部实现是高度优化的,只在第一次调用时做加锁,之后就不会再加锁,性能更好
这里有一个很关键的地方
static std::once_flag s_flag;
此处必须用static! 来定义这个变量
GetInstance()
是一个静态成员函数,里面定义了 static std::once_flag s_flag;
,意味着 s_flag
这个标志是函数内部的静态局部变量。
静态局部变量只会初始化一次,而且在程序整个生命周期内存在,不管你调用多少次 GetInstance(),都用的是同一个 s_flag
如果你不写 static,s_flag 就变成了普通局部变量,每次调用 GetInstance() 时都会新建一个,根本没法记录“这个代码块执行过没”,call_once 失去作用,线程安全就没法保证了
为什么在创建_instance的时候这种写法是错误的
_instance=std::make_shared<T>();//错误!!!
因为我们使用make_shared的时候,内部会自动调用 T 的构造函数,但我们一开始将构造函数设置为 private
导致无法调用构造函数
所以我们用下面这种写法
_instance = shared_ptr<T>(new T);
我们实现逻辑系统的单例类,继承自Singleton
,这样LogicSystem
的构造函数和拷贝构造函数就都变为私有的了,因为基类的构造函数和拷贝构造函数都是私有的。另外LogicSystem
也有了基类的成员_instance
和GetInstance
函数。从而达到单例效果
#pragma once
#include"Singleton.h"
#include
#include
#include"CSession.h"
#include
#include
#include"const.h"
#include
#include
#include
typedef std::function<void(shared_ptr<CSession>, short msg_id, string msg_data)> FunCallBack;
class LogicSystem:public Singleton<LogicSystem>
{
friend class Singleton<LogicSystem>;
public:
~LogicSystem();
void PostMsgToQue( shared_ptr<LogicSystem>msg );
private:
LogicSystem();
void Dealmsg();
void RegisterCallBacks();
void HelloWordCallBack(shared_ptr<CSession>, short msg_id, string msg_data);
std::thread _worker_thread;
std::queue<shared_ptr<LogicNode>> _msg_que;
std::mutex _mutex;
std::condition_variable _consume;
bool _b_stop;
std::map<short, FunCallBack> _fun_callbacks;
};
FunCallBack
为要注册的回调函数类型,其参数为绘画类智能指针,消息id,以及消息内容
_msg_que
为逻辑队列
_mutex
为保证逻辑队列安全的互斥量
_consume
表示消费者条件变量,用来控制当逻辑队列为空时保证线程暂时挂起等待,不要干扰其他线程。
_fun_callbacks
表示回调函数的map,根据id查找对应的逻辑处理函数。
_worker_thread
表示工作线程,用来从逻辑队列中取数据并执行回调函数。
_b_stop
表示收到外部的停止信号,逻辑类要中止工作线程并优雅退出。
LogicNode
定义在CSession.h
中
我们这一步就是在做我们之前说过的将回调中要处理的逻辑一个个放入队列中一个个处理
我们对代码的知识点进行讲解
我们这里用typedef
+std::function
简化函数
typedef std::function<void(shared_ptr<CSession>, short msg_id, string msg_data)> FunCallBack;
意思是将返回值为void,并且有这三个相应参数的函数变成一个模板函数,然后取名为 FunCallBack
这里举个例子
#include
#include
#include
using namespace std;
class CSession {};
typedef function<void(shared_ptr<CSession>, short, string)> FunCallBack;
// 一个实际的回调函数
void MyHandler(shared_ptr<CSession> session, short id, string data) {
cout << "处理消息: " << id << ", 内容: " << data << endl;
}
int main() {
FunCallBack cb = MyHandler; // 把函数赋值给变量
cb(make_shared<CSession>(), 100, "Hello"); // 像函数一样调用
}
输出:
处理消息: 100, 内容: Hello
这样就简化了调用函数
std::condition_variable
是 C++ 标准库中的一个同步原语,属于
头文件。用于在多线程编程中实现线程之间的条件等待和通知机制,常常与互斥锁(如 std::mutex
)一起使用。
主要功能:
等待条件:线程可以在某个条件未满足时阻塞,等待其他线程通知。
通知:当条件满足时,另一个线程可以唤醒等待的线程
class LogicNode
{
friend class LogicSystem;
public:
LogicNode(shared_ptr<CSession> session, shared_ptr<RecvNode> recvnode);
private:
shared_ptr<CSession> _session;
shared_ptr<RecvNode> _recvnode;
};
LogicNode
用于封装一个消息节点,包含会话信息(CSession
)和接收到的消息数据(RecvNode
)
它作为 LogicSystem
处理的消息队列 _msg_que
的元素,负责传递消息和上下文
构造函数中我们将_b_stop
初始化为false 意味着外部没有通知服务器关闭
然后我们调用注册函数将消息id和回调函数绑定,这样后续触发的时候会调用回调函数
//构造函数:
LogicSystem::LogicSystem():_b_stop(false){
RegisterCallBacks();
_worker_thread = std::thread (&LogicSystem::DealMsg, this);
}
//注册函数
void LogicSystem::RegisterCallBacks()
{
//当处理 MSG_HELLO_WORD 这个消息的时候就会调用 HelloWordCallBack这个回调函数
//这里的MSG_HELLO_WORD 是1001 为消息id
_fun_callbacks[MSG_HELLO_WORD] = std::bind(&LogicSystem::HelloWordCallBack,
this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3);
}
因为我们写的这个回调函数HelloWordCallBack
有三个参数,所以这里需要三个占位符
下面的回调函数就是解析信息和id 然后再将id和消息发送回去
//回调函数
void LogicSystem::HelloWordCallBack(shared_ptr<CSession>session,
const short& msg_id, const string& msg_data)
{
Json::Reader reader;
Json::Value root;
reader.parse(msg_data, root);
std::cout << "receive msg id is" << root["id"].asInt() << " msg data is" << root["data"].asString() << std::endl;
//回复给客户端
root["data"] = "server has receive msg,msg data is " + root["data"].asString();
std::string return_str = root.toStyledString();
session->Send(return_str, root["id"].asInt());
}
同时我们注意我们在构造函数中启动一个线程
_worker_thread = std::thread (&LogicSystem::DealMsg, this);
这个DealMsg
就是用来监听消息队列 _msg_que,当有消息到达时取出并处理
void LogicSystem::Dealmsg()
{
for (;;)
{
//加锁
std::unique_lock<std::mutex> unique_lk(_mutex);
//判断队列为空
while (_msg_que.empty() && !_b_stop)
{
_consume.wait(unique_lk);//先释放资源再唤醒
}
//取出所有数据 及时处理 退出循环
if (_b_stop)
{
while (!_msg_que.empty())
{
auto msg_node = _msg_que.front();
cout << "recv msg id is" << msg_node->_recvnode->_msg_id << endl;
auto call_back_iter = _fun_callbacks.find(msg_node->_recvnode->_msg_id);
if (call_back_iter == _fun_callbacks.end())
{
_msg_que.pop();
continue;
}
call_back_iter->second(msg_node->_session, msg_node->_recvnode->_msg_id,
std::string(msg_node->_recvnode->_data, msg_node->_recvnode->_cur_len));
_msg_que.pop();
}
break;
}
//如果继续 队列中还有数据
auto msg_node = _msg_que.front();
cout << "recv msg id is" << msg_node->_recvnode->_msg_id << endl;
auto call_back_iter = _fun_callbacks.find(msg_node->_recvnode->_msg_id);
if (call_back_iter == _fun_callbacks.end())
{
_msg_que.pop();
continue;
}
call_back_iter->second(msg_node->_session, msg_node->_recvnode->_msg_id,
std::string(msg_node->_recvnode->_data, msg_node->_recvnode->_cur_len));
_msg_que.pop();
}
}
大致流程就是通过加锁,然后判断队列中是否有元素,如果有,就通过消息id去找相对应的回调函数,看能否找到,找不到就弹出并继续,找到就调用回调函数,传入对应的 _session、_msg_id 和从 _data 构造的字符串
最后还有封装了一个投递函数,就是将消息投进消息队列中
void LogicSystem::PostMsgToQue(shared_ptr<LogicNode> msg)
{
//加锁
std::unique_lock<std::mutex> unique_lk(_mutex);
_msg_que.push(msg);
if (_msg_que.size() == 1)//由0变1
{
_consume.notify_one();//唤醒
}
}
因为我们在处理队列信息的时候当队列为空,我们会停止,但后续又有消息来的时候,如果队列大小从0-1,那我们要唤醒,不让其一直卡住
最后正常收发信息
本文实现了服务器的逻辑类,包括并发控制等手段
❤️ 如果你觉得本文对你有帮助,欢迎点赞、评论与收藏。更多 c++ asio网络编程 开发知识,敬请关注后续更新!