本项目,利用moduo网络库,利用js进行网络传输,njinx进行负载均衡,redis实现消息订阅。
项目功能如下:
1、客户端新用户注册
2. 客户端用户登录
3. 添加好友和添加群组
4. 好友聊天
5. 离线消息
6. nginx配置tcp负载均衡
7. 集群聊天系统支持客户端跨服务器通
使用的技术栈如下:
Json序列化和反序列化
muduo网络库开发
nginx源码编译安装和环境部署
nginx的tcp负载均衡器配置
redis缓存服务器编程实践
基于发布-订阅的服务器中间件redis消息队列编程实践
MySQL数据库编程
CMake构建编译环境
Github托管项目
因为Json是一种轻量级的数据交换格式(也叫数据序列化方式)。Json采用完全独立于编程语言的文本格式来存储和表示数据。简洁和清晰的层次结构使得 Json 成为理想的数据交换语言。 易于人阅读和编写,同时也易于机器解析和生成,并有效地提升网络传输效率。
JSON for Modern C++ 是一个由德国大牛 nlohmann 编写的在 C++ 下使用的 JSON 库。
具有以下特点:使用 C++ 11 标准编写,使用 json 像使用 STL 容器一样,STL 和 json 容器之间可以相互转换
#include "json.hpp"
using json = nlohmann::json;
json 对象 js 相当于装入key-value的形式 的容器 都打包
json js;
// 添加数组
js["id"] = {1, 2, 3, 4, 5};
// 添加key-value
js["name"] = "zhang san";
// 添加对象
js["msg"]["zhang san"] = "hello world";
js["msg"]["liu shuo"] = "hello china";
// 上面等同于下面这句一次性添加数组对象
js["msg"] = {{"zhang san", "hello world"}, {"liu shuo", "hello china"}};
cout << js << endl;
// 输出为:
// {"id":[1,2,3,4,5],"msg":{"liu shuo":"hello china","zhang san":"hello world"},"name":"zhang san"}
vector<int> vec;
vec.push_back(1);
vec.push_back(2);
vec.push_back(5);
js["list"] = vec;
// 直接序列化一个map容器
map<int, string> m;
m.insert({1, "黄山"});
m.insert({2, "华山"});
m.insert({3, "泰山"});
js["path"] = m;
cout << js << endl;
//{"list":[1,2,5],"path":[[1,"黄山"],[2,"华山"],[3,"泰山"]]}
可以转成字符串以及char数组 网络传输用
// json 转成字符串 并且转成char 数组
string s=js.dump();
cout<<s<<endl;
cout<<s.c_str()<<endl;
反序列化 利用json::parse(s) 反序列化字符串 可以反序列化嵌套的js
// 序列化层字符串或者char数组
string s = js.dump();
// 模拟从网络接收到json字符串,通过json::parse函数把json字符串专程json对象
json js2 = json::parse(s);
// 直接取key-value
string name = js2["name"];
cout << "name:" << name << endl;
// 直接反序列化vector容器
vector<int> v = js2["list"];
for (int val : v) {
cout << val << " ";
}
cout << endl;
// 直接反序列化map容器
map<int, string> m2 = js2["path"];
for (auto p : m2) {
cout << p.first << " " << p.second << endl;
}
cout << endl;
// 可以总结打印
cout << js2["id"] << endl;
auto list = js2["id"];
cout << list[0] << endl;
// 嵌套反序列化 json 嵌套json
// js["msg"]["zhang san"] = "hello world";
// js["msg"]["liu shuo"] = "hello china";
// // 上面等同于下面这句一次性添加数组对象
// js["msg"] = {{"zhang san", "hello world"}, {"liu shuo", "hellochina"}};
auto magjs = js2["msg"];
cout << magjs["zhang san"] << endl;
配制nginx文件,发的请求都在8000端口,然后负载均衡分发到不同的服务器上,并且配制好机器的端口以及权重这些。max_fails=3 fail_timeout=30s;表示30s发送一次心跳,如果3次检测失败就判定服务器挂掉了。(如果发现客户端闪退,把server下面的连个超时时间去掉)
服务器 气动的端口要要与配置文件一直一致客户端连接8000端口
为了保持通信,让各个ChatServer服务器互相之间直接建立TCP连接进行通信,相当于在服务器网络之间进行广播。这样的设计使得各个服务器之间耦合度太高,不利于系统扩展,并且会占用系统大量的socket资源,各服务器之间的带宽压力很大,不能节省资源给更多的客户端提供服务,因此绝对不是一个好的设计。
不需要每个服务器建立连接,只需要与消息队列连接就行。集群部署的服务器之间进行通信,最好的方式就是引入中间件消息队列,解耦各个服务器,使整个系统松耦合,提高服务器的响应能力,节省服务器的带宽资源,
订阅某个消息之后有人发布那种消息,队列就会通知转发给你
线程一直循环监听订阅的上下文,然后如果有就会调用预定的回调函数handler。
解决该项目中的问题 :在同一个server的客户端都可以通过该server的在线用户map找到对应的conn连接,然后进行客户端与客户端的通讯。但是由于负载均衡,所以客户端可能存在于不同的server中,不同的server并不共享存储的在线用户map,所以客户端与客户端的通讯就要通过服务器订阅的方式,比如说这个client1在server1中登录了,那么自由server1才存有client1的conn TCP连接,那么该服务器订阅该client1 的id的通道,每当这个通道有消息(别人客户端发送消息给client1时候就会发不到这个client1 的iid的通道中),server1j就会被推送该消息,然后server1就会对该消息进行处理。
定义redis类,要实现redis消息订阅以及消息发布,要有分别负责publish消息和责subscribe消息的同步上下文对象,并且实现发布消息函数以及订阅和取消订阅消息函数。他们都是通过通道channel来实现的。要实现回调操作,收到订阅的消息之后进行的函数操作。_notify_message_handler。在独立线程中接收订阅通道中的消息的函数。
class Redis {
public:
Redis();
~Redis();
// 连接redis服务器
bool connect();
// 向redis指定的通道channel发布消息
bool publish(int channel, string message);
// 向redis指定的通道subscribe订阅消息
bool subscribe(int channel);
// 向redis指定的通道unsubscribe取消订阅消息
bool unsubscribe(int channel);
// 在独立线程中接收订阅通道中的消息
void observer_channel_message();
// 初始化向业务层上报通道消息的回调对象 检测到消息 进行回调处理。
void init_notify_handler(function<void(int, string)> fn);
private:
// hiredis同步上下文对象,负责publish消息
redisContext* _publish_context;
// hiredis同步上下文对象,负责subscribe消息
redisContext* _subcribe_context;
// 回调操作,收到订阅的消息,给service层上报
function<void(int, string)> _notify_message_handler;
};
实现类
Redis::Redis() : _publish_context(nullptr), _subcribe_context(nullptr) {}
Redis::~Redis() {
if (_publish_context != nullptr) {
redisFree(_publish_context);
}
if (_subcribe_context != nullptr) {
redisFree(_subcribe_context);
}
}
bool Redis::connect() {
// 负责publish发布消息的上下文连接
_publish_context = redisConnect("127.0.0.1", 6379);
if (nullptr == _publish_context) {
cerr << "connect redis failed!" << endl;
return false;
}
// 负责subscribe订阅消息的上下文连接
_subcribe_context = redisConnect("127.0.0.1", 6379);
if (nullptr == _subcribe_context) {
cerr << "connect redis failed!" << endl;
return false;
}
// 在单独的线程中,监听通道上的事件,有消息给业务层进行上报
thread t([&]() { observer_channel_message(); });
t.detach();
cout << "connect redis-server success!" << endl;
return true;
}
// 向redis指定的通道channel发布消息
bool Redis::publish(int channel, string message) {
redisReply* reply = (redisReply*)redisCommand(
_publish_context, "PUBLISH %d %s", channel, message.c_str());
if (nullptr == reply) {
cerr << "publish command failed!" << endl;
return false;
}
freeReplyObject(reply);
return true;
}
// 向redis指定的通道subscribe订阅消息
bool Redis::subscribe(int channel) {
// SUBSCRIBE命令本身会造成线程阻塞等待通道里面发生消息,这里只做订阅通道,不接收通道消息
// 通道消息的接收专门在observer_channel_message函数中的独立线程中进行
// 只负责发送命令,不阻塞接收redis
// server响应消息,否则和notifyMsg线程抢占响应资源
if (REDIS_ERR ==
redisAppendCommand(this->_subcribe_context, "SUBSCRIBE %d", channel)) {
cerr << "subscribe command failed!" << endl;
return false;
}
// redisBufferWrite可以循环发送缓冲区,直到缓冲区数据发送完毕(done被置为1)
int done = 0;
while (!done) {
if (REDIS_ERR == redisBufferWrite(this->_subcribe_context, &done)) {
cerr << "subscribe command failed!" << endl;
return false;
}
}
// redisGetReply
return true;
}
// 向redis指定的通道unsubscribe取消订阅消息
bool Redis::unsubscribe(int channel) {
if (REDIS_ERR ==
redisAppendCommand(this->_subcribe_context, "UNSUBSCRIBE %d", channel)) {
cerr << "unsubscribe command failed!" << endl;
return false;
}
// redisBufferWrite可以循环发送缓冲区,直到缓冲区数据发送完毕(done被置为1)
int done = 0;
while (!done) {
if (REDIS_ERR == redisBufferWrite(this->_subcribe_context, &done)) {
cerr << "unsubscribe command failed!" << endl;
return false;
}
}
return true;
}
// 在独立线程中接收订阅通道中的消息
void Redis::observer_channel_message() {
redisReply* reply = nullptr;
while (REDIS_OK == redisGetReply(this->_subcribe_context, (void**)&reply)) {
// 订阅收到的消息是一个带三元素的数组 一直循环阻塞 监听
if (reply != nullptr && reply->element[2] != nullptr &&
reply->element[2]->str != nullptr) {
// 给业务层上报通道上发生的消息 给对应的handler
_notify_message_handler(atoi(reply->element[1]->str),
reply->element[2]->str);
}
freeReplyObject(reply);
}
cerr << ">>>>>>>>>>>>> observer_channel_message quit <<<<<<<<<<<<<" << endl;
}
void Redis::init_notify_handler(function<void(int, string)> fn) {
this->_notify_message_handler = fn;
}
数据库表如下,分别是用户表,朋友表,离线消息表。
offlinemassage
使用c库的MySQL类对数据库进行交互操作。实现了包括连接,更新,查询,还有返回连接的操作。类定义如下:
#include
// 数据库操作类
class MySQL {
private:
MYSQL* _conn;
public:
// 初始化数据库连接
MySQL();
// 释放数据库连接资源
~MySQL();
// 连接数据库
bool connect();
// 更新操作
bool update(string sql);
// 查询操作
MYSQL_RES* query(string sql);
// 获取连接
MYSQL* getConnection();
};
类实现如下:
构造函数以及析构函数调用库的初始化与断开函数。连接函数调用mysql_real_connect方法进行连接,金鑫了适配中文的操作。
// 初始化数据库连接
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) {
// w为了适配中文
mysql_query(_conn, "set names gbk");
LOG_INFO << "connet mysql success!!!";
} else {
LOG_INFO << "connet mysql fail!!!";
}
return p;
}
其他三个函数也是传入sql语句然后调用数据库对应的方法进行操作。
// 更新操作
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())) {
// muduo 库的日志输出
LOG_INFO << __FILE__ << ":" << __LINE__ << ":" << sql << "查询失败!";
return nullptr;
}
return mysql_use_result(_conn);
}
MYSQL* MySQL::getConnection() {
return _conn;
}
业务需求,我们需要实现三个数据库操作类,第一个是用户model,第二个是朋友model,第三个是离线消息model。这里只展示u操作user表中的类。
操作数据库中的user表,就需要两个人一个是user类,一个是操作user的工具类。user类就是与数据库user表中毒了属性有关,二操作类支持,增改查。操作类初始化是的数据库进行连接。
using namespace std;
class User {
private:
/* data */
int id;
string name;
string password;
string state;
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; }
};
// 针对表的增删改查
class UserModel {
private:
/* data */
public:
// user 表的增加方法
bool insert(User& user);
// 根据用户号码查询用户信息
User query(int id);
// 更新用户状态
bool updateState(User& user);
// 重置用户状态信息
void restState();
UserModel() {
cout << "init usermodel" << endl;
mysql.connect();
}
MySQL mysql;
};
实现这个操作类页很简单,就是简单得组装sql语句然后传入。
bool UserModel::insert(User& user) {
// 组装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());
cout << "insert user" << endl;
// 插入
if (mysql.update(sql)) {
// 返回 主键 id 给user
user.setId(mysql_insert_id(mysql.getConnection()));
return true;
}
return false;
}
User UserModel::query(int id) {
char sql[1024] = {0};
sprintf(sql, "select * from user where id = %d ", id);
auto res = mysql.query(sql);
if (res != nullptr) {
// 查询一行 typedef char **MYSQL_ROW;是二维数组类型的
// 拿出来全部是字符串 并且对应表里面的字段
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]);
// 返回的res是指针 需要释放资源
mysql_free_result(res);
return user;
}
}
return User();
}
bool UserModel::updateState(User& user) {
char sql[1024] = {0};
sprintf(sql, "update user set state = '%s' where id = %d",
user.getState().c_str(), user.getId());
if (mysql.update(sql)) {
return true;
}
return false;
}
void UserModel::restState() {
char sql[1024] = "update user set state = 'offline' where state ='online'";
mysql.update(sql);
}
利用枚举类型定义消息类型
enum EnMsgType {
LOGIN_MSG = 1, // 登录消息
LOGIN_MSG_ACK, // 登录响应消息
LOGINOUT_MSG, // 注销消息
REG_MSG, // 注册消息
REG_MSG_ACK, // 注册响应消息
ONE_CHAT_MSG, // 聊天消息
ADD_FRIEND_MSG, // 添加好友消息
CREATE_GROUP_MSG, // 创建群组
ADD_GROUP_MSG, // 加入群组
GROUP_CHAT_MSG, // 群聊天
};
这是定义处理函数类型:
using MsgHandler = function<void(const TcpConnectionPtr& conn, json& js, Timestamp time)>;
处理函数要与定义好的处理类型一致。这里实现了登录,注册,一对一聊天,添加朋友的业务功能。(事实上,还有群组的功能,为了方便这里就不展示了)
// 处理登录以及注册业务的代码
void login(const TcpConnectionPtr& conn, json& js, Timestamp time);
void reg(const TcpConnectionPtr& conn, json& js, Timestamp time);
// 一对一聊天回调法术
void oneChat(const TcpConnectionPtr& conn, json& js, Timestamp time);
// 添加好友业务
void addFriend(const TcpConnectionPtr& conn, json& js, Timestamp time);
}
由于要每一种消息id对应一种业务的处理方法没所以要用字典把消息id与方法存储对应起来,为了方便一对一通讯还要存储在线用户,存储的是moduo库的conn连接指针。这些事临界资源,所以要定义一盒互斥锁保证线程安全。
private:
ChatService();
// 一个消息id 对应一种业务处理方法操作
unordered_map<int, MsgHandler> _msgHandlerMap;
// 存储在线用户的 通讯连接 为了后面用户聊天
unordered_map<int, TcpConnectionPtr> _userConnMap;
// 定义互斥锁 保证 字典的线程安全
mutex _connMutex;
service唯一实例。以及从map获取对应的处理器的犯法,以及处理客户端异常退出的犯法
// 获取 单个对象的接口函数
static ChatService* instance();
// 从map获取对应的处理器 就是获取对应的方法
MsgHandler getHandler(int);
// 处理客户端异常退出
void clientCloseException(const TcpConnectionPtr& conn);
在数据操作方面,联系数据层的model类对象,也要在service这里提前定义好
// 数据操作类对象 用来操作对应的数据
UserModel _usermodel;
OfflineMsgModel _offlineMsgModel;
FriendModel _friendModel;
还要定义一个处理订阅的redis通道消息的回调函数
void handleRedisSubscribeMessage(int, string);
最后还有一个处理,server 服务器异常 退出 的处理函数reset,进行重置所有客户端。
// reset 服务器异常 退出 重置说有客户端
void reset();
首先是构造函数就先把处理回调函数注册栅,通过map插入的方式注册。注意:由于这些函数是类里面的成员函数 类成员函数指针需要通过对象来调用 所以要使用了bind函数将login函数和当前对象this绑定起来。以及进行redis连接和注册redis的回调函数
ChatService::ChatService() {
// 字典映射回调函数
// _msgHandlerMap[LOGIN_MSG] = login;
cout << "init ChatService" << endl;
_msgHandlerMap.insert({LOGIN_MSG, bind(&ChatService::login, this, _1, _2, _3)});
_msgHandlerMap.insert({REG_MSG, bind(&ChatService::reg, this, _1, _2, _3)});
_msgHandlerMap.insert( {ONE_CHAT_MSG, bind(&ChatService::oneChat, this, _1, _2, _3)});
_msgHandlerMap.insert({ADD_FRIEND_MSG, bind(&ChatService::addFriend, this, _1, _2, _3)});
// redis 操作 然后注册回调函数
if (_redis.connect()) {
_redis.init_notify_handler(
bind(&ChatService::handleRedisSubscribeMessage, this, _1, _2));
}
}
从字典获取处理函数的方法如下:如果没有该方法返回用lamda表达式返回一个默认的,
MsgHandler ChatService::getHandler(int msgid) {
if (!_msgHandlerMap.count(msgid)) {
// z通过muduo库 打印
// 、、返回一个默认的 处理器 用lamda表达式
return [=](const TcpConnectionPtr& conn, json& js, Timestamp time) {
LOG_ERROR << "msgid:" << msgid << " can not find handler";
};
} else {
return _msgHandlerMap[msgid];
}
}
处理客户端异常退出的方法就是做两件事 1、从map表删除 2、更新数据库状态信息。
// 处理客户端异常退出
void ChatService::clientCloseException(const TcpConnectionPtr& conn) {
// 从map表查找 找到相应deid 做两件事 从map表删除 更新数据库状态信息
// 处理表要进行线程安全问题
User user;
{
// 记录用户链接信息 临界资源 要考虑线程问题 操作map的代码放入作用域
lock_guard<mutex> lock(_connMutex);
for (auto it = _userConnMap.begin(); it != _userConnMap.end(); it++) {
if (it->second == conn) {
user.setId(it->first);
// 删除用户链接信息
_userConnMap.erase(it);
break;
}
}
}
// 下线就 取消订阅
_redis.unsubscribe(user.getId());
// 更新用户状态
user.setState("offline");
_usermodel.updateState(user);
}
处理redis推送的通道消息:当有客户端在你订阅的通道中发布消息,就会从redis通道中获取订阅的消息,这个是订阅的通道有消息之后得处理函数,消息传进来的是用户id表示要跟哪个用户进行通讯,通过该id找到conn连接,然后把消息发给他。因为登录的时候这个servser就已经订阅了该user的id通道。所以通过消息的id一定能够找到对应的user conn
// 从redis消息队列中获取订阅的消息
void ChatService::handleRedisSubscribeMessage(int userid, string msg) {
// 消息通道传入的是 toid 因为这个通道就是toid的
// 所以该服务器_userConnMap必定会有 这个id的连接
lock_guard<mutex> lock(_connMutex);
auto it = _userConnMap.find(userid);
if (it != _userConnMap.end()) {
it->second->send(msg);
return;
}
// 存储该用户的离线消息 这是发布消息到取消息的过程中下线的情况
_offlineMsgModel.insert(userid, msg);
}
处理server异常退出的函数。就是调用usermodel user操作类对所有的user的状态都设置为下线。
void ChatService::reset() {
_usermodel.restState();
}
1、登录
从js中拿到对应的id以及密码,然后对其进行查询,如果用户存在但是是在线状态那么就表示重复登录了就返回,如果不存在也返回。正常登录,首先在在线用户map表中插入(共享资源加锁)。然后订阅这个id的通道消息,就是说有别的服务器发布消息在这个id的通道的时候这个server就会接受到,接下来就是常规的,修改状态,返回登录成功的信息给客户端,显示好友这些。
void ChatService::login(const TcpConnectionPtr& conn, json& js, Timestamp time) {
LOG_INFO << "do login service!!";
int id = js["id"].get<int>();
string password = js["password"];
User user = _usermodel.query(id);
if (user.getId() == id && user.getPwd() == password) {
if (user.getState() == "online") {
// 该用户已经登录,不允许重复登录
json response;
response["msgid"] = LOGIN_MSG_ACK;
response["errno"] = 2;
response["errmsg"] = "this account is using, input another!";
conn->send(response.dump());
} else {
{
// 记录用户链接信息 临界资源 要考虑线程问题
lock_guard<mutex> lock(_connMutex);
_userConnMap.insert({id, conn});
// 加个括号 智能指针 直接解锁
}
// 登录成功 更新状态
// 向redis订阅 该id的channel 任何放入该id的消息都会推送到
_redis.subscribe(id);
user.setState("online");
_usermodel.updateState(user);
json response;
response["msgid"] = LOGIN_MSG_ACK;
response["errno"] = 0;
response["id"] = user.getId();
response["name"] = user.getName();
// 查询该用户是否有离线消息
vector<string> offlineMsgVec = _offlineMsgModel.query(id);
if (!offlineMsgVec.empty()) {
response["offlinemsg"] = offlineMsgVec;
// 读取该用户的离线消息后,把该用户的所有离线消息删除掉
_offlineMsgModel.remove(id);
}
// 查询好友信息
vector<User> uservec = _friendModel.query(id);
if (!uservec.empty()) {
vector<string> vec2;
for (User& user : uservec) {
json js;
js["id"] = user.getId();
js["name"] = user.getName();
js["state"] = user.getState();
vec2.push_back(js.dump());
}
// 返回去 字段
response["friends"] = vec2;
}
// 发送给客户端
conn->send(response.dump());
}
} else {
// 该用户不存在,用户存在但是密码错误,登录失败
json response;
response["msgid"] = LOGIN_MSG_ACK;
response["errno"] = 1;
response["errmsg"] = "id or password is invalid!";
conn->send(response.dump());
}
}
2、注册函数
void ChatService::reg(const TcpConnectionPtr& conn, json& js, Timestamp time) {
LOG_INFO << "do reg 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 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());
}
}
3、添加好友
void ChatService::addFriend(const TcpConnectionPtr& conn,
json& js,
Timestamp time) {
int userid = js["id"].get<int>();
int friendid = js["friendid"].get<int>();
_friendModel.insert(userid, friendid);
}
4、客户端一对一聊天
有三种情况:
1、第一这个客户端与目标客户端在同一个server上,字节通过该server的在线map获取conn 连接,然后放送
2、不在同一台服务器,但是通过数据库查询到目标客户端是在线的,说明在其他服务器上,字节发布消息到对应的id通道上。
3、数据库查找到用户不在线,字节发送离线消息。
// 群组聊天业务
void ChatService::groupChat(const TcpConnectionPtr& conn,
json& js,
Timestamp time) {
int userid = js["id"].get<int>();
int groupid = js["groupid"].get<int>();
vector<int> useridVec = _groupModel.queryGroupUsers(userid, groupid);
lock_guard<mutex> lock(_connMutex);
for (int id : useridVec) {
auto it = _userConnMap.find(id);
if (it != _userConnMap.end()) {
// 转发群消息
it->second->send(js.dump());
} else {
// 查询toid是否在线
User user = _usermodel.query(id);
if (user.getState() == "online") {
_redis.publish(id, js.dump());
} else {
// 存储离线群消息
_offlineMsgModel.insert(id, js.dump());
}
}
}
}
本项目网络层用的是moduo网络库
使用moduo网络库,必须要完成下面这几个步骤。
1、组合tcpserver对象 2、 创建eventloop事件循环对象的指针
3、 明确构造函数需要什么参数
4、当前服务器的构造函数中 注册连接回调函数以及处理读写事件的回调函数
5、设置线程数量 他会自己分配io线程 和worker线程
其中服务器的构造函数必须传进去下面这三个参数:
EventLoop* loop,const InetAddress& listenAddr,const string& nameArg,
所有server类应该有两个对象,以及专门处理用户的连接与断开的回调函数还有专门处理用户读写事件回调函数 。所以类的设计如下:
class ChatServer {
private:
TcpServer _server;
EventLoop* _loop; //指向循环事件的指针
// 专门处理用户的连接与断开的回调函数
void onConnection(const TcpConnectionPtr& conn);
// 专门处理用户读写事件回调函数 函数必须是有三个参数
// 一个是tcp链接一个是buf缓冲区 一个是时间
void onMessage(const TcpConnectionPtr& conn, Buffer* buffer, Timestamp time);
public:
// 启动ChatServer服务
ChatServer(EventLoop* loop,
const InetAddress& listenAddr,
const string& nameArg);
void start();
};
#endif // CHATSERVER_H
专门处理连接的回调函数,如果有客户端连接上来或者断开连接就会调用这个函数。这个函数要处理的是,断开连接的时候的处理。clientCloseException是后面业务层实现的处理函数,主要是修改客户端的在线状态,然后关闭连接。
// 专门处理用户的连接与断开的回调函数
void ChatServer::onConnection(const TcpConnectionPtr& conn) {
// 客户端 断开连接
if (!conn->connected()) {
ChatService::instance()->clientCloseException(conn);
conn->shutdown();
}
}
这个函数是专门处理客户端发过来的信息,函数必须是有三个参数, 一个是tcp链接,一个是buf缓冲区 一个是时间。首先要检测传过来的json是否正确。如果不正确返回不正确信息。
解析函获取的消息类型,然后传给业务层的唯一实例获取对应的提前注册好的处理函数,这些出来函数都是同一个类型的,这样才能传入一样的参数,
void ChatServer::onMessage(const TcpConnectionPtr& conn,Buffer* buffer,Timestamp time) {
string buf = buffer->retrieveAllAsString();
cout << "server get buf: " << buf << endl;
json js;
// 将字符串数据反序列化
if (json::accept(buf)) {
js = json::parse(buf);
// 在这里处理解析后的JSON对象
} else {
// JSON字符串不合法,处理错误情况
cout << " buf 不正确 " << buf << endl;
return;
}
// 解析后 知道magid 就是消息类型 比如说登录 注册
// 完全解耦网络模块代码和业务模块代码 分开来 在这里不做业务模块
// 通过消息 得到hander
// 先获取唯一实例 然后传进去 找到处理方法 .get()转成int
auto msghandler =
ChatService::instance()->getHandler(js["msgid"].get<int>());
msghandler(conn, js, time);
}
void ChatServer::start() {
_server.start();
}
main函数传入连个参数一个是ip地址一个是端口号。并且通过信号捕获的方式,捕获server断开信号,然后调server异常退出的重置user的状态信息的函数
// 处理服务器 Ctrl c 结束后 重置user的状态信息
void resetHandler(int) {
ChatService::instance()->reset();
exit(0);
}
int main(int argc, char** argv) {
if (argc < 3) {
cerr << "command invalid! example: ./ChatServer 127.0.0.1 6000" << endl;
exit(-1);
}
// 解析通过命令行参数传递的ip和port
char* ip = argv[1];
uint16_t port = atoi(argv[2]);
//信号捕获
signal(SIGINT, resetHandler);
EventLoop loop;
InetAddress addr(ip, port);
ChatServer server(&loop, addr, "ChatServer");
server.start(); // listenfd epoll_ctl=>epoll
loop.loop(); // epoll_wait以阻塞方式等待新用户连接,已连接用户的读写事件等
return 0;
}
其实还有客户端的编写,客户端的编写就是常规的客户端逻辑,比如说设计菜单,封装发送请求函数之类的。
通过本项目的收获如下。
1、对服务器的网络I/O模块,业务模块,数据模块分层的设计思想有了更深入的了解
2. 掌握C++ muduo网络库的编程以及实现原理
3. Json的编程应用
4. 学习了nginx配置部署tcp负载均衡器的应用以及原理
5. 掌握服务器中间件的应用场景和基于发布-订阅的redis编程实践以及应用原理
6. 使用CMake构建自动化编译环境