IM项目:进阶版即时通讯项目---消息存储

文章目录

消息存储服务

功能设计

消息存储子服务,顾名思义,把消息进行存储

  1. 文本消息:存储到elasticsearch文档搜索服务中
  2. 文件/语音/图片:存储到文件管理子服务中

项目为什么要引入elasticsearch?其实主要目的就是想要能够完成历史消息的搜索:

  1. 获取最近的N条消息
  2. 获取指定时间段的消息
  3. 根据关键字搜索好友聊天消息

模块划分

  1. 基于gflags进行配置文件和参数解析服务
  2. 基于spdlog进行日志输出服务
  3. 基于etcd进行服务注册中心服务
  4. 基于ODB实现的MySQL对象管理,这个主要是用于从RabbitMQ中消费到消息之后,向数据库中存储一份,以便于通过时间进行范围查找,从数据库根据指定用户的所有好友消息
  5. 基于rpc服务进行远程调用
  6. 基于RabbitMQ实现的,从消息队列服务器消费获取聊天消息,将文本消息存储到elasticsearch服务,将文件消息存储到文件管理子服务,将所有的消息都存储到MySQL中一份

想要对于这些数据进行存储,那么首先要先对于数据进行描述,因此就要用一些信息来描述文本和文件的消息

数据管理

数据库中只存储的是文本消息和其他类型消息的元信息

数据库结构

  1. 消息ID
  2. 消息产生时间
  3. 消息发送者用户ID
  4. 消息产生会话ID
  5. 消息类型
  6. 消息内容
  7. 文件ID
  8. 文件大小
  9. 文件名称

数据库操作

  1. 新增消息
  2. 通过消息ID来获取消息信息
  3. 通过会话ID进行时间范围查询消息
  4. 通过会话ID进行数量范围查询消息

因此,我们可以定义出下面的ODB文件

cpp 复制代码
#pragma once
#include <string>
#include <cstddef> 
#include <odb/nullable.hxx>
#include <odb/core.hxx>
#include <boost/date_time/posix_time/posix_time.hpp>

namespace im {

#pragma db object table("message")

class Message 
{
public:
    Message()
    {}

    Message(const std::string &mid,
        const std::string &ssid,
        const std::string &uid,
        const unsigned char mtype,
        const boost::posix_time::ptime &ctime):
        _message_id(mid), _session_id(ssid),
        _user_id(uid), _message_type(mtype),
        _create_time(ctime)
    {}
    
    std::string message_id() const { return _message_id; }
    void message_id(const std::string &val) { _message_id = val; }

    std::string session_id() const { return _session_id; }
    void session_id(const std::string &val) { _session_id = val; }
    
    std::string user_id() const { return _user_id; }
    void user_id(const std::string &val) { _user_id = val; }
    
    unsigned char message_type() const { return _message_type; }
    void message_type(unsigned char val) { _message_type = val; }
    
    boost::posix_time::ptime create_time() const { return _create_time; }
    void create_time(const boost::posix_time::ptime &val) { _create_time = val; }

    std::string content() const 
    { 
        if (!_content) 
            return std::string();
        return *_content; 
    }

    void content(const std::string &val) { _content = val; }

    std::string file_id() const 
    { 
        if (!_file_id) 
            return std::string();
        return *_file_id; 
    }

    void file_id(const std::string &val) { _file_id = val; }
    
    std::string file_name() const 
    { 
        if (!_file_name) return std::string();
        return *_file_name; 
    }

    void file_name(const std::string &val) { _file_name = val; }
    
    unsigned int file_size() const 
    { 
        if (!_file_size) 
            return 0;
        return *_file_size; 
    }

    void file_size(unsigned int val) { _file_size = val; }

private:
    friend class odb::access;
    #pragma db id auto
    unsigned long _id;
    #pragma db type("varchar(64)") index unique
    std::string _message_id;
    #pragma db type("varchar(64)") index
    std::string _session_id;                //所属会话ID
    #pragma db type("varchar(64)")
    std::string _user_id;                   //发送者用户ID
    unsigned char _message_type;            //消息类型 0-文本;1-图片;2-文件;3-语音
    #pragma db type("TIMESTAMP")
    boost::posix_time::ptime _create_time;  //消息的产生时间

    odb::nullable<std::string> _content;    //文本消息内容--非文本消息可以忽略
    #pragma db type("varchar(64)")
    odb::nullable<std::string> _file_id;    //文件消息的文件ID -- 文本消息忽略
    #pragma db type("varchar(128)")
    odb::nullable<std::string> _file_name;  //文件消息的文件名称 -- 只针对文件消息有效
    odb::nullable<unsigned int> _file_size; //文件消息的文件大小 -- 只针对文件消息有效
};

}

elasticsearch

为什么要引入elasticsearch?本质上就是因为,在数据库中进行关键字的模糊匹配效率非常低,因此采用elasticsearch来进行存储和搜索效率会相对高一些

服务重写

老规矩,还是根据brpc生成的服务框架中进行函数重写,然后生成对应的服务信息

其余两个这里就不写了,因为本质上跟前面的差不多

cpp 复制代码
class MessageServiceImpl : public im::MsgStorageService 
{
public:
    MessageServiceImpl(
        const std::shared_ptr<elasticlient::Client> &es_client,
        const std::shared_ptr<odb::core::database> &mysql_client,
        const ServiceManager::ptr &channel_manager,
        const std::string &file_service_name,
        const std::string &user_service_name) :
        _es_message(std::make_shared<ESMessage>(es_client)),
        _mysql_message(std::make_shared<MessageTable>(mysql_client)),
        _file_service_name(file_service_name),
        _user_service_name(user_service_name),
        _mm_channels(channel_manager)
    {
        _es_message->createIndex();
    }

    ~MessageServiceImpl()
    {}

    virtual void GetHistoryMsg(::google::protobuf::RpcController* controller,
        const ::im::GetHistoryMsgReq* request,
        ::im::GetHistoryMsgRsp* response,
        ::google::protobuf::Closure* done) 
    {
        LOG_DEBUG("收到获取历史消息请求!");
        brpc::ClosureGuard rpc_guard(done);
        auto err_response = [this, response](const std::string &rid, 
            const std::string &errmsg) -> void {
            response->set_request_id(rid);
            response->set_success(false);
            response->set_errmsg(errmsg);
            return;
        };

        // 1. 提取关键要素:会话ID,起始时间,结束时间
        std::string rid = request->request_id();
        std::string chat_ssid = request->chat_session_id();
        boost::posix_time::ptime stime = boost::posix_time::from_time_t(request->start_time());
        boost::posix_time::ptime etime = boost::posix_time::from_time_t(request->over_time());
        // 2. 从数据库中进行消息查询
        auto msg_lists = _mysql_message->range(chat_ssid, stime, etime);
        if (msg_lists.empty()) 
        {
            LOG_DEBUG("获取区间消息成功:但是消息数量为0, {}-{}, {}-{}", 
                request->start_time(), request->over_time(),
                boost::posix_time::to_simple_string(stime),
                boost::posix_time::to_simple_string(etime));
            response->set_request_id(rid);
            response->set_success(true);
            return ;
        }

        LOG_DEBUG("指定区间 {}-{}, {}-{} 内的消息数量:{}", 
            request->start_time(), request->over_time(),
            boost::posix_time::to_simple_string(stime),
            boost::posix_time::to_simple_string(etime),
            msg_lists.size());

        // 3. 统计所有文件类型消息的文件ID,并从文件子服务进行批量文件下载
        std::unordered_set<std::string> file_id_lists;
        for (const auto &msg : msg_lists) 
        {
            if (msg.file_id().empty()) 
                continue;
            LOG_DEBUG("需要下载的文件ID: {}", msg.file_id());
            file_id_lists.insert(msg.file_id());
        }

        std::unordered_map<std::string, std::string> file_data_lists;
        bool ret = _GetFile(rid, file_id_lists, file_data_lists);
        if (ret == false) 
        {
            LOG_ERROR("{} 批量文件数据下载失败!", rid);
            return err_response(rid, "批量文件数据下载失败!");
        }

        // 4. 统计所有消息的发送者用户ID,从用户子服务进行批量用户信息获取
        std::unordered_set<std::string> user_id_lists;
        for (const auto &msg : msg_lists) {
            user_id_lists.insert(msg.user_id());
        }
        std::unordered_map<std::string, UserInfo> user_lists;
        ret = _GetUser(rid, user_id_lists, user_lists);
        if (ret == false) 
        {
            LOG_ERROR("{} 批量用户数据获取失败!", rid);
            return err_response(rid, "批量用户数据获取失败!");
        }

        // 5. 组织响应
        response->set_request_id(rid);
        response->set_success(true);
        for (const auto &msg : msg_lists) 
        {
            auto message_info = response->add_msg_list();
            message_info->set_message_id(msg.message_id());
            message_info->set_chat_session_id(msg.session_id());
            message_info->set_timestamp(boost::posix_time::to_time_t(msg.create_time()));
            message_info->mutable_sender()->CopyFrom(user_lists[msg.user_id()]);
            switch(msg.message_type()) 
            {
                case MessageType::STRING:
                    message_info->mutable_message()->set_message_type(MessageType::STRING);
                    message_info->mutable_message()->mutable_string_message()->set_content(msg.content());
                    break;
                case MessageType::IMAGE:
                    message_info->mutable_message()->set_message_type(MessageType::IMAGE);
                    message_info->mutable_message()->mutable_image_message()->set_file_id(msg.file_id());
                    message_info->mutable_message()->mutable_image_message()->set_image_content(file_data_lists[msg.file_id()]);
                    break;
                case MessageType::FILE:
                    message_info->mutable_message()->set_message_type(MessageType::FILE);
                    message_info->mutable_message()->mutable_file_message()->set_file_id(msg.file_id());
                    message_info->mutable_message()->mutable_file_message()->set_file_size(msg.file_size());
                    message_info->mutable_message()->mutable_file_message()->set_file_name(msg.file_name());
                    message_info->mutable_message()->mutable_file_message()->set_file_contents(file_data_lists[msg.file_id()]);
                    break;
                case MessageType::SPEECH:
                    message_info->mutable_message()->set_message_type(MessageType::SPEECH);
                    message_info->mutable_message()->mutable_speech_message()->set_file_id(msg.file_id());
                    message_info->mutable_message()->mutable_speech_message()->set_file_contents(file_data_lists[msg.file_id()]);
                    break;
                default:
                    LOG_ERROR("消息类型错误!!");
                    return;
            }
        }

        return;
    }

    virtual void GetRecentMsg(::google::protobuf::RpcController* controller,
        const ::im::GetRecentMsgReq* request,
        ::im::GetRecentMsgRsp* response,
        ::google::protobuf::Closure* done) 
    {
        brpc::ClosureGuard rpc_guard(done);
        auto err_response = [this, response](const std::string &rid, 
            const std::string &errmsg) -> void {
            response->set_request_id(rid);
            response->set_success(false);
            response->set_errmsg(errmsg);
            return;
        };
        // 1. 提取请求中的关键要素:请求ID,会话ID,要获取的消息数量
        std::string rid = request->request_id();
        std::string chat_ssid = request->chat_session_id();
        int msg_count = request->msg_count();

        // 2. 从数据库,获取最近的消息元信息
        auto msg_lists = _mysql_message->recent(chat_ssid, msg_count);
        if (msg_lists.empty()) 
        {
            response->set_request_id(rid);
            response->set_success(true);
            return ;
        }

        // 3. 统计所有消息中文件类型消息的文件ID列表,从文件子服务下载文件
        std::unordered_set<std::string> file_id_lists;
        for (const auto &msg : msg_lists) 
        {
            if (msg.file_id().empty()) 
                continue;
            LOG_DEBUG("需要下载的文件ID: {}", msg.file_id());
            file_id_lists.insert(msg.file_id());
        }

        std::unordered_map<std::string, std::string> file_data_lists;
        bool ret = _GetFile(rid, file_id_lists, file_data_lists);
        if (ret == false) 
        {
            LOG_ERROR("{} 批量文件数据下载失败!", rid);
            return err_response(rid, "批量文件数据下载失败!");
        }

        // 4. 统计所有消息的发送者用户ID,从用户子服务进行批量用户信息获取
        std::unordered_set<std::string> user_id_lists;
        for (const auto &msg : msg_lists) 
        {
            user_id_lists.insert(msg.user_id());
        }
        std::unordered_map<std::string, UserInfo> user_lists;
        ret = _GetUser(rid, user_id_lists, user_lists);
        if (ret == false) 
        {
            LOG_ERROR("{} 批量用户数据获取失败!", rid);
            return err_response(rid, "批量用户数据获取失败!");
        }

        // 5. 组织响应
        response->set_request_id(rid);
        response->set_success(true);
        for (const auto &msg : msg_lists) 
        {
            auto message_info = response->add_msg_list();
            message_info->set_message_id(msg.message_id());
            message_info->set_chat_session_id(msg.session_id());
            message_info->set_timestamp(boost::posix_time::to_time_t(msg.create_time()));
            message_info->mutable_sender()->CopyFrom(user_lists[msg.user_id()]);
            switch(msg.message_type()) 
            {
                case MessageType::STRING:
                    message_info->mutable_message()->set_message_type(MessageType::STRING);
                    message_info->mutable_message()->mutable_string_message()->set_content(msg.content());
                    break;
                case MessageType::IMAGE:
                    message_info->mutable_message()->set_message_type(MessageType::IMAGE);
                    message_info->mutable_message()->mutable_image_message()->set_file_id(msg.file_id());
                    message_info->mutable_message()->mutable_image_message()->set_image_content(file_data_lists[msg.file_id()]);
                    break;
                case MessageType::FILE:
                    message_info->mutable_message()->set_message_type(MessageType::FILE);
                    message_info->mutable_message()->mutable_file_message()->set_file_id(msg.file_id());
                    message_info->mutable_message()->mutable_file_message()->set_file_size(msg.file_size());
                    message_info->mutable_message()->mutable_file_message()->set_file_name(msg.file_name());
                    message_info->mutable_message()->mutable_file_message()->set_file_contents(file_data_lists[msg.file_id()]);
                    break;
                case MessageType::SPEECH:
                    message_info->mutable_message()->set_message_type(MessageType::SPEECH);
                    message_info->mutable_message()->mutable_speech_message()->set_file_id(msg.file_id());
                    message_info->mutable_message()->mutable_speech_message()->set_file_contents(file_data_lists[msg.file_id()]);
                    break;
                default:
                    LOG_ERROR("消息类型错误!!");
                    return;
            }
        }
        return;
    }

    virtual void MsgSearch(::google::protobuf::RpcController* controller,
        const ::im::MsgSearchReq* request,
        ::im::MsgSearchRsp* response,
        ::google::protobuf::Closure* done) 
    {
        brpc::ClosureGuard rpc_guard(done);
        auto err_response = [this, response](const std::string &rid, 
            const std::string &errmsg) -> void {
            response->set_request_id(rid);
            response->set_success(false);
            response->set_errmsg(errmsg);
            return;
        };

        // 关键字的消息搜索--只针对文本消息
        // 1. 从请求中提取关键要素:请求ID,会话ID, 关键字
        std::string rid = request->request_id();
        std::string chat_ssid = request->chat_session_id();
        std::string skey = request->search_key();

        // 2. 从ES搜索引擎中进行关键字消息搜索,得到消息列表
        auto msg_lists = _es_message->search(skey, chat_ssid);
        if (msg_lists.empty()) 
        {
            response->set_request_id(rid);
            response->set_success(true);
            return ;
        }

        // 3. 组织所有消息的用户ID,从用户子服务获取用户信息
        std::unordered_set<std::string> user_id_lists;
        for (const auto &msg : msg_lists) 
        {
            user_id_lists.insert(msg.user_id());
        }
        std::unordered_map<std::string, UserInfo> user_lists;
        bool ret = _GetUser(rid, user_id_lists, user_lists);
        if (ret == false) 
        {
            LOG_ERROR("{} 批量用户数据获取失败!", rid);
            return err_response(rid, "批量用户数据获取失败!");
        }

        // 4. 组织响应 
        response->set_request_id(rid);
        response->set_success(true);
        for (const auto &msg : msg_lists) 
        {
            auto message_info = response->add_msg_list();
            message_info->set_message_id(msg.message_id());
            message_info->set_chat_session_id(msg.session_id());
            message_info->set_timestamp(boost::posix_time::to_time_t(msg.create_time()));
            message_info->mutable_sender()->CopyFrom(user_lists[msg.user_id()]);
            message_info->mutable_message()->set_message_type(MessageType::STRING);
            message_info->mutable_message()->mutable_string_message()->set_content(msg.content());
        }
        return;
    }

    void onMessage(const char *body, size_t sz) 
    {
        LOG_DEBUG("收到新消息,进行存储处理!");

        // 1. 取出序列化的消息内容,进行反序列化
        im::MessageInfo message;
        bool ret = message.ParseFromArray(body, sz);
        if (ret == false) 
        {
            LOG_ERROR("对消费到的消息进行反序列化失败!");
            return;
        }

        // 2. 根据不同的消息类型进行不同的处理
        std::string file_id, file_name, content;
        int64_t file_size;
        switch(message.message().message_type()) 
        {
            // 1. 如果是一个文本类型消息,取元信息存储到ES中
            case MessageType::STRING:
                content = message.message().string_message().content();
                ret = _es_message->appendData(
                    message.sender().user_id(),
                    message.message_id(),
                    message.timestamp(),
                    message.chat_session_id(),
                    content);
                if (ret == false) 
                {
                    LOG_ERROR("文本消息向存储引擎进行存储失败!");
                    return;
                }
                break;

            //  2. 如果是一个图片/语音/文件消息,则取出数据存储到文件子服务中,并获取文件ID
            case MessageType::IMAGE:
                {
                    const auto &msg = message.message().image_message();
                    ret = _PutFile("", msg.image_content(), msg.image_content().size(), file_id);
                    if (ret == false) 
                    {
                        LOG_ERROR("上传图片到文件子服务失败!");
                        return ;
                    }
                }
                break;

            case MessageType::FILE:
                {
                    const auto &msg = message.message().file_message();
                    file_name = msg.file_name();
                    file_size = msg.file_size();
                    ret = _PutFile(file_name, msg.file_contents(), file_size, file_id);
                    if (ret == false) 
                    {
                        LOG_ERROR("上传文件到文件子服务失败!");
                        return ;
                    }
                }
                break;

            case MessageType::SPEECH:
                {
                    const auto &msg = message.message().speech_message();
                    ret = _PutFile("", msg.file_contents(), msg.file_contents().size(), file_id);
                    if (ret == false) 
                    {
                        LOG_ERROR("上传语音到文件子服务失败!");
                        return ;
                    }
                }
                break;

            default:
                LOG_ERROR("消息类型错误!");
                return;
        }

        // 3. 提取消息的元信息,存储到mysql数据库中
        im::Message msg(message.message_id(), 
            message.chat_session_id(),
            message.sender().user_id(),
            message.message().message_type(),
            boost::posix_time::from_time_t(message.timestamp()));
        msg.content(content);
        msg.file_id(file_id);
        msg.file_name(file_name);
        msg.file_size(file_size);
        ret = _mysql_message->insert(msg);
        if (ret == false) 
        {
            LOG_ERROR("向数据库插入新消息失败!");
            return;
        }
    }

private:
    bool _GetUser(const std::string &rid,
        const std::unordered_set<std::string> &user_id_lists,
        std::unordered_map<std::string, UserInfo> &user_lists) 
    {
        auto channel = _mm_channels->choose(_user_service_name);
        if (!channel) 
        {
            LOG_ERROR("{} 没有可供访问的用户子服务节点!",  _user_service_name);
            return false;
        }
        UserService_Stub stub(channel.get());
        GetMultiUserInfoReq req;
        GetMultiUserInfoRsp rsp;
        req.set_request_id(rid);
        for (const auto &id : user_id_lists) 
        {
            req.add_users_id(id);
        }

        brpc::Controller cntl;
        stub.GetMultiUserInfo(&cntl, &req, &rsp, nullptr);
        if (cntl.Failed() == true || rsp.success() == false) 
        {
            LOG_ERROR("用户子服务调用失败:{}!", cntl.ErrorText());
            return false;
        }
        const auto &umap = rsp.users_info();
        for (auto it = umap.begin(); it != umap.end(); ++it) 
        {
            user_lists.insert(std::make_pair(it->first, it->second));
        }

        return true;
    }

    bool _GetFile(const std::string &rid,
        const std::unordered_set<std::string> &file_id_lists,
        std::unordered_map<std::string, std::string> &file_data_lists) 
    {
        auto channel = _mm_channels->choose(_file_service_name);
        if (!channel) 
        {
            LOG_ERROR("{} 没有可供访问的文件子服务节点!",  _file_service_name);
            return false;
        }

        FileService_Stub stub(channel.get());
        GetMultiFileReq req;
        GetMultiFileRsp rsp;
        req.set_request_id(rid);
        for (const auto &id : file_id_lists) 
        {
            req.add_file_id_list(id);
        }

        brpc::Controller cntl;
        stub.GetMultiFile(&cntl, &req, &rsp, nullptr);
        if (cntl.Failed() == true || rsp.success() == false) 
        {
            LOG_ERROR("文件子服务调用失败:{}!", cntl.ErrorText());
            return false;
        }

        const auto &fmap = rsp.file_data();
        for (auto it = fmap.begin(); it != fmap.end(); ++it) 
        {
            file_data_lists.insert(std::make_pair(it->first, it->second.file_content()));
        }

        return true;
    }

    bool _PutFile(const std::string &filename,
        const std::string &body, 
        const int64_t fsize,
        std::string &file_id) 
    {
        // 实现文件数据的上传
        auto channel = _mm_channels->choose(_file_service_name);
        if (!channel) 
        {
            LOG_ERROR("{} 没有可供访问的文件子服务节点!",  _file_service_name);
            return false;
        }

        FileService_Stub stub(channel.get());
        PutSingleFileReq req;
        PutSingleFileRsp rsp;
        req.mutable_file_data()->set_file_name(filename);
        req.mutable_file_data()->set_file_size(fsize);
        req.mutable_file_data()->set_file_content(body);
        brpc::Controller cntl;
        stub.PutSingleFile(&cntl, &req, &rsp, nullptr);
        if (cntl.Failed() == true || rsp.success() == false) 
        {
            LOG_ERROR("文件子服务调用失败:{}!", cntl.ErrorText());
            return false;
        }

        file_id = rsp.file_info().file_id();
        return true;
    }

private:
    ESMessage::ptr _es_message;
    MessageTable::ptr _mysql_message;
    //这边是rpc调用客户端相关对象
    std::string _user_service_name;
    std::string _file_service_name;
    ServiceManager::ptr _mm_channels;
};
相关推荐
柳叶寒2 天前
医院信息化与智能化系统(17)
java·nacos·gateway·全栈·项目
cyt涛4 天前
Docker — 跨平台和环境部署
java·运维·mysql·docker·容器·部署·项目
柳叶寒6 天前
医院信息化与智能化系统(15)
java·数据库·全栈·项目
cyt涛7 天前
Tomcat 和 Docker部署Java项目的区别
java·docker·微服务·tomcat·部署·项目·单体
Jocelyn_书8 天前
uniapp编译多端项目App、小程序,input框键盘输入后
小程序·uni-app·项目
柳叶寒13 天前
医院信息化与智能化系统(8)
java·数据库·全栈·项目
柳叶寒15 天前
医院信息化与智能化系统(6)
java·全栈·项目
柳叶寒21 天前
医院信息化与智能化系统(1)
java·全栈·项目
阿珏酱22 天前
多平台文章同步浏览器插件 – ArticleSync
api·项目·折腾代码
程序员鱼皮1 个月前
我整理了 50 多个简历致命问题,知道为什么投简历没回复了!
面试·程序员·求职·项目·简历