微服务即时通讯系统(4)语音识别子服务,消息转发子服务,消息管理子服务

  1. 语音识别子服务(speech文件)

在七个模块中语音识别子服务最为独立,也最为简单,下面是他的接口,只有一个业务,就是将语音文件转换成文字消息,主要还是调用了阿里云的语音识别SDK进行

service SpeechService {

rpc SpeechRecognition(SpeechRecognitionReq) returns

(SpeechRecognitionRsp); }

语音识别接口:从request里面获取用户ID和语音文件,然后调用阿里云的语音识别接口,获得对应的文本信息,将文本消息放入request里面就结束

void SpeechRecognition(google::protobuf::RpcController* controller,

const ::zhou::SpeechRecognitionReq* request,

::zhou::SpeechRecognitionRsp* response,

::google::protobuf::Closure* done)

{

LOG_DEBUG("收到语音转文字请求!");

brpc::ClosureGuard rpc_guard(done);

std::string err ;

std::string res = _asr_client->recognize(request->speech_content(), err);

if(res.empty())

{

LOG_ERROR("{}语音识别失败", request->request_id());

response->set_request_id(request->request_id());

response->set_success(false);

response->set_errmsg("语音识别失败:"+err);

return ;

}

response->set_request_id(request->request_id());

response->set_success(true);

response->set_recognition_result(res);

}

  1. 消息转发子服务(transmite文件)

消息转发子服务就只有一个接口,但是却是项目中的核心功能,主要是利用rabbitmq将消息转发给消息管理子服务让其进行持久化

service MsgTransmitService {

rpc GetTransmitTarget(NewMessageReq) returns

(GetTransmitTargetRsp);

}

消息转发接口:从request里面获得用户ID,会话ID,获得消息的正文(采用MessageContent结构进行接收),通过用户子服务获取用户的个人信息,然后再新建一条message结构,将message的个人信息,content,时间,会话ID,进行添加好以后,再将消息放入rabbitmq的发布机里面,实现消息的一个生产(后面的消费是消息子服务的工作),再将消息内容和需要转发的人的uid放入response里面,结束

void GetTransmitTarget(google::protobuf::RpcController* controller,

const ::zhou::NewMessageReq* request,

::zhou::GetTransmitTargetRsp* response,

::google::protobuf::Closure* done) override {

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;

};

//从请求中获取关键信息:用户ID,所属会话ID,消息内容

std::string rid = request->request_id();

std::string uid = request->user_id();

std::string chat_ssid = request->chat_session_id();

const MessageContent &content = request->message();

// 进行消息组织:发送者-用户子服务获取信息,所属会话,消息内容,产生时间,消息ID

auto channel = _mm_channels->choose(_user_service_name);

if (!channel) {

LOG_ERROR("{}-{} 没有可供访问的用户子服务节点!", rid, _user_service_name);

return err_response(rid, "没有可供访问的用户子服务节点!");

}

UserService_Stub stub(channel.get());

GetUserInfoReq req;

GetUserInfoRsp rsp;

req.set_request_id(rid);

req.set_user_id(uid);

brpc::Controller cntl;

stub.GetUserInfo(&cntl, &req, &rsp, nullptr);

if (cntl.Failed() == true || rsp.success() == false) {

LOG_ERROR("{} - 用户子服务调用失败:{}!", request->request_id(), cntl.ErrorText());

return err_response(request->request_id(), "用户子服务调用失败!");

}

MessageInfo message;

message.set_message_id(uuid());

message.set_chat_session_id(chat_ssid);

message.set_timestamp(time(nullptr));

message.mutable_sender()->CopyFrom(rsp.user_info());

message.mutable_message()->CopyFrom(content);

// 获取消息转发客户端用户列表

auto target_list = _mysql_session_member_table->members(chat_ssid);

// 将封装完毕的消息,发布到消息队列,待消息存储子服务进行消息持久化

bool ret = _mq_client->publish(_exchange_name, message.SerializeAsString(), _routing_key);

if (ret == false) {

LOG_ERROR("{} - 持久化消息发布失败:{}!", request->request_id(), cntl.ErrorText());

return err_response(request->request_id(), "持久化消息发布失败:!");

}

//组织响应

response->set_request_id(rid);

response->set_success(true);

response->mutable_message()->CopyFrom(message);

for (const auto &id : target_list) {

response->add_target_id_list(id);

}

}

  1. 消息管理子服务(message文件)

消息管理子服务功能主要是包括消息的管理,查询和消费,虽然接口不多,但都是核心功能

service MsgStorageService {

rpc GetHistoryMsg(GetHistoryMsgReq) returns

(GetHistoryMsgRsp); rpc GetRecentMsg(GetRecentMsgReq) returns (GetRecentMsgRsp);

rpc MsgSearch(MsgSearchReq) returns (MsgSearchRsp);

在介绍三个接口之前,先介绍一个关键的函数(消息消费函数)onMessage:因为消息是四种类型,而我们对每种消息的存储处理不一样,并且在消息转发子服务中,我们只是将消息添加到rabbitmq发送队列,并没有对消息进行消费,现在要在消息子服务中对消息进行消费,所以存在这个函数,首先,我们从对从rabbitmq里面获取的消息进行序列化得到MessageInfo类型,然后通过message.message().message_type()函数获得该消息是什么类型的,若是普通文本类型,就将其存入ES搜索引擎内部,如果是图片类型,则将其上传到文件管理子服务处,并得到图片的文件ID,如果是文件类型,也上传到文字子服务处,获得对应的文件名,文件ID,文件大小,如果是语音文件就上传到文件子服务,得到对应的文件ID,

然后无论是什么类型的消息,最后都会被抽象化为message结构,填充文件ID,会话ID,文件类型,产生时间,消息内容(只有文本消息有),文件ID,文件名,文件大小(三者只有大类文件消息有),然后再将这个结构插入数据库,而这个函数的本质是一个回调函数,当消息转发服务器发送消息时,这边就会接收到发布的消息然后调用这个函数,让这个函数对消息进行持久化(存入数据库的搜索引擎),这这个函数是在初始化这个调用服务时被绑定

也就是这里

MessageServiceImpl *msg_service = new MessageServiceImpl(_es_client,

_mysql_client, _mm_channels, _file_service_name, _user_service_name);

int ret = _rpc_server->AddService(msg_service,

brpc::ServiceOwnership::SERVER_OWNS_SERVICE);

if (ret == -1) {

LOG_ERROR("添加Rpc服务失败!");

abort();

}

brpc::ServerOptions options;

options.idle_timeout_sec = timeout;

options.num_threads = num_threads;

ret = _rpc_server->Start(port, &options);

if (ret == -1) {

LOG_ERROR("服务启动失败!");

abort();

}

auto callback = std::bind(&MessageServiceImpl::onMessage, msg_service,

std::placeholders::_1, std::placeholders::_2);

_mq_client->consume(_queue_name, callback);

上面是绑定onmessage的关键代码,下面是onmessage的实现,二者并不连接

void onMessage(const char *body, size_t sz) {

LOG_DEBUG("收到新消息,进行存储处理!");

//1. 取出序列化的消息内容,进行反序列化

zhou::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数据库中

zhou::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;

}

}

  1. 获取历史消息的接口设计:先从request里面获得会话ID,用户ID,起始时间,结束时间,然后再数据库中进行查询,得到对应的消息列表,在从消息列表中剥离文件消息,获取所有需要下载的文件消息的ID,再通过文件子服务获得所有文件ID对应的文件,然后再从消息的列表里面获取对应的userid,通过用户子服务来获取所有的用户信息,然后通过遍历消息列表,通过里面的message_id的索引方式来按照时间顺序将消息(无论是文件消息还是文本消息都按照顺序返回,还是由于因为我们获取批量文件是,使用map进行装载吗,而索引便是文件对应的消息ID)

virtual void GetHistoryMsg(::google::protobuf::RpcController* controller,

const ::zhou::GetHistoryMsgReq* request,

::zhou::GetHistoryMsgRsp* 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,起始时间,结束时间

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()) {

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;

}

  1. 获取最近消息的接口:这个的逻辑和上面通过时间获取消息的接口逻辑大差不差,主要是这个接口是从最近开始的时间起算,然后你传入需要的消息条数,然后忘ES搜索引擎和数据库中进行查询,然后调用文件子服务和用户子服务,得到消息和用户信息以后填入response,进行返回

virtual void GetRecentMsg(::google::protobuf::RpcController* controller,

const ::zhou::GetRecentMsgReq* request,

::zhou::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;

}

  1. 消息查询接口:搜索消息接口和上面的两个接口也是差不多的,主要的区别是这次消息的搜索是在ES里面对文本消息进行搜索,得到以后通过用户子服务得到用户的个人信息,然后对response进行填充,完成

virtual void MsgSearch(::google::protobuf::RpcController* controller,

const ::zhou::MsgSearchReq* request,

::zhou::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;

}

在消息管理子服务中,最重要的就是消息的消费和持久化,然后是对消息的获取和查询工作

相关推荐
GDDGHS_25 分钟前
Flink的架构体系
大数据·架构·flink
李宥小哥1 小时前
架构15-服务网格
架构
李宥小哥1 小时前
架构11-虚拟化容器
架构
小春学渗透1 小时前
DAY168内网对抗-基石框架篇&单域架构&域内应用控制&成员组成&用户策略&信息收集&环境搭建
网络·安全·架构·内网攻防
m0_748232922 小时前
大数据-155 Apache Druid 架构与原理详解 数据存储 索引服务 压缩机制
大数据·架构·apache
秋意钟2 小时前
MySQL基本架构
数据库·mysql·架构
檀越剑指大厂2 小时前
【Docker系列】Docker 构建多平台镜像:arm64 架构的实践
docker·容器·架构
uhakadotcom3 小时前
Bun相比npm包管理工具有啥优点?
前端·架构·github
O(1)的boot4 小时前
微服务的问题
java·数据库·微服务
给我买个墨镜戴4 小时前
黑马商城微服务复习(5)
微服务·云原生·架构