C++项目推荐-基于muduo库的单聊群聊项目-可写简历

1 项目简介

今天分享基于muduo库实现的单聊群聊项目,该项目支持QT客户端一对一聊天,服务端基于muduo+MySQL+Redis.

视频讲解:C++校招项目-基于muduo库的分布式单聊群聊项目-可写简历

源项目地址:https://github.com/haojoy/WeChat.git

2 Linux C++后端编译和运行

部署项目,我只讲解基于我改过的版本,原始版本大家参考原有的部署方式。

部署前提:

  • 安装好MySQL
  • 安装好Redis

该项目需要MySQL和redis基础

首先安装依赖库:

  • json库相关:sudo apt-get install nlohmann-json3-dev
  • redis开发包: sudo apt-get install -y libhiredis-dev
#首先解压老廖提供的代码

# 进入项目
cd WeChat
mkdir build
cd build
#重新cmake 编译debug方式
cmake -DCMAKE_BUILD_TYPE=Debug ..
make -j4

编译成功后build目录的bin目录产生ChatServer和ChatClient执行文件。

  • ChatServer:是后端服务程序,可以接入qt客户端,也可以接入ChatClient
  • ChatClient:是Linux环境的命令行客户端,具体功能看源码实现。

启动ChatServer之前,我们要根据自己的MySQL账号信息修改mysql相关的配置。

WeChat/application/chatserver/include/server/db/connection.h

const string  kMySqlIp = "127.0.0.1";
const string kMySqlUserName = "root";
const string kMySqlPassword = "123456";
const int kMySqlPort = 3306;
const string kMySqlDbName = "wechat";

主要是修改用户kMySqlUserName和密码kMySqlPassword。

重新make下。

运行后端程序,记得以sudo方式运行,因为里面有些目录的创建需要sudo权限。

sudo ./bin/ChatServer

默认的监听端口:

8088:fileserver相关

8080:chatserver相关

启动Linux命令行客户端

./bin/ChatClient 127.0.0.1 8080

我们可以选择创建用户

========================
1. login
2. register
3. quit
========================
choice:2
username:darren
userpassword:123
name register success, userid is 1, do not forget it!

3 QT客户端编译和运行

编译环境:QT5.15.2 MinGW 64-bit

3.1 修改chatserver和fileserver地址

运行代码前修改服务器地址chatserver和fileserver的ip和端口。

3.1.1 修改chatserver地址

修改位置: Net\packdef.h

#define _DEF_TCP_PORT (8080)

#define _DEF_SERVER_IP ("192.168.1.27")

3.1.2 修改fileserver地址

修改位置: Common\fileTransferProtocol.h

#define _DEF_FILE_SERVER_IP ("192.168.1.27")

#define _DEF_FILE_SERVER_PROT (8088)

3.2 启动QT和注册账号

这个QT客户端是有修改过:void Kernel::slot_ChangeUserIcon()才正常设置头像。

使用QT5.15.2 MinGW 64-bit启动QT,如果需要聊天,则需要启动两个qt客户端。

这里开了两个QT客户端进行聊天。

注意:目前QT客户端还有部分功能并不完整,大家可以自行添加功能,或者修改Linux命令行的ChatClient进行测试。

4 Linux后端框架快速分析

这个项目基于muduo架构,如果你不熟悉muduo则需要先学习muduo网络模型,这个网上资料很多的。

4.1 数据库的创建

application/chatserver/src/db/connection.cpp,这里直接使用代码创建数据库和对应的表单

bool Connection::createDBTables() {
	if(createDBCnt_++ != 0){
		return true;
	}

	if (mysql_real_connect(_conn, kMySqlIp.c_str(), kMySqlUserName.c_str(), kMySqlPassword.c_str(), nullptr, kMySqlPort, nullptr, 0) == nullptr) {
        LOG_ERROR << "MySQL connection error: " << mysql_error(_conn);
        return false;
    }

	string queryStr = "CREATE DATABASE IF NOT EXISTS `" + kMySqlDbName + "`";
	if (mysql_query(_conn, queryStr.c_str()) != 0) {
		LOG_ERROR << "MySQL createDatabase error: " << mysql_error(_conn);
		return false;
	}

	queryStr = "USE `" + kMySqlDbName + "`";
	if (mysql_query(_conn, queryStr.c_str()) != 0) {
		LOG_ERROR << "MySQL useDatabase error: " << mysql_error(_conn);
		return false;
	}

	string sql_tuser = "CREATE TABLE IF NOT EXISTS `t_user` (\
		`userid` int NOT NULL AUTO_INCREMENT PRIMARY KEY, \
		`avatar_id` VARCHAR(36) DEFAULT NULL, \
		`username` VARCHAR(64) DEFAULT NULL, \
		`password` VARCHAR(64) DEFAULT NULL, \
		`tel` VARCHAR(15) DEFAULT NULL, \
		`state` enum('online','offline') CHARACTER SET latin1 DEFAULT 'offline' \
	)ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;";
	if (mysql_query(_conn, sql_tuser.c_str()) != 0) {
		LOG_ERROR << "MySQL createTable  t_user error: " << mysql_error(_conn);
		return false;
	}

	string sql_tfile = "CREATE TABLE IF NOT EXISTS `t_file` (\
		file_id VARCHAR(36) NOT NULL PRIMARY KEY, \
		file_name VARCHAR(255) NOT NULL, \
		file_path TEXT NOT NULL, \
		file_size BIGINT NOT NULL, \
		file_md5 CHAR(32) NOT NULL, \
		file_state VARCHAR(50) NOT NULL DEFAULT 'PENDING' \
	)ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;";
	if (mysql_query(_conn, sql_tfile.c_str()) != 0) {
		LOG_ERROR << "MySQL createTable t_file error: " << mysql_error(_conn);
		return false;
	}

	string sql_friendship = "CREATE TABLE IF NOT EXISTS `t_friendship` (\
		`userid` int NOT NULL, \
		`friend_id` int NOT NULL, \
		KEY `userid` (`userid`,`friend_id`) \
	)ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;";
	if (mysql_query(_conn, sql_friendship.c_str()) != 0) {
		LOG_ERROR << "MySQL createTable t_friendship error: " << mysql_error(_conn);
		return false;
	}

	string sql_offlinemsg = "CREATE TABLE IF NOT EXISTS `t_offlinemsg` (\
		`id` INT AUTO_INCREMENT PRIMARY KEY, \
		`sendTo` INT NOT NULL, \
		`sendFrom` INT NOT NULL, \
		`messageContent` TEXT NOT NULL, \
		`messageType` ENUM('text', 'friend_apply', 'vedio', 'audio', 'file') NOT NULL DEFAULT 'text', \
		`createTime` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP \
	)ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;";
	if (mysql_query(_conn, sql_offlinemsg.c_str()) != 0) {
		LOG_ERROR << "MySQL createTable t_offlinemsg error: " << mysql_error(_conn);
		return false;
	}

	string sql_allgrp = "CREATE TABLE IF NOT EXISTS `t_allgrp` (\
		`id` int(11) NOT NULL AUTO_INCREMENT, \
		`groupname` varchar(50) CHARACTER SET latin1 NOT NULL, \
		`groupdesc` varchar(200) CHARACTER SET latin1 DEFAULT '', \
		PRIMARY KEY (`id`), \
		UNIQUE KEY `groupname` (`groupname`) \
	) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;";
	if (mysql_query(_conn, sql_allgrp.c_str()) != 0) {
		LOG_ERROR << "MySQL createTable t_allgrp error: " << mysql_error(_conn);
		return false;
	}

	string sql_grpuser = "CREATE TABLE IF NOT EXISTS `t_grpuser` (\
		`groupid` int(11) NOT NULL, \
		`userid` int(11) NOT NULL, \
		`grouprole` enum('creator','normal') CHARACTER SET latin1 DEFAULT NULL, \
		KEY `groupid` (`groupid`,`userid`) \
	) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;";
	if (mysql_query(_conn, sql_grpuser.c_str()) != 0) {
		LOG_ERROR << "MySQL createTable t_grpuser error: " << mysql_error(_conn);
		return false;
	}
	return true;
}


4.2 main函数位置

main函数入口位置:WeChat/application/chatserver/src/main.cpp, line 17

(gdb) b main
Breakpoint 1 at 0x1584a6: file /home/lqf/linux/reactor/WeChat/application/chatserver/src/main.cpp, line 17.
(gdb) 

4.3 架构核心

消息协议设计

协议采用json做序列化,设计的json字符串里有个msgid字段,用来区分不同的消息。消息类型如下所示:

enum Message
{
    LOGIN_MSG = 1, // 登录消息
    LOGIN_MSG_ACK, // 登录响应消息
    LOGINOUT_MSG, // 注销消息
    REG_MSG, // 注册消息
    REG_MSG_ACK, // 注册响应消息
    ONE_CHAT_MSG, // 聊天消息
    ADD_FRIEND_REQ, // 添加好友消息
    ADD_FRIEND_RSP,

    CREATE_GROUP_MSG, // 创建群组
    ADD_GROUP_MSG, // 加入群组
    GROUP_CHAT_MSG, // 群聊天

    GET_FRIEND_INFO_REQ, // 获取待添加好友的信息
    GET_FRIEND_INFO_RSP, // 查找信息结果
    REFRESH_FRIEND_LIST,

    SET_AVATAR_RQ,
    SET_AVATAR_RS,
    SET_AVATAR_COMPLETE_NOTIFY,
};

然后通过_msgHandlerMap根据不同的msgid调用对应的函数进行处理。

// 注册消息以及对应的Handler回调操作
ChatService::ChatService()
{
    // 用户基本业务管理相关事件处理回调注册
    _msgHandlerMap.insert({LOGIN_MSG, std::bind(&ChatService::login, this, _1, _2, _3)});
    _msgHandlerMap.insert({LOGINOUT_MSG, std::bind(&ChatService::loginout, this, _1, _2, _3)});
    _msgHandlerMap.insert({REG_MSG, std::bind(&ChatService::userRegister, this, _1, _2, _3)});
    _msgHandlerMap.insert({ONE_CHAT_MSG, std::bind(&ChatService::oneChat, this, _1, _2, _3)});
    _msgHandlerMap.insert({ADD_FRIEND_REQ, std::bind(&ChatService::addFriendReq, this, _1, _2, _3)});
    _msgHandlerMap.insert({ADD_FRIEND_RSP, std::bind(&ChatService::addFriendRsp, this, _1, _2, _3)});

    // 群组业务管理相关事件处理回调注册
    _msgHandlerMap.insert({CREATE_GROUP_MSG, std::bind(&ChatService::createGroup, this, _1, _2, _3)});
    _msgHandlerMap.insert({ADD_GROUP_MSG, std::bind(&ChatService::addGroup, this, _1, _2, _3)});
    _msgHandlerMap.insert({GROUP_CHAT_MSG, std::bind(&ChatService::groupChat, this, _1, _2, _3)});

    _msgHandlerMap.insert({GET_FRIEND_INFO_REQ, std::bind(&ChatService::getFriendInfoReq, this, _1, _2, _3)});
    _msgHandlerMap.insert({SET_AVATAR_RQ, std::bind(&ChatService::dealAvatarUpdateRq, this, _1, _2, _3)});
    _msgHandlerMap.insert({SET_AVATAR_COMPLETE_NOTIFY, std::bind(&ChatService::dealAvatarUploadComplete, this, _1, _2, _3)});

    // 连接redis服务器
    if (_redis.connect())
    {
        // 设置上报消息的回调
        _redis.init_notify_handler(std::bind(&ChatService::handleRedisSubscribeMessage, this, _1, _2));
    }
}

处理逻辑

// 上报读写事件相关信息的回调函数
void ChatServer::onMessage(const TcpConnectionPtr &conn,
                           Buffer *buffer,
                           Timestamp time)
{
    string buf = buffer->retrieveAllAsString();

    // 测试,添加json打印代码
    cout << buf << endl;

    // 数据的反序列化
    json js;
    .......
    js = json::parse(buf);
    ........
    // 达到的目的:完全解耦网络模块的代码和业务模块的代码
    // 通过js["msgid"] 获取=》业务handler=》conn  js  time
    auto msgHandler = ChatService::instance()->getHandler(js["msgid"].get<int>());
    // 回调消息绑定好的事件处理器,来执行相应的业务处理
    msgHandler(conn, js, time);
}

登录逻辑

登录正常后,以user id作为key, TcpConnectionPtr作为value插入到_userConnMap,后续发送消息就是根据这个user id找到对应的客户端连接。

// 处理登录业务  id  pwd   pwd
void ChatService::login(const TcpConnectionPtr &conn, json &js, Timestamp time)
{
    .......
    // 登录成功,记录用户连接信息
    {
        lock_guard<mutex> lock(_connMutex);
        _userConnMap.insert({id, conn});
    }

}

登录成功后,可以:

  • 查询用户信息:_userModel.queryuserinfo
  • 查询离线消息:_offlineMsgModel.query(id)
  • 查询好友列表: _friendModel.query(id)
  • 查询群列表:_groupModel.queryGroups(id)

然后根据查询结果,做json序列化后发送给客户端。

4.4 分布式框架

这个项目采用了分布式架构的方式,以支持更多的客户端加入,不同的服务直接使用redis进行消息转发。

其实逻辑并不复杂,以一对一聊天代码为例:

登录相关处理

  • 不管客户端登录哪个ChatServer,登录成功后都从redis消息队列订阅自己的channel,channel根据user id
void ChatService::login(const TcpConnectionPtr &conn, json &js, Timestamp time)
{
// id用户登录成功后,向redis订阅channel(id)
_redis.subscribe(id);
}

一对一聊天相关处理

一对一聊天发送消息相关处理(ChatService::oneChat函数):

  • 先查询对方是否在同一个ChatServer,如果是同一个ChatServer,则可以在_userConnMap查询到对方
int toid = js["receiverid"].get<int>();
    {
        lock_guard<mutex> lock(_connMutex);
        auto it = _userConnMap.find(toid);
        if (it != _userConnMap.end())
        {
            // toid在线,转发消息   服务器主动推送消息给toid用户
            it->second->send(js.dump());        //如果在同一个服务器,则直接发送
            return;
        }
    }

直接调用对方的tcpconnection发送信息即可。

  • 如果查询不到对方,则将消息发送给消息队列,以对方user id作为channel,这样对方就能收到消息的推送
// 查询toid是否在线 
    User user = _userModel.query(toid);
    if (user.getState() == "online")
    {
        _redis.publish(toid, js.dump());
        return;
    }

如何获取订阅数据

订阅数据的获取有单独的线程,class Redis 这个类在封装的时候提供了回调接口_notify_message_handler,

有独立的线程不断调用observer_channel_message:

// 在独立线程中接收订阅通道中的消息
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)
        {
            // 给业务层上报通道上发生的消息
            _notify_message_handler(atoi(reply->element[1]->str) , reply->element[2]->str);
        }

        freeReplyObject(reply);
    }

    cerr << ">>>>>>>>>>>>> observer_channel_message quit <<<<<<<<<<<<<" << endl;
}

具体是调用ChatService::handleRedisSubscribeMessage处理订阅的数据

// 从redis消息队列中获取订阅的消息
void ChatService::handleRedisSubscribeMessage(int userid, string msg)
{
    lock_guard<mutex> lock(_connMutex);
    auto it = _userConnMap.find(userid);
    if (it != _userConnMap.end())
    {
        it->second->send(msg);
        return;
    }

    // 存储该用户的离线消息
    _offlineMsgModel.insert(userid, msg);
}

5 扩展

需要思考的问题:

  • 当前的协议设计是否有粘包半包的问题
  • 是否可以使用kafka消息队列替换redis消息队列。
  • 目前的明文密码方式是否合适。
  • 在线状态写到数据库里是否合适?
#项目##实习##后端开发##C++##简历中的项目经历要怎么写#
全部评论

相关推荐

评论
点赞
2
分享

创作者周榜

更多
牛客网
牛客企业服务