要考虑细一点: 登陆成功, 登录失败, 哪里失败了, 登录成功更新状态
chatservice.cpp
// 处理登录业务
void ChatService::login(const TcpConnectionPtr &conn, json &js, Timestamp time)
{
// LOG_INFO<<"do login service"; //测试用
//int id = js["id"];
int id = js["id"].get(); // js字符串 转成整型
string pwd = js["password"];
User user = _usermodel.query(id);
if (user.getId() == id && user.getPwd() == pwd) // id默认为-1
{
// 登陆成功
if (user.getState() == "online") // 用户在线, 不允许重复登录
{
json response;
response["msgid"] = LOGIN_MSG_ACK;
response["errno"] = 2;
response["errmsg"] = "用户已经登录, 不允许重复登录!";
conn->send(response.dump());
}
else
{
user.setState("online"); //登录成功, 更新状态
_usermodel.updateState(user); // 刷新状态
json response;
response["msgid"] = LOGIN_MSG_ACK;
response["errno"] = 0;
response["id"] = user.getId();
response["name"] = user.getName();
conn->send(response.dump());
}
}
else
{
// 登录失败
if (user.getId() == -1) // 没找到
{
json response;
response["msgid"] = LOGIN_MSG_ACK;
response["errno"] = 1;
response["errmsg"] = "用户名不存在!";
conn->send(response.dump());
}
else if (user.getPwd() != pwd)
{
json response;
response["msgid"] = LOGIN_MSG_ACK;
response["errno"] = 3;
response["errmsg"] = "密码错误!";
conn->send(response.dump());
}
}
}
usermodel.hpp
// 更新 用户状态信息
bool updateState(User user);
usermodel.cpp
User UserModel::query(int id)
{
//1. 组装 sql语句
char sql[1024]={0};
sprintf(sql, "select * from user where id=%d", id);
MySQL mysql;
if(mysql.connect())
{
MYSQL_RES *res = mysql.query(sql); // 数据库api
if(res!=nullptr)
{
MYSQL_ROW row = mysql_fetch_row(res);
if(row != nullptr)
{
User user;
user.setId(atoi(row[0]));
user.setName(row[1]);
user.setPwd(row[2]);
user.setState(row[3]);
mysql_free_result(res); // 释放一下资源, 否则内存不断泄露
return 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;
}
reason: [json.exception.type_error.302] type must be number, but is null
检查一下, 本代码 使用 用户id 进行登录, 而不是用户名!
测试的话, 事先查一下 用户id
{"msgid":1,"id":22,"password":"101010"}
.get
–js字符串转整型–>错误, 不支持 字符串转int
get()
不是设计用来做隐式转换的,而是严格类型检查工具。int id = js["id"].get(); // js字符串 转成整型
{"msgid":1,"id":22,"password":"101010"}
//还没有设计下线 状态改变, 自己多试试那几种登录情况
js["id"]
转 int
的几种方式
js["id"].get()
"id":22
→ 22
"id":"22"
→ 抛出异常(需 try-catch
)int id = js["id"]
"id":22
→ 22
(隐式转换)"id":"22"
→ 编译错误js.value("id", 0)
"id":22
→ 22
"id":"22"
→ 返回默认值 0
(不抛异常)长连接的特点
聊天服务器为何适合长连接
服务器必须主动推:当用户A发给用户B时,服务器需要立刻找到用户B的连接,把消息推过去
**解决方案:**维护一个"用户ID ⇨ 连接"的映射表
include/server/chatservice.hpp
//存储在线用户的 连接信息
unordered_map _userConnMap; // 只有登录成功了, 才会存储
src/server/chatservice.cpp
//登陆成功里 添加---具体看自己代码, 哪里是登陆成功
// 存储用户登录信息
// _userConnMap.insert({id, conn});
_userConnMap.emplace(id,conn);
onMessage()—> 在网络库中, 会被多线程调用, 业务层要 考虑 现成安全
在线用户信息 在 运行中, 是不断改变的, 一定要考虑 线程安全!!
锁的力度 一定要小!!!
include/server/chatservice.hpp
//定义互斥锁, 保证_userConnMap安全
mutex _connMutex;
//存储在线用户的 连接信息
unordered_map _userConnMap; // 只有登录成功了, 才会存储
src/server/chatservice.cpp
使用 作用域, 时加锁力度 尽可能小!!
// 存储用户登录信息
// _userConnMap.insert({id, conn});
{
lock_guard lock(_connMutex); // 自动解锁
_userConnMap.emplace(id,conn);
}
必须是客户端 断开连接, 服务端不能断开, 就算是 服务端断开连接, 客户端必须先退出, 否则 这个 修改状态 无法完成
客户端只要断开连接, 就需要修改用户的 状态
include/server/chatservice.hpp
// 客户端异常退出处理
void clientCloseException(const TcpConnectionPtr &conn);
src/server/chatservice.cpp
// 客户端异常退出处理
void ChatService::clientCloseException(const TcpConnectionPtr &conn)
{
User user;
{
lock_guard lock(_connMutex);
for (auto it = _userConnMap.begin(); it != _userConnMap.end(); ++it)
{
if (it->second == conn)
{
user.setId(it->first);
_userConnMap.erase(it);
break;
}
}
}
if(user.getId()!=-1)
{
//更新用户状态信息
user.setState("offline");
_usermodel.updateState(user);
}
}
src/server/chatserver.cpp
void ChatServer::onConnect(const TcpConnectionPtr& conn)
{
// 客户端断开连接
if(!conn->connected())
{
//客户端异常退出
ChatService::instance()->clientCloseException(conn);
conn->shutdown();
}
}
一定要退出 telnet
完成
msgid: //说明是什么业务
fromid: id
fromname:
to: id
msg: "......"
客户端发送聊天消息 → 服务器收到并解析
首先判断接收者(to
)是否在线:
为保证线程安全,访问用户连接表时加锁(用 lock_guard
)
include/public.hpp
ONE_CHAT_MSG //一对一聊天
include/server/chatservice.hpp
// 一对一 聊天业务
void oneChat(const TcpConnectionPtr &conn, json &js, Timestamp time);
src/server/chatservice.cpp
// 一对一 聊天业务 并进行处理器绑定
void ChatService::oneChat(const TcpConnectionPtr &conn, json &js, Timestamp time)
{
int toid = js["to"].get();
{
lock_guard lock(_connMutex);
auto it = _userConnMap.find(toid);
if(it!=_userConnMap.end())
{
//在线, 转发消息
it->second->send(js.dump());
return;
}
}
//不在线, 存储离线消息
}
{"msgid":1,"id":22,"password":"101010"}
{"msgid":3,"name":"hzh1","password":"101010"}
{"msgid":1,"id":23,"password":"101010"}
{"msgid":5,"id":22,"from":"hzh","to":23,"msg":"i am hzh!hello!"}
{"msgid":5,"id":23,"from":"hzh1","to":22,"msg":"我不认识你!"}
offline_message
表,包含 user_id
和 message
两个字段。定义了
OfflineMessage
类来封装与离线消息表的交互。这样做的目的是将数据库操作与业务逻辑解耦,符合“分层设计”的原则。通过这一层的封装,业务代码与数据库操作分离,减少了耦合,提高了代码的可维护性。
OfflineMessage
类,提供方法:
insert
: 存储离线消息。remove
: 删除某个用户的所有离线消息(避免用户登录后再收到历史消息)。query
: 查询用户的所有离线消息,返回一个 vector
容器。offline_message
表。
offlinemessage
中插入一条新记录。即使 userid
相同,每次插入的消息都会作为一条独立的记录存储在表中,因为没有任何逻辑限制userid
的重复。vector
, 每条都加进去还不考虑 在另一台 上, 需要同步消息
include/server/offlinemessagemodel.hpp
#ifndef OFFLINEMESSAGEMODEL_H
#define OFFLINEMESSAGEMODEL_H
#include
#include
using namespace std;
// 提供离线消息表的操作接口方法
class OfflineMessageModel
{
public:
// 添加离线消息
void insert(int userid, const string &msg);
// 删除离线消息
void remove(int userid);
// 查询离线消息
vector query(int userid);
};
#endif
src/server/offlinemessagemodel.cpp
#include "offlinemessagemodel.hpp"
#include "db.h"
#include
// 添加离线消息
void OfflineMessageModel::insert(int userid, const string &msg)
{
// 1. 创建数据库连接
// 2. 执行 SQL 语句插入离线消息
// 3. 关闭数据库连接
// 4. 返回插入结果
char sql[1024] = {0};
sprintf(sql, "insert into offlinemessage (userid, message) values (%d, '%s')", userid, msg.c_str());
MySQL mysql;
if (mysql.connect())
{
mysql.update(sql);
}
}
// 删除离线消息
void OfflineMessageModel::remove(int userid)
{
// 1. 创建数据库连接
// 2. 执行 SQL 语句删除离线消息
// 3. 关闭数据库连接
// 4. 返回删除结果
char sql[1024] = {0};
sprintf(sql, "delete from offlinemessage where userid = %d", userid);
MySQL mysql;
if(mysql.connect())
{
mysql.update(sql);
}
}
// 查询离线消息
vector OfflineMessageModel::query(int userid)
{
// 1. 创建数据库连接
// 2. 执行 SQL 语句查询离线消息
// 3. 关闭数据库连接
// 4. 返回查询结果
char sql[1024] = {0};
sprintf(sql, "select message from offlinemessage where userid = %d", userid);
vector vec;
MySQL mysql;
if(mysql.connect())
{
MYSQL_RES *res = mysql.query(sql); // 数据库api
if(res!=nullptr)
{
MYSQL_ROW row;
while((row = mysql_fetch_row(res)) != nullptr)
{
vec.push_back(row[0]); // 将查询到的消息添加到结果向量中
}
mysql_free_result(res); // 释放一下资源, 否则内存不断泄露
}
return vec;
}
return vec;
}
include/server/chatservice.hpp
#include
// 离线消息操作对象
OfflineMessageModel _offlineMsg;
src/server/chatservice.cpp
//补充 点对点聊天的 离线存储业务
//不在线, 存储离线消息
_offlineMsg.insert(toid, js.dump());
//补充 登录业务, 登陆成功后, 查询是否有离线消息
// 查询离线消息
vector vec = _offlineMsg.query(id);
if (!vec.empty())
{
response["offlinemsg"] = vec; // 离线消息 js可以直接序列化容器
// 离线消息发送完毕, 删除离线消息
_offlineMsg.remove(id);
}
新增了文件, 容易出现 链接错误, 因为找不到
需要把 build里面的全部删了, 重新 cmake
{"msgid":1,"id":22,"password":"101010"}
{"msgid":5,"id":22,"from":"hzh","to":23,"msg":"i am hzh!hello!"}
至此, 数据库 离线消息 表已经存储了, 可以自行查看
{"msgid":1,"id":23,"password":"101010"}
至此, 数据库 离线消息 表已经 删除了
之前,客户端异常下线主要导致了用户状态的变化。然而,目前更常遇到的是服务器端异常,例如无法识别字符或格式错误,这些问题通常会导致服务器异常下线。
这种异常 导致 用户 状态 没改变, 所以 需要 进行完善!!
目前 仅处理 服务端 ctrl+c 的异常, 暂不处理 客户端发过来消息 不对导致的 服务器退出问题
使用信号捕捉函数: signal------这个实际一般不推荐
(一般使用sigaction()函数)-----老师没用这个
捕获终止信号:使用 signal.h
来捕获 Ctrl+C 终止信号(SIGINT),并在信号处理函数中进行用户状态的重置。
编写重置函数:
chat service
中添加 reset
方法,用来重置用户的状态。reset
方法调用 user model
中的 update
方法,将所有在线(online)状态的用户状态更新为离线(offline)。确保正确更新数据库:通过 SQL 语句 UPDATE user SET state = 'offline' WHERE state = 'online'
来批量更新数据库中用户的状态。
测试过程:
main.cpp
仅负责信号捕获,ChatService
处理业务逻辑,UserModel
负责数据库操作
src/server/main.cpp
使用信号捕捉
#include
#include "chatservice.hpp"
void resetHandler(int)
{
ChatService::instance()->reset();
exit(0);
}
//main
signal(SIGINT, resetHandler);
include/server/chatservice.hpp
// 服务器异常退出处理, 重置用户状态函数
void reset();
src/server/chatservice.cpp
// 服务器异常退出处理, 重置用户状态函数
void ChatService::reset()
{
// 更新所有用户的状态----把在线用户都设置为离线
_usermodel.resetState();
}
include/server/usermodel.hpp
// 重置用户状态
void resetState();
src/server/usermodel.cpp
// 重置用户状态
void UserModel::resetState()
{
//1. 组装 sql语句
char sql[1024]={0};
sprintf(sql, "update user set state='offline' where state='online'");
MySQL mysql;
if(mysql.connect())
{
mysql.update(sql);
}
}
{"msgid":1,"id":22,"password":"101010"}
服务器: ctrl+c