当前位置: 首页 > ai >正文

C++ 网络编程(12)利用单例逻辑实现逻辑类

🎯 C++ 网络编程(12)服务器逻辑层单例模式设计
📅 更新时间:2025年6月15日
🏷️ 标签:C++ | Boost.Asio | 网络编程 | 单例模式 | 并发 | 加锁

文章目录

  • 前言
  • 一、什么是单例模式?
  • 二、单例模板类
    • 1.代码部分:
    • 2.细节点详解
      • 问题1
      • 问题2
      • 问题3
      • 问题4
        • 关键点
      • 问题5
  • 三、LogicSystem单例类
    • 1.LogicSystem类
    • 2.代码详解
  • 四、LogicNode封装消息节点
  • 五、实现LogicSystem.cpp
  • 总结


前言

前文第11章我们完善了消息节点,使用了tlv协议的格式去发送消息,
今天我们来实现用单例模式实现逻辑类


一、什么是单例模式?

单例模式(Singleton Pattern) 是一种创建型设计模式,它的核心目的是:

✅ 保证一个在整个程序中只能有一个实例
✅ 并且提供一个全局访问点来获取这个实例

🧠 通俗理解
你可以把“单例”理解成:

程序里的 “唯一老大”,这个类你只能创建一次
再创建?不行!得给你原来的那个!

就像你电脑系统里的「任务管理器」只能打开一个

二、单例模板类

接下来我们实现一个单例模板类,因为服务器的逻辑处理需要单例模式,后期可能还会有一些模块的设计也需要单例模式,所以先实现一个单例模板类,然后其他想实现单例类只需要继承这个模板类即可

1.代码部分:

#pragma once
#include<memory>
#include<mutex>
#include<iostream>
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<T>();//错误!!!})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;//外部定义

2.细节点详解

问题1

为什么要将
默认构造拷贝构造拷贝赋值写在protected权限下

因为我们要做的是单例模式,这样可以禁止外部随意创建或复制对象

Singleton(const Singleton<T>&) = delete

可以禁止下面这种情况发生

Singleton<MyClass> s2 = s1;

然后这样写

Singleton& operator=(const Singleton<T>&) = delete

禁止别人写 s2 = s1;
拷贝赋值同样可能让多个对象共享不一致状态,也破坏了单例性质

问题2

默认构造函数的写法问题
下面两种写法区别在哪?

Singleton() = default;Singleton(){}

虽然都是默认构造函数而且什么都没有,但第一个是默认第二个是用户自己写的

前者真正调用编译器生成的默认实现
保留了默认构造函数的所有特性(比如更高的性能、更强的编译器优化)

Singleton() = default; 是在明确告诉编译器:“我就是要用默认构造函数,不加任何逻辑”

Singleton() {} 虽然也“能用”,但从语义上看不清楚你是故意要自定义、还是写了个空的

问题3

为什么成员变量智能指针对象 _instance和 成员函数GetInstance都要用static静态变量

🧠 核心:

因为 单例模式的目标 是:
类只存在一个全局唯一对象,且不需要实例化这个类就能访问它

static 恰好实现了这个目标!

下面这个写法
保证了这个类全局只有一份实例指针

static std::shared_ptr<T> _instance;

然后后面定义的函数也是静态的,因为一开始我们没有实例化对象也可以直接调用

static std::shared_ptr<T> GetInstance()

问题4

为什么要用 std::once_flagstd::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 失去作用,线程安全就没法保证了

问题5

为什么在创建_instance的时候这种写法是错误的

_instance=std::make_shared<T>();//错误!!!

因为我们使用make_shared的时候,内部会自动调用 T 的构造函数,但我们一开始将构造函数设置为 private
导致无法调用构造函数

所以我们用下面这种写法

_instance = shared_ptr<T>(new T);

三、LogicSystem单例类

我们实现逻辑系统的单例类,继承自Singleton<LogicSystem>,这样LogicSystem构造函数拷贝构造函数就都变为私有的了,因为基类的构造函数和拷贝构造函数都是私有的。另外LogicSystem也有了基类的成员_instanceGetInstance函数。从而达到单例效果

1.LogicSystem类

#pragma once
#include"Singleton.h"
#include<queue>
#include<thread>
#include"CSession.h"
#include<map>
#include<functional>
#include"const.h"
#include<json/json.h>
#include<json/value.h>
#include<json/reader.h>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;
};

2.代码详解

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 <iostream>
#include <functional>
#include <memory>
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++ 标准库中的一个同步原语,属于 <condition_variable> 头文件。用于在多线程编程中实现线程之间的条件等待和通知机制,常常与互斥锁(如 std::mutex)一起使用。

主要功能:
等待条件:线程可以在某个条件未满足时阻塞,等待其他线程通知。
通知:当条件满足时,另一个线程可以唤醒等待的线程

四、LogicNode封装消息节点

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 的元素,负责传递消息和上下文

五、实现LogicSystem.cpp

构造函数中我们将_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网络编程 开发知识,敬请关注后续更新!

http://www.xdnf.cn/news/14286.html

相关文章:

  • 初探 Pytest:编写并运行你的第一个测试用例
  • .net6接口多个实现类使用特性标记并解析
  • React-router实现原理剖析
  • 基于SVD的推荐系统:详尽的原理与实践解析
  • 网络安全相关概念与技术详解
  • 高速 PCB 设计的材料选择,第 2 部分
  • ubuntu 22.04 安装部署kibana 7.10.0详细教程
  • Linux——libevent库
  • Python实例题:Python计算曲线曲面积分
  • 网页后端开发(基础2--maven单元测试)
  • useMemo vs useCallback:React 性能优化的两大利器
  • 如何通过 noindex 阻止网页被搜索引擎编入索引?
  • 哈希函数结构:从MD到海绵的进化之路
  • AudioLab安卓版:音频处理,一应俱全
  • Redis中的zset的底层实现
  • SeaTunnel与Hive集成
  • Chapter12-API testing
  • 极客时间《后端存储实战课》阅读笔记
  • 快速使用 Flutter 中的 SnackBar 和 Toast
  • Vue-Leaflet地图组件开发(四)高级功能与深度优化探索
  • 【JAVA】48. Semaphore信号量控制资源并发访问
  • Python函数基础知识(2/3)
  • 电阻篇---下拉电阻
  • 3_STM32开发板使用(STM32F103ZET6)
  • Spring Boot诞生背景:从Spring的困境到设计破局
  • MAZANOKE:一款隐私优先的浏览器图像优化工具及Docker部署指南
  • 基于AWS无服务器架构的区块链API集成:零基础设施运维实践
  • Java面试题:分布式ID时钟回拨怎么处理?序列号耗尽了怎么办?
  • VINS-Fusion 简介、安装、编译、数据集/相机实测
  • 传统数据仓库正在被 Agentic AI 吞噬