文章目录
消息存储服务
功能设计
消息存储子服务,顾名思义,把消息进行存储
- 文本消息:存储到elasticsearch文档搜索服务中
- 文件/语音/图片:存储到文件管理子服务中
项目为什么要引入elasticsearch?其实主要目的就是想要能够完成历史消息的搜索:
- 获取最近的N条消息
- 获取指定时间段的消息
- 根据关键字搜索好友聊天消息
模块划分
- 基于gflags进行配置文件和参数解析服务
- 基于spdlog进行日志输出服务
- 基于etcd进行服务注册中心服务
- 基于ODB实现的MySQL对象管理,这个主要是用于从RabbitMQ中消费到消息之后,向数据库中存储一份,以便于通过时间进行范围查找,从数据库根据指定用户的所有好友消息
- 基于rpc服务进行远程调用
- 基于RabbitMQ实现的,从消息队列服务器消费获取聊天消息,将文本消息存储到elasticsearch服务,将文件消息存储到文件管理子服务,将所有的消息都存储到MySQL中一份
想要对于这些数据进行存储,那么首先要先对于数据进行描述,因此就要用一些信息来描述文本和文件的消息
数据管理
数据库中只存储的是文本消息和其他类型消息的元信息
数据库结构
- 消息ID
- 消息产生时间
- 消息发送者用户ID
- 消息产生会话ID
- 消息类型
- 消息内容
- 文件ID
- 文件大小
- 文件名称
数据库操作
- 新增消息
- 通过消息ID来获取消息信息
- 通过会话ID进行时间范围查询消息
- 通过会话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;
};