文章目录
- 业务模块ChatService
-
- 网络模块-连接回调
- 实现解耦
- 业务头文件
- 公共头文件
- 业务函数定义文件
- 补充网路模块onMessgae()
- 测试
- 至此
- mysql数据库代码封装
-
- ORM (对象关系映射) 框架
- DAO (Data Access Object) 数据访问对象模式
- 分离数据层与业务层
- 数据库读取头文件
- 数据库类函数源文件
- 使用ORM, 实现注册业务
-
- **第一步:**
- **第二步:**
- **第三步:**
- 错误1:
- 小技巧–红色波浪线问题
- 补充并测试注册业务
- mysql连接失败解决
- 阶段总结-1
业务模块ChatService
网络模块-连接回调
先补充一下 这部分 ChatServer::onConnect 函数
先考虑一下:
不想在这里 写 ifelse等等, 调用登录业务啥的,注册服务啥的—- 这样不好—-这样把 网络模块和 业务模块 放一起了, 强耦合性, 并没有解耦
强耦合(Tight Coupling)是指 模块(类、组件、服务)之间高度依赖彼此的实现细节
实现解耦
不要在 网络模块里 直接调用业务模块的 代码和方法, 我们希望 在网络模块里, 是看不到业务模块的 代码
解耦
- 网络模块应避免直接依赖业务模块的代码或方法,保持模块间的隔离性。理想情况下,网络模块的代码中不应出现业务逻辑相关的引用。
怎么实现解耦呢?
- 可以通过回调、协议抽象或事件通知等方式实现解耦,确保网络模块只处理通信相关逻辑,业务模块负责具体的数据处理和业务规则。
在oop语言里边,要解偶模块之间的这个关系啊,一般有两种方法:
一种就是使用基于面向接口的编程
在C++里边没有所谓的接口。
实际上,在C++里边接口就是抽象类嘛,抽象基类,面向抽象基类的编程,或者就是基于这个。
…
英语: 在编程中,handler(中文常译为“处理器”或“处理程序”)通常指用于响应特定事件或管理特定任务的代码单元。
业务头文件
include/server/chatservice.hpp
// 仅需要一个 实例即可 因此使用单例模式
#ifndef CHATSERVICE_H
#define CHATSERVICE_H
#include <muduo/net/TcpConnection.h>
#include <unordered_map>
#include <functional>
using namespace std;
using namespace muduo;
using namespace muduo::net;
#include "json.hpp"
using namespace nlohmann;
//表示处理消息的事件回调方法类型
using MsgHandler = std::function<void(const TcpConnectionPtr &conn, json &js, Timestamp time)>; // #1
// 聊天服务器 业务类
class ChatService
{
public:
// 获取单例对象的接口函数 #6
static ChatService *instance();
// 处理的登录业务 #2
void login(const TcpConnectionPtr &conn, json &js, Timestamp time);
// 处理注册业务 #3
void reg(const TcpConnectionPtr &conn, json &js, Timestamp time);
//获取消息对应的处理器 #7
MsgHandler getHandler(int msgid);
private:
// 单例模式—-构造函数私有化,并写一个惟一的实例
ChatService(); // #5
// 存储消息id 和 其对应的 业务处理方法
unordered_map<int, MsgHandler> _msghandlermap; // #4
};
#endif
公共头文件
include/public.hpp
#ifndef PUBLIC_H
#define PUBLIC_H
/*
server 和 client 的公共文件
*/
enum EnMsgType // 枚举
{
LOGIN_MSG=1, // 与业务的login函数连接
REG_MSG //注册消息
};
#endif
业务函数定义文件
src/server/service.cpp
使用了 线程安全的 非互斥锁 懒汉式单例模式—详见c++笔记专栏
在 C++ 中,如果异常没有被捕获(uncaught exception),默认情况下会调用 std::terminate(),导致程序终止。这是 C++ 异常处理机制的核心行为之一。
“如果网络模块(如 muduo)调用了服务模块(如 ChatService),而服务模块抛出了异常,但网络模块没有捕获它,那整个进程就会崩溃。这种情况下,我们是不是必须在网络模块里处理服务模块抛出的异常?否则程序就完蛋了?”
答案是:是的! 如果异常没有被捕获,它会导致 程序终止(std::terminate 被调用),这在服务器程序中是 灾难性的(所有客户端连接都会断开,服务不可用)。
#include "chatservice.hpp"
#include "public.hpp"
#include <muduo/base/Logging.h>
using namespace muduo;
// 获取单例对象的 函数接口
ChatService *ChatService::instance()
{
static ChatService service; // 线程安全 懒汉式单例
return &service;
}
// 注册消息以及对应的 handler 回调
ChatService::ChatService()
{
// 网络模块和业务模块 解耦的核心
_msghandlermap.insert({LOGIN_MSG, std::bind(&ChatService::login, this, _1, _2, _3)});
_msghandlermap.insert({REG_MSG, std::bind(&ChatService::reg, this, _1, _2, _3)});
}
//获取消息对应的处理器
MsgHandler ChatService::getHandler(int msgid)
{
//记录错误日志, msgid 没有对应的事件回调
auto it = _msghandlermap.find(msgid);
if(it == _msghandlermap.end())
{
// 使用 muduo 自带的日志系统
// 不要endl, 已经封装了
// LOG_ERROR<<"msgid: "<<msgid<<"can not find Handler!";
return [=](const TcpConnectionPtr &conn, json &js, Timestamp time){
LOG_ERROR<<"msgid: "<<msgid<<"can not find Handler!";
};
}
else
{
return _msghandlermap[msgid];
}
}
// 处理登录业务
void ChatService::login(const TcpConnectionPtr &conn, json &js, Timestamp time)
{
LOG_INFO<<"do login service"; //测试用
}
// 处理注册业务
void ChatService::reg(const TcpConnectionPtr &conn, json &js, Timestamp time)
{
LOG_INFO<<"do reg service"; 测试用
}
补充网路模块onMessgae()
void ChatServer::onMessage(const TcpConnectionPtr& conn,
Buffer* buffer,
Timestamp time)
{
string buf = buffer->retrieveAllAsString();
// 数据反序列化
json js = json::parse(buf);
//达到的目的: 完全解耦网络模块的代码 和 业务模块的 代码
//通过js["msgid"]获取=>业务handler=>传 conn, js, time等信息
/*
这样, 业务仅有 两行代码, 没有任何业务层的方法(login,reg等)调用
仅需在 服务层内部, 做一个 业务相应的 回调
*/
//获取msgid 对应的处理器
auto msgHandler = ChatService::instance()->getHandler(js["msgid"].get<int>()); //json键是字符串, 要换成整型—-get方法, 会用就行
// 回调消息绑定好的 事件处理器, 来执行相应的业务处理
msgHandler(conn, js, time);
}
测试
注意: 前面的 CMakeLists.txt 要添加 json 头文件搜索路径
//client
{"msgid":1}
//server 打印
20250418 03:41:52.258725Z 31993 INFO do login service – chatservice.cpp:50
至此
每一个大的模块, 写完都要测试一下
网络 模块 完毕, 剩下的 将专注于 业务层 与 数据层!!!
mysql数据库代码封装
业务层处理 数据层—-> 要低耦合, 不要在 业务层 写数据库代码
ORM (对象关系映射) 框架
ORM (Object-Relational Mapping) 是一种编程技术,用于在面向对象编程语言中实现不兼容类型系统之间的数据转换,即在关系型数据库和面向对象语言之间建立映射关系。
DAO (Data Access Object) 数据访问对象模式
DAO 是一种核心的J2EE设计模式,用于抽象和封装对数据源的访问,将底层数据访问逻辑与业务逻辑分离。
1. 定义
DAO (Data Access Object) 是数据访问对象模式的简称,它:
- 位于业务逻辑与持久化存储之间
- 封装所有数据访问细节
- 为上层提供统一的数据操作接口
2. 核心组件
- DAO接口:定义数据操作的标准方法
- DAO实现类:针对不同数据源的具体实现
- 数据传输对象(DTO):在层间传输数据的载体
分离数据层与业务层
业务层操作的都是 对象 —> 使用ORM
数据层 封装了所有数据库操作 —> 使用DAO
数据库读取头文件
添加 数据层头文件搜索路径 以及 编译要加 mysqlclient
include_directories(${PROJECT_SOURCE_DIR}/include/server/db) #数据层头文件
target_link_libraries(Chatserver muduo_net muduo_base pthread mysqlclient)
一定要安装 libmysqlclient-dev, 要有这个库文件
sudo apt-get install libmysqlclient-devsudo apt-get install libmysqlclient-dev
include/db/db.h
#ifndef DB_H
#define DB_H
#include <mysql/mysql.h> //只有安装了 库, 才有这个头文件
#include <string>
using namespace std;
// 数据库配置信息
static string server = "127.0.0.1";
static string user = "root";
static string password = "123456";
static string dbname = "chat";
// 数据库操作类
class MySQL
{
public:
// 初始化数据库连接
MySQL();
~MySQL();
// 连接数据库
bool connect();
// 更新操作
bool update(string sql);
// 查询操作
MYSQL_RES *query(string sql);
private:
MYSQL *_conn;
};
#endif
数据库类函数源文件
要注意: CMakeLists.txt 编译时 要包含这个cpp
# 所有源文件
aux_source_directory(. SRC_LIST)
# 数据层步骤 需要添加
aux_source_directory(./db DB_LIST)
# 生成可执行
add_executable(Chatserver ${SRC_LIST} ${DB_LIST})
# 链接库
target_link_libraries(Chatserver muduo_net muduo_base pthread mysqlclient)
src/db/db.cpp
代码看不明白—-需要补充mysql知识
#include "db.h"
#include <muduo/base/Logging.h>
using namespace muduo;
// 初始化数据库连接
MySQL::MySQL()
{
_conn = mysql_init(nullptr);
}
// 释放数据库连接资源这里用UserModel示例,通过UserModel如何对业务层封装底层数据库的操作。代码示例如下:
MySQL::~MySQL()
{
if (_conn != nullptr)
mysql_close(_conn);
}
// 连接数据库
bool MySQL::connect()
{
MYSQL *p = mysql_real_connect(_conn, server.c_str(), user.c_str(),
password.c_str(), dbname.c_str(), 3306, nullptr, 0);
if (p != nullptr)
{
// c/c++代码默认的编码字符是ASCII, 如果不设置, 从mysql 拉下来的中文显示 不能正常用
mysql_query(_conn, "set names gbk");
}
return p;
}
// 更新操作
bool MySQL::update(string sql)
{
if (mysql_query(_conn, sql.c_str()))
{
LOG_INFO << __FILE__ << ":" << __LINE__ << ":"
<< sql << "更新失败!";
return false;
}
return true;
}
// 查询操作
MYSQL_RES *MySQL::query(string sql)
{
if (mysql_query(_conn, sql.c_str()))
{
LOG_INFO << __FILE__ << ":" << __LINE__ << ":"
<< sql << "查询失败!";
return nullptr;
}
return mysql_use_result(_conn);
}
使用ORM, 实现注册业务
第一步:
需要先定义类, 这个类 需要跟 数据库里的表 一一对应
sprintf 是一个用于格式化字符串的函数,它将格式化的数据写入一个字符串缓冲区,而不是像 printf 那样直接输出到标准输出(stdout)
以用户类为例:
include/server/user.hpp
// 映射类
// user的 ORM类, 用于映射字段
#ifndef USER_H
#define USER_H
#include <string>
using namespace std;
class User
{
public:
User(int id=-1, string name="", string password = "", string state="offline")
{
this->id = id;
this->name = name;
this->password=password;
this->state=state;
}
// 修改接口
void setId(int id) {this->id = id;}
void setName(string name) {this->name = name;}
void setPwd(string pwd) {this->password = pwd;}
void setState(string state) {this->state = state;}
// 获取
int getId() {return this->id ;}
string getName() {return this->name;}
string getPwd() {return this->password;}
string getState() {return this->state;}
private:
int id;
string name;
string password;
string state;
};
#endif
第二步:
修改 用户类的 头文件
include/server/usermodel.hpp
// 实际操作 表 的 类—-增删改查
#ifndef USERMODEL_H
#define USERMODEL_H
#include "user.hpp"
//User 表的 数据操作类
class UserModel
{
public:
//user表的 增加方法
bool insert(User &user);
};
#endif;
第三步:
定义 修改 类 的 函数
db.h 与 db.cpp 添加 获取数据库对象的 方法
MYSQL *MySQL::getConnection()
{
return _conn;
}
src/server/usermodel.cpp
#include "usermodel.hpp"
#include "db.h"
#include <iostream>
using namespace std;
//user表的 增加方法
bool UserModel::insert(User &user)
{
//1. 组装 sql语句
char sql[1024]={0};
sprintf(sql, "insert into user(name, password, state) values('%s', '%s', '%s')", user.getName().c_str(), user.getPwd().c_str(), user.getState().c_str()); // 注意 %s, 每个要单独''起来, 整个''是连起来的一个字符串
MySQL mysql;
if(mysql.connect())
{
if(mysql.update(sql))
{
//获取插入成功的 用户数据生成的 主键id
user.setId(mysql_insert_id(mysql.getConnection()));
return true;
}
}
return false;
}
错误1:
sprintf(sql, "insert into user(name, password, state) values('%s', '%s', '%s')", user.getName().c_str(), user.getPwd().c_str(), user.getState().c_str());
insert into user(name, password, state) values('%s', '%s', '%s'); // 这是mysql中的添加语句, user与表名必须一致!!
小技巧–红色波浪线问题
在vscode里, 有时候cmake能正常编译, 但是 编写代码 有的 会出现 红色波浪线, 尽管包含了头文件, 它也还在 , 这个 对于 有点强迫症的, 可忍受不了!!!
修改一下 .vscode的 json文件: c_cpp_properties.json
"includePath": [
"${workspaceFolder}/**"
}
// 改成 下面这个, 具体看自己的 头文件在哪
"includePath": [
"${workspaceFolder}/**",
"${workspaceFolder}/include/**",
"${workspaceFolder}/thirdparty/**"
],
更简单的: 让 cmake 管理 路径
"configurationProvider": "ms-vscode.cmake-tools" // 关键!让CMake接管配置
还有一种是 右下角 会有 IntelliSense 提示 未配置, 黄色感叹号, 可以点击 以下, 选择 cmake , 也能解决
一共三种 方法, 那种能用 用哪个
补充并测试注册业务
model 给 业务 提供的 都是 对象!!
select * from user; //mysql里查看 user表的全部信息
第一步: 业务中添加 数据操作类对象
chatservice.hpp
private:
// 注册业务测试
// 数据操作类对象
UserModel _usermodel;
第二步:补充 注册业务代码
src/server/chatservice.cpp
// 处理注册业务
void ChatService::reg(const TcpConnectionPtr &conn, json &js, Timestamp time)
{
// LOG_INFO<<"do reg service"; 测试用
// 注册仅需要 name password
string name = js["name"];
string password = js["password"];
User user;
user.setName(name);
user.setPwd(password);
bool state = _usermodel.insert(user);
if(state)
{
//注册成功
json response;
response["msgid"] = REG_MSG_ACK;
response["errno"] = 0;
response["id"] = user.getId();
conn->send(response.dump());
}
else
{
//注册失败
json response;
response["msgid"] = REG_MSG_ACK;
response["errno"] = 1;
conn->send(response.dump());
}
}
mysql连接失败解决
- auth_socket 插件只允许通过 Unix Socket 本地连接(不能用于 TCP/IP 或远程连接)
- 如果你的代码使用 127.0.0.1(TCP/IP)连接,auth_socket 会拒绝认证 (即使是在本机,127.0.0.1 也算 TCP/IP 连接,不是 Unix Socket)
查看当前认证插件:
SELECT user, plugin FROM mysql.user;
修改 root 认证方式(两种选择):
-
改为 mysql_native_password(兼容旧客户端):
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '123456';
FLUSH PRIVILEGES;
阶段总结-1
自己的 语言:
db.h—> 提供 数据库 基本连接,查询等的 声明
db.cpp—> 提供db.h 的具体实现
user.hpp —> 数据库的 模型类 , 由于内容少, 直接 包含了 具体实现
usermodel.hpp —> 提供 给 业务 的 对象, 仅包含 操作方法的 声明
usermodel.cpp —> 对 操作方法的 具体实现
在 业务模块中, 例如 chatservice.hpp 和 cpp, 都是 仅接收了 对象, 调用 对应方法, 没有设计 具体实现, 达到了 解耦性
ai 润色:
- db.h:声明数据库基本连接、查询等接口
- db.cpp:实现数据库核心功能的具体细节
- user.hpp:定义与数据库表对应的模型类(采用头文件实现模式)
- usermodel.hpp:声明面向业务的操作接口
- usermodel.cpp:实现具体的业务逻辑操作
- chatservice.hpp/cpp:通过接口调用业务方法,完全隔离具体实现
评论前必须登录!
注册