CPP集群聊天服务器开发实践(一):用户注册与登录
目录
1 客户端用户注册与登录
1.1 主要思想
1.2 网络层
1.3 业务层
1.4 数据层
1.5 测试结果
1 客户端用户注册与登录
1.1 主要思想
实现网络层、业务层、数据层的解耦,提高系统的可维护性。
网络层:主要实现对客户端连接、客户端读写请求的捕获与回调,将其分发到多个线程中执行。
业务层:主要实现客户端读写请求回调的具体操作,当前阶段主要包含:登录业务、注册业务、用户异常退出业务
数据层:主要实现数据库中表的CUAD操作(增删改查)
1.2 网络层
利用muduo网络库实现epoll+线程池模式的网络模式。此模式具有模板化的特性,使用时可以原封不动的照搬。其原理可以查看前面关于muduo网络库以及epoll原理讲解的文章。其主要分为以下几个部分:
(1)组合Tcpserver对象以及Eventloop对象
(2)public下定义构造函数,实现上述对象的初始化以及处理连接的onConnection和处理读写的onMessage回调函数的注册
(3)private下定义onConnection和onMessage回调函数的具体实现
其中,在onConnection中:主要实现用户的连接以及用户异常关闭的情况,用户的连接直接调用muduo库的connected()方法;用户异常关闭则调用业务层定义的方法。
在onMessage中,为了实现网络层和业务层的解耦,通过定义哈希map _msgHandlerMap 将用户操作(msgid表征)和对应的回调处理进行一对一映射。即根据用户发来的msgid来获取对应的handler,获取到对应的handler后执行相应的业务。
整体来说由于muduo库强大的功能,实现比较简单,具体源码如下:
#include"chatserver.hpp"
#include"json.hpp"
#include<functional>
#include<string>
#include"chatservice.hpp"
using namespace std;
using namespace placeholders;
using json=nlohmann::json;
ChatServer::ChatServer(EventLoop *loop,
const InetAddress &listenAddr,
const string &nameArg) : _server(loop, listenAddr, nameArg), _loop(loop)
{
//注册连接回调
_server.setConnectionCallback(std::bind(&ChatServer::onConnection,this,_1));
//注册读写回调
_server.setMessageCallback(std::bind(&ChatServer::onMessage,this,_1,_2,_3));
//设置线程数量
_server.setThreadNum(4);
}
void ChatServer::start()
{
_server.start();
}
// 上报连接相关信息的回调函数
void ChatServer::onConnection(const TcpConnectionPtr &conn)
{
//客户端断开连接
if (!conn->connected())
{
//客户端异常关闭
ChatService::instance()->clientCloseException(conn);
conn->shutdown();
}
}
// 上报读写事件相关信息的回调函数
void ChatServer::onMessage(const TcpConnectionPtr &conn,
Buffer *buffer,
Timestamp time)
{
string buf=buffer->retrieveAllAsString();
//数据的反序列化
json js=json::parse(buf);
//实现网络模块和业务模块的解耦:通过hs["msgid"]调取对应业务handler(回调业务代码),可以将conn,js,time传给handler,执行回调
//解耦操作一般两种:1.面向基类2.回调操作
//js["msgid"].get<int>():利用get方法将json对象转换为int类型,get方法提供的是模板
auto msgHanndler=ChatService::instance()->getHandler(js["msgid"].get<int>());
//回调消息绑定好的事件处理器,来执行相应的业务处理
msgHanndler(conn,js,time);
}
1.3 业务层
上文提到,网络层和业务层解耦的关键就是回调操作。网络层获取到用户发来的msgid后,根据_msgHandlerMap找到对应的handler,每个handler都和对应的login或者register操作进行绑定,找到对应的handler后即执行相应的操作。其主要分为以下几个部分:
(1)构造函数中注册消息(msgid)以及对应的handler回调操作
(2)处理登录业务:根据id找到对应的user对象,验证用户是否已经登录,之后验证password是否正确,登录成功修改用户状态并进行响应
(3)处理注册业务:根据用户name 和 password进行注册,将自增的id作为账号返回给用户
(4)处理用户异常退出业务:定义关于用户id和状态信息的映射_userConnMap,从哈希表中获取异常关闭的用户id,修改数据库的状态,并删除哈希表中对应条目
业务层的实现主要靠回调操作,同时涉及单例模式、互斥锁等一些知识,具体可以查看专栏前面分享的文章。
具体源码如下:
#include"chatservice.hpp"
#include"public.hpp"
//利用muduo库的封装好的日志输出
#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())
{
return [=](const TcpConnectionPtr &conn, json &js, Timestamp time)
{
LOG_ERROR << "msgid: " << msgid << "can not find handler";
};
}
else
{
return _msgHandlerMap[msgid];
}
}
// 处理登录业务 id+pwd 验证pwd
void ChatService::login(const TcpConnectionPtr &conn, json &js, Timestamp time)
{
LOG_INFO<<"DO LOGIN SERVICE!";
int id=js["id"];
string pwd=js["password"];
//查询id对应的user对象
User user = _userModel.query(id);
if(user.getId()==id&&user.getPwd()==pwd){
if(user.getState()=="online"){
//该用户已经登录,不允许重复登录
json response;
response["msgid"]=LOGIN_MSG_ACK;
//响应error number如果为0,表示业务成功
response["errno"] = 2;
response["errmsg"] = "该账号已经登录,请重新输入新账号";
//组装好json通过网络返回给客户端
conn->send(response.dump());//数据序列化
}
else{
//登录成功,记录用户连接信息
/*群组聊天时,onMessage会被多线程调用,同时这个记录用户连接的map也会被多线程调用
并且这个map会不断发生变化,需要考虑 线程安全 的问题*/
{
lock_guard<mutex> lock(_connMutex);
_userConnMap.insert({id,conn});
}
//登录成功,更新用户state->online
user.setState("online");
_userModel.updateState(user);
json response;
response["msgid"]=LOGIN_MSG_ACK;
//响应error number如果为0,表示业务成功
response["errno"] = 0;
response["id"]=user.getId();
response["name"]=user.getName();
//组装好json通过网络返回给客户端
conn->send(response.dump());//数据序列化
}
}
else{
//用户不存在或者用户存在密码错误,登录失败
json response;
response["msgid"]=LOGIN_MSG_ACK;
//响应error number如果为0,表示业务成功
response["errno"] = 1;
response["errmsg"] = "用户名或者密码错误";
//组装好json通过网络返回给客户端
conn->send(response.dump());//数据序列化
}
}
// 处理注册业务
void ChatService::reg(const TcpConnectionPtr &conn, json &js, Timestamp time)
{
LOG_INFO << "DO REGISTER SERVICE!";
string name = js["name"];
string pwd = js["password"];
User user;
user.setName(name);
user.setPwd(pwd);
bool state = _userModel.insert(user);
if (state)
{
// 注册成功,返回客户端json消息,并把自增的id返回给用户当作账号
json response;
response["msgid"]=REG_MSG_ACK;
//响应error number如果为0,表示业务成功
response["errno"] = 0;
response["id"]=user.getId();
//组装好json通过网络返回给客户端
conn->send(response.dump());//数据序列化
}
else
{
// 注册失败
json response;
response["msgid"]=REG_MSG_ACK;
//响应error number如果为1,表示业务不成功
response["errno"] = 1;
//组装好json通过网络返回给客户端
conn->send(response.dump());//数据序列化
}
}
//处理客户端异常退出
void ChatService::clientCloseException(const TcpConnectionPtr &conn)
{
User user;
//做两件事:1.用户数据库state->offline 2._userConnMap中用户信息删除
{
lock_guard<mutex> lock(_connMutex);
//遍历_userConnMap
for(auto it=_userConnMap.begin();it!=_userConnMap.end();++it){
if(it->second==conn){
user.setId(it->first);
//从map表删除用户的连接信息
_userConnMap.erase(it);
break;
}
}
}
//更新用户的状态信息
if(user.getId()!=-1){
user.setState("offline");
_userModel.updateState(user);
}
}
1.4 数据层
数据层与业务层解耦的关键在于ORM架构的实现,可以有效防止操作数据库过程中重复的sql代码,通过将数据库的信息封装为对象进行操作,其核心主要包含dp.cpp(数据库连接、数据库更新、数据库查询功能);user.hpp(数据表的对象封装);usermodel.cpp(对user对象的增删改查操作)。主要包含以下内容:
(1)dp.cpp:可以理解为数据库的底层操作,主要依赖于mysql库提供的方法,实现数据库的连接、更新、查询操作
(2)user.hpp:主要是对user表的对象实例化封装,根据表的字段定义变量(私有),并提供get、set的公有化方法
(3)usermodel.cpp:这一层与业务层紧密相关,根据业务需要对user对象提供插入、查询、更新状态的方法,并调用底层的mysql库方法(dp.cpp中的内容)。具体步骤包含:a. 组装sql语句 b. 连接数据库 c.调用底层操作。
具体源码如下:
dp.cpp:
#include"db.h"
#include<muduo/base/Logging.h>
//数据库配置信息
static string server = "127.0.0.1";
static string user = "root";
static string password = "123456";
static string dbname = "chat";
//初始化数据库连接,为连接开辟资源
MySQL::MySQL()
{
_conn = mysql_init(nullptr);
}
//释放数据库连接资源
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");
LOG_INFO << "connect mysql success!";
}
else
{
LOG_INFO << "connect mysql fail!";
}
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);
}
//获取连接
MYSQL *MySQL::getConnection()
{
return _conn;
}
user.hpp:
#ifndef USER_H
#define USER_H
#include<string>
using namespace std;
/*
mysql> SHOW COLUMNS FROM user;
+----------+--------------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+----------+--------------------------+------+-----+---------+----------------+
| id | int | NO | PRI | NULL | auto_increment |
| username | varchar(50) | NO | UNI | NULL | |
| password | varchar(50) | NO | | NULL | |
| state | enum('online','offline') | YES | | offline | |
+----------+--------------------------+------+-----+---------+----------------+
*/
//定义数据库对象,将数据库信息整合为一个对象提交给业务层
//匹配User表的ORM类
class User{
public:
User(int id = -1, string name = "", string pwd = "", string state = "offline")
{
this->id = id;
this->name = name;
this->password = pwd;
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
usermodel.cpp:
#include"usermodel.hpp"
#include"db.h"
#include<iostream>
#include<muduo/base/Logging.h>
using namespace std;
//User表的增加方法
bool UserModel::insert(User &user)
{
//1. 组成sql语句
char sql[1024] = {0};
sprintf(sql, "insert into user(username,password,state) values('%s', '%s', '%s')",
user.getName().c_str(), user.getPwd().c_str(), user.getState().c_str());
LOG_INFO<<sql;
//2.连接数据库
MySQL mysql;
if (mysql.connect())
{
if(mysql.update(sql)){
//获取插入成功的用户数据生成的主键id
user.setId(mysql_insert_id(mysql.getConnection()));
return true;
}
}
return false;
}
//根据用户号码查询用户信息
User UserModel::query(int id)
{
//1. 组成sql语句
char sql[1024] = {0};
sprintf(sql, "select * from user where id = %d",id);
//2.连接数据库
MySQL mysql;
if (mysql.connect())
{
MYSQL_RES* res= mysql.query(sql);
//res不为空,查询成功
if(res!=nullptr){
//返回查到的行,得到的是字符串,可以用[ ]取值
MYSQL_ROW row = mysql_fetch_row(res);
if(row!=nullptr){
User user;
//Convert a string to an integer.
user.setId(atoi(row[0]));
user.setName(row[1]);
user.setPwd(row[2]);
user.setState(row[3]);
//res使用指针动态分配资源,需要释放,防止内存泄漏
mysql_free_result(res);
return user;
}
}
}
//返回默认user,匿名对象
return User();
}
//更新用户的状态信息
bool UserModel::updateState(User user)
{
//1. 组成sql语句
char sql[1024] = {0};
sprintf(sql, "update user set state='%s' where id = %d",
user.getState().c_str(), user.getId());
MySQL mysql;
if(mysql.connect()){
if(mysql.update(sql)){
return true;
}
}
return false;
}
1.5 测试结果
主要针对用户注册、登录、用户异常退出业务的测试。
可以看到,客户端发送json字符串可以实现响应的业务,同时用户异常退出时数据库相应的状态也会变为offline。