您的位置: 首页 - 站长

WaP网站模块公司网站建设维护的岗位

当前位置: 首页 > news >正文

WaP网站模块,公司网站建设维护的岗位,外贸订单的网站,长沙哪里可以做网站目录 1. 消息存储子服务的实现1.1 功能设计1.2 模块划分1.3 模块功能示意图1.4 数据管理1.4.1 数据库消息管理1.4.2 ES文本消息管理 1.5 接口的实现1.5.1 消息存储子服务所用到的protobuf接口实现1.5.2 最近N条消息获取接口实现1.5.3 指定时间段消息搜索接口实现1.5.4 关键字消… 目录 1. 消息存储子服务的实现1.1 功能设计1.2 模块划分1.3 模块功能示意图1.4 数据管理1.4.1 数据库消息管理1.4.2 ES文本消息管理 1.5 接口的实现1.5.1 消息存储子服务所用到的protobuf接口实现1.5.2 最近N条消息获取接口实现1.5.3 指定时间段消息搜索接口实现1.5.4 关键字消息搜索接口实现1.5.5 搭建Rpc服务和创建消息存储子服务的工厂类 2. 好友管理子服务的实现2.1 功能设计2.2 模块划分2.3 功能模块示意图2.4. 数据库数据管理2.4.1 用户关系表的实现2.4.2 会话信息表的实现2.4.3 会话成员表的实现2.4.4 好友申请事件表的实现 2.5 ES用户信息管理2.6 接口的实现2.6.1 消息存储子服务所用到的protobuf接口实现2.6.2 获取好友列表和申请添加好友接口实现2.6.3 获取待处理好友申请事件和处理好友申请接口实现2.6.4 删除好友和搜索好友接口实现2.6.5 会话操作接口实现2.6.6 搭建Rpc服务和创建用户子服务的工厂类 3. 入口网关子服务的实现3.1 功能设计3.2 模块划分3.3 模块功能示意图3.4 接口的实现 4 Docker的介绍4.1 docker的安装4.2 docker常用指令4.2.1 容器操作4.2.2 镜像操作 4.3 dockerfile编写规则简介4.4 dockercompose编写规则简介 5. 项目部署5.1 编写项目配置文件5.2 查询程序依赖5.3 编写每个子服务的dockerfile文件5.4 编写entrypoint.sh文件5.5 编写docker-compose文件 6. 服务端总结 1. 消息存储子服务的实现 1.1 功能设计 1消息存储子服务主要用于管理消息的存储 文本消息储存在 ElasticSearch 文档搜索服务中。文件/语音/图片需要转储到文件管理子服务中。 2除了管理消息的存储还需要管理消息的搜索获取因此需要对外提供以下接口 获取历史消息 获取最近 N 条消息用于登录成功后点击对方头像打开聊天框时显示最近的消息。获取指定时间段内的消息用户可以进行聊天消息的按时间搜索。 关键字消息搜索用户可以针对指定好友的聊天进行聊天消息的关键字搜索。 1.2 模块划分 1以下是消息存储模块划分 参数/配置文件解析模块基于 gflags 框架直接使用进行参数/配置文件解析。日志模块基于 spdlog 框架封装的模块直接使用进行日志输出。服务注册模块基于 etcd 框架封装的注册模块直接使用进行聊天消息存储子服务的注册。数据库数据操作模块基于 odb-mysql 数据管理封装的模块进行数据库数据操作用于从 MQ 中消费到消息后向数据库中存储一份以便于通过时间进行范围性查找。从数据库根据指定用户的所有好友信息。rpc 服务模块基于 brpc 框架搭建 rpc 服务器。服务发现与调用模块基于 etcd 框架与 brpc 框架封装的服务发现与调用模块 连接文件管理子服务将文件/语音/图片类型的消息以及用户头像之类的文件数据转储到文件管理子服务。连接用户管理子服务在消息搜索时根据发送用户的 ID 获取发送者用户信息。 ES 客户端模块基于 elasticsearch 框架实现访问客户端向 es 服务器进行文本聊天消息的存储以便于文本消息的关键字搜索。MQ 消费模块基于 rabbitmq-client 封装的消费者模块从消息队列服务器消费获取聊天消息将文本消息存储到 ElasticSearch 服务将文件消息转储到文件管理子服务所有消息的简息都需要向数据库存储一份。 1.3 模块功能示意图 1如下是模块功能图
1.4 数据管理 1.4.1 数据库消息管理 1在消息的存储管理中所有的消息简息都要在数据库中存储一份进行消息的持久化以便于进行时间范围性查询和离线消息的实现。消息类型有四种文本文件语音图片。我们不可能将文件数据也存储到数据库中因此数据库中只存储文本消息和其他类型消息的元信息即可。 2消息存储数据库表结构 消息 ID唯一标识。消息产生时间用于进行时间性搜索。消息发送者用户 ID明确消息的发送者。消息产生会话 ID明确消息属于哪个会话。消息类型明确消息的类型。消息内容只存储文本消息文件/语音/图片数据不进行存储或者说是存储在文件子服务中。文件 ID只有文件/语音/图片类消息会用到。文件大小只有文件/语音/图片类消息会用到。文件名称只有文件类消息会用到。 3数据库操作 新增消息。通过消息 ID 获取消息信息。通过会话 ID时间范围获取指定时间段之内的消息并按时间进行排序。通过会话 ID消息数量获取最近的 N 条消息逆序limit 即可。 4ODB映射数据结构message.hxx的实现 #pragma once #include string #include cstddef #include odb/nullable.hxx #include odb/core.hxx #include boost/date_time/posix_time/posix_time.hppnamespace MyTest {#pragma db object table(message)class Message {public: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; }void content(const std::string val) { _content val; }std::string content() const { if(!_content){return std::string();}return *_content; }void file_id(const std::string val) { _file_id val; }std::string file_id() const { if(!_file_id){return std::string();}return *_file_id; }void file_name(const std::string val) { _file_name val; }std::string file_name() const { if(!_file_name){return std::string();}return *_file_name; }void file_size(unsigned int val) { _file_size val; }unsigned int file_size() const { if(!_file_size){return 0;}return _file_size; } private:friend class odb::access;#pragma db id autounsigned long _id;#pragma db type(varchar(64)) index uniquestd::string _message_id;#pragma db type(varchar(64)) indexstd::string _session_id; //所属会话ID#pragma db type(varchar(64))std::string _user_id; //发送者用户IDunsigned char _message_type; //消息类型 0-文本1-图片2-文件3-语音#pragma db type(TIMESTAMP)boost::posix_time::ptime _create_time; //消息的产生时间odb::nullablestd::string _content; //文本消息内容–非文本消息可以忽略#pragma db type(varchar(64))odb::nullablestd::string _file_id; //文件消息的文件ID – 文本消息忽略#pragma db type(varchar(128))odb::nullablestd::string _file_name; //文件消息的文件名称 – 只针对文件消息有效odb::nullableunsigned int _file_size; //文件消息的文件大小 – 只针对文件消息有效}; }5运行如下命令可以通过odb生成mysql代码 odb -d mysql –std c11 –generate-query –generate-schema –profile boost/date-time message.hxx # 最后所要填写的取决与文件所在的路径6生成的message.sql代码 / This file was generated by ODB, object-relational mapping (ORM)* compiler for C./CREATE DATABASE IF NOT EXISTS bite_im; USE bite_im; DROP TABLE IF EXISTS message;CREATE TABLE message (id BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,message_id varchar(64) NOT NULL,session_id varchar(64) NOT NULL,user_id varchar(64) NOT NULL,message_type TINYINT UNSIGNED NOT NULL,create_time TIMESTAMP NULL,content TEXT NULL,file_id varchar(64) NULL,file_name varchar(128) NULL,file_size INT UNSIGNED NULL)ENGINEInnoDB;CREATE UNIQUE INDEX message_id_iON message (message_id);CREATE INDEX session_id_iON message (session_id);1.4.2 ES文本消息管理 1因为当前聊天室项目中实现了聊天内容的关键字搜索功能但是如果在数据库中进行关键字的模糊匹配则效率会非常低因此采用 ES 进行消息内容存储与搜索但是在搜索的时候需要进行会话的过滤因此这里也要考虑 ES 索引的构造。 2对消息ES的封装 #include icsearch.hpp #include message.hxxnamespace MyTest {class ESMessage{public:using ptr std::shared_ptrESMessage;ESMessage(const std::shared_ptrelasticlient::Client es_client) :_es_client(es_client) {}bool createIndex(){bool ret ESIndex(_es_client, message).append(user_id, keyword, standard, false).append(message_id, keyword, standard, false).append(create_time, long, standard, false).append(chat_session_id, keyword, standard, true).append(content).create();if(ret false){LOG_INFO(消息信息索引创建失败!);return false;}LOG_INFO(消息信息索引创建成功!);return true;}bool appendData(const std::string user_id,const std::string message_id,const long create_time,const std::string chat_session_id,const std::string content){bool ret ESInsert(_es_client, message).append(message_id, message_id).append(create_time, create_time).append(user_id, user_id).append(chat_session_id, chat_session_id).append(content, content).insert(message_id);if(ret false){LOG_ERROR(消息数据插入/更新失败!);return false;}LOG_INFO(消息数据新增/更新成功!);return true;}bool remove(const std::string mid){bool ret ESRemove(_es_client, message).remove(mid);if(ret false){LOG_ERROR(消息数据删除失败!);return false;}LOG_INFO(消息数据删除成功!);return true;}std::vectorbite_im::Message search(const std::string key, const std::string ssid){std::vectorbite_im::Message res;Json::Value json_user ESSearch(_es_client, message).append_must_term(chat_session_id.keyword, ssid).append_must_match(content, key).search();if(json_user.isArray() false){LOG_ERROR(用户搜索结果为空或者结果不是数组类型);return res;}int sz json_user.size();LOG_DEBUG(检索结果条目数量{}, sz);for (int i 0; i sz; i){bite_im::Message message;message.user_id(json_user[i][_source][user_id].asString());message.message_id(json_user[i][_source][message_id].asString());boost::posix_time::ptime ctime(boost::posix_time::from_time_t(json_user[i][_source][create_time].asInt64()));message.create_time(ctime);message.session_id(json_user[i][_source][chat_session_id].asString());message.content(json_user[i][_source][content].asString());res.push_back(message);}return res;}private:std::shared_ptrelasticlient::Client _es_client;}; }1.5 接口的实现 1.5.1 消息存储子服务所用到的protobuf接口实现 syntax proto3; package bite_im; import base.proto;option cc_generic_services true;message GetHistoryMsgReq {string request_id 1;string chat_session_id 2;int64 start_time 3;int64 over_time 4;optional string user_id 5;optional string session_id 6; } message GetHistoryMsgRsp {string request_id 1;bool success 2;string errmsg 3; repeated MessageInfo msg_list 4; }message GetRecentMsgReq {string request_id 1;string chat_session_id 2;int64 msg_count 3;optional int64 cur_time 4;//用于扩展获取指定时间前的n条消息optional string user_id 5;optional string session_id 6; } message GetRecentMsgRsp {string request_id 1;bool success 2;string errmsg 3; repeated MessageInfo msg_list 4; }message MsgSearchReq {string request_id 1;optional string user_id 2;optional string session_id 3;string chat_session_id 4;string search_key 5; } message MsgSearchRsp {string request_id 1;bool success 2;string errmsg 3; repeated MessageInfo msg_list 4; }service MsgStorageService {rpc GetHistoryMsg(GetHistoryMsgReq) returns (GetHistoryMsgRsp);rpc GetRecentMsg(GetRecentMsgReq) returns (GetRecentMsgRsp);rpc MsgSearch(MsgSearchReq) returns (MsgSearchRsp); }1.5.2 最近N条消息获取接口实现 从请求中获取会话 ID 要获取的消息数量。访问数据库从数据库中按时间排序获取指定数量的消息简略信息消息 ID会话 ID消息类型产生时间发送者用户 ID文本消息内容文件消息元信息。循环构造完整消息从用户子服务获取消息的发送者用户信息从文件子服务获取文件/语音/图片数据。组织响应返回给网关服务器。 namespace MyTest {class MessageServiceImpl : public bite_im::MsgStorageService{public:MessageServiceImpl(const std::shared_ptrelasticlient::Client es_client,const std::shared_ptrodb::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_sharedESMessage(es_client)),_mysql_message(std::make_sharedMessageTable(mysql_client)),_file_service_name(file_service_name),_user_service_name(user_service_name),_mm_channels(channel_manager){_es_message-createIndex();}virtual void GetRecentMsg(::google::protobuf::RpcController controller,const ::bite_im::GetRecentMsgReq* request,::bite_im::GetRecentMsgRsp* response,::google::protobuf::Closure* done) {brpc::ClosureGuard rpc_guard(done);auto err_response this, response - 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_setstd::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_mapstd::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_setstd::string user_id_lists; // {猪爸爸吧 祝妈妈猪爸爸吧祝爸爸}for(const auto msg : msg_lsits){user_id_lists.insert(msg.user_id());}std::unordered_mapstd::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(消息类型错误);break;}}}private:bool _GetUser(const std::string rid,const std::unordered_setstd::string user_id_lists,std::unordered_mapstd::string, UserInfo user_lists){auto channel _mm_channels-choose(_user_service_name);if(!channel) {LOG_ERROR({} 没有可供访问的文件子服务节点, _file_service_name);return false;}bite_im::UserService_Stub stub(channel.get());bite_im::GetMultiUserInfoReq req;bite_im::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 iter umap.begin(); iter ! umap.end(); iter) {user_lists.insert(std::make_pair(iter-first, iter-second));}return true;}bool _GetFile(const std::string rid,const std::unordered_setstd::string file_id_lists,std::unordered_mapstd::string, std::string file_data_lists){auto channel _mm_channels-choose(_file_service_name);if(!channel) {LOG_ERROR({} 没有可供访问的文件子服务节点, _file_service_name);return false;}bite_im::FileService_Stub stub(channel.get());bite_im::GetMultiFileReq req;bite_im::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 iter fmap.begin(); iter ! fmap.end(); iter){file_data_lists.insert(std::make_pair(iter-first, iter-second.file_content()));}return true;}}; }1.5.3 指定时间段消息搜索接口实现 从请求中获取会话 ID 要获取的消息的起始时间与结束时间。访问数据库从数据库中按时间进行范围查询获取消息简略信息消息 ID会话 ID消息类型产生时间发送者用户 ID文本消息内容文件消息元信息。循环构造完整消息从用户子服务获取消息的发送者用户信息从文件子服务获取文件/语音/图片数据。组织响应返回给网关服务器。 namespace MyTest {class MessageServiceImpl : public bite_im::MsgStorageService{public:virtual void GetHistoryMsg(::google::protobuf::RpcController* controller,const ::bite_im::GetHistoryMsgReq* request,::bite_im::GetHistoryMsgRsp* response,::google::protobuf::Closure* done){brpc::ClosureGuard rpc_guard(done);auto err_response this, response - 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_lsits.empty()){response-set_request_id(rid);response-set_success(true);return;}//3. 统计所有文件类型消息的文件ID并从文件子服务进行批量文件下载std::unordered_setstd::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_mapstd::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_setstd::string user_id_lists; // {猪爸爸吧 祝妈妈猪爸爸吧祝爸爸}for(const auto msg : msg_lsits){user_id_lists.insert(msg.user_id());}std::unordered_mapstd::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(消息类型错误);break;}}}}; }1.5.4 关键字消息搜索接口实现 从请求中获取会话 ID 搜索关键字。基于封装的 ES 客户端访问 ES 服务器进行文本消息搜索以消息内容进行搜索以会话 ID 进行过滤从 ES 服务器获取到消息简息消息 ID会话 ID 文本消息内容。循环从数据库根据消息 ID 获取消息简息消息 ID消息类型会话 ID发送者ID产生时间文本消息内容文件消息元数据。循环从用户子服务获取所有消息的发送者用户信息构造完整消息。组织响应返回给网关服务器。 namespace MyTest {class MessageServiceImpl : public bite_im::MsgStorageService{public:virtual void MsgSearch(::google::protobuf::RpcController* controller,const ::bite_im::MsgSearchReq* request,::bite_im::MsgSearchRsp* response,::google::protobuf::Closure* done) {brpc::ClosureGuard rpc_guard(done);auto err_response this, response - 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_setstd::string user_id_lists;for(const auto msg : msg_lists) {user_id_lists.insert(msg.user_id());}std::unordered_mapstd::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());}}private:ESMessage::ptr _es_message;MessageTable::ptr _mysql_message;//这边是rpc调用客户端相关对象std::string _user_service_name;std::string _file_service_name;ServiceManager::ptr _mm_channels;}; }1.5.5 搭建Rpc服务和创建消息存储子服务的工厂类 1创建MessageServer类来搭建RPC服务器 namespace MyTest {class MessageServer{public:using ptr std::shared_ptrMessageServer;MessageServer(const MQClient::ptr mq_client,const Discovery::ptr service_discoverer,const Registry::ptr reg_client,const std::shared_ptrelasticlient::Client es_client,const std::shared_ptrodb::core::database mysql_client,const std::shared_ptrbrpc::Server server) :_mq_client(mq_client),_service_discoverer(service_discoverer),_registry_client(reg_client),_es_client(es_client),_mysql_client(mysql_client),_rpc_server(server) {}// 搭建RPC服务器并启动服务器void start(){_rpc_server-RunUntilAskedToQuit();}~MessageServer() {}private:Discovery::ptr _service_discoverer;Registry::ptr _registry_client;MQClient::ptr _mq_client;std::shared_ptrelasticlient::Client _es_client;std::shared_ptrodb::core::database _mysql_client;std::shared_ptrbrpc::Server _rpc_server;}; }2创建工厂类MessageServerBuilder来实现用户子服务的创建以及Rpc服务器的创建 namespace MyTest {class MessageServerBuilder{public://构造es客户端对象void make_es_object(const std::vectorstd::string host_list) {_es_client ESClientFactory::create(host_list);}//构造mysql客户端对象void make_mysql_object(const std::string user,const std::string pswd,const std::string host,const std::string db,const std::string cset,int port,int conn_pool_count){_mysql_client ODBFactory::create(user, pswd, host, db, cset, port, conn_pool_count);}//用于构造服务发现客户端信道管理对象void make_discovery_object(const std::string reg_host,const std::string base_service_name,const std::string file_service_name,const std::string user_service_name) {_user_service_name user_service_name;_file_service_name file_service_name;_mm_channels std::make_sharedServiceManager();_mm_channels-declared(file_service_name);_mm_channels-declared(user_service_name);LOG_DEBUG(设置文件子服务为需添加管理的子服务{}, file_service_name);auto put_cb std::bind(ServiceManager::onServiceOnline, _mm_channels.get(), std::placeholders::_1, std::placeholders::_2);auto del_cb std::bind(ServiceManager::onServiceOffline, _mm_channels.get(), std::placeholders::_1, std::placeholders::_2);_service_discoverer std::make_sharedDiscovery(reg_host, base_service_name, put_cb, del_cb);}//用于构造服务注册客户端对象void make_registry_object(const std::string reg_host,const std::string service_name,const std::string access_host) {_registry_client std::make_sharedRegistry(reg_host);_registry_client-registry(service_name, access_host);}//用于构造消息队列客户端对象void make_mq_object(const std::string user, const std::string passwd,const std::string host,const std::string exchange_name,const std::string queue_name,const std::string binding_key) {_exchange_name exchange_name;_queue_name queue_name;_mq_client std::make_sharedMQClient(user, passwd, host);_mq_client-declareComponents(exchange_name, queue_name, binding_key);}void make_rpc_server(uint16_t port, int32_t timeout, uint8_t num_threads){if(!_es_client){LOG_ERROR(还未初始化ES搜索引擎模块);abort();}if(!_mysql_client){LOG_ERROR(还未初始化Mysql数据库模块);abort();}if (!_mm_channels){LOG_ERROR(还未初始化信道管理模块);abort();}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);}//构造RPC服务器对象UserServer::ptr build(){if(!_service_discoverer) {LOG_ERROR(还未初始化服务发现模块);abort();}if(!_registry_client) {LOG_ERROR(还未初始化服务注册模块);abort();}if(!_rpc_server) {LOG_ERROR(还未初始化RPC服务器模块);abort();}MessageServer::ptr server std::make_sharedMessageServer(_mq_client, _service_discoverer, _registry_client,_es_client, _mysql_client, _rpc_server);return server;}private:Registry::ptr _registry_client;std::shared_ptrelasticlient::Client _es_client;std::shared_ptrodb::core::database _mysql_client;std::string _user_service_name;std::string _file_service_name;ServiceManager::ptr _mm_channels;Discovery::ptr _service_discoverer;std::string _exchange_name;std::string _queue_name;MQClient::ptr _mq_client;std::shared_ptrbrpc::Server _rpc_server;}; }3在构建Rpc服务的时候需要消息队列进行订阅所以在MessageServiceImpl类当中设置其回调函数 namespace MyTest {class MessageServiceImpl : public bite_im::MsgStorageService{public:void onMessage(const char *body, size_t sz){LOG_DEBUG(收到新消息进行存储处理);//1. 取出序列化的消息内容进行反序列化bite_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. 如果是一个图片/语音/文件消息则取出数据存储到文件子服务中并获取文件IDcase 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(消息类型错误);break;}//3. 提取消息的元信息存储到mysql数据库中bite_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;}}}; }4实现消息存储子服务的服务器的搭建 #include message_server.hpp //主要实现语音识别子服务的服务器的搭建DEFINE_bool(run_mode, false, 程序的运行模式false-调试 true-发布); DEFINE_string(log_file, , 发布模式下用于指定日志的输出文件); DEFINE_int32(log_level, 0, 发布模式下用于指定日志输出等级);DEFINE_string(registry_host, http://127.0.0.1:2379, 服务注册中心地址); DEFINE_string(instance_name, /message_service/instance, 当前实例名称); DEFINE_string(access_host, 127.0.0.1:10005, 当前实例的外部访问地址);DEFINE_int32(listen_port, 10005, Rpc服务器监听端口); DEFINE_int32(rpc_timeout, -1, Rpc调用超时时间); DEFINE_int32(rpc_threads, 1, Rpc的IO线程数量);DEFINE_string(base_service, /service, 服务监控根目录); DEFINE_string(file_service, /service/file_service, 文件管理子服务名称); DEFINE_string(user_service, /service/user_service, 用户管理子服务名称);DEFINE_string(es_host, http://127.0.0.1:9200/, ES搜索引擎服务器URL);DEFINE_string(mysql_host, 127.0.0.1, Mysql服务器访问地址); DEFINE_string(mysql_user, root, Mysql服务器访问用户名); DEFINE_string(mysql_pswd, 123456, Mysql服务器访问密码); DEFINE_string(mysql_db, bite_im, Mysql默认库名称); DEFINE_string(mysql_cset, utf8, Mysql客户端字符集); DEFINE_int32(mysql_port, 0, Mysql服务器访问端口); DEFINE_int32(mysql_pool_count, 4, Mysql连接池最大连接数量);DEFINE_string(mq_user, root, 消息队列服务器访问用户名); DEFINE_string(mq_pswd, 123456, 消息队列服务器访问密码); DEFINE_string(mq_host, 127.0.0.1:5672, 消息队列服务器访问地址); DEFINE_string(mq_msg_exchange, msg_exchange, 持久化消息的发布交换机名称); DEFINE_string(mq_msg_queue, msg_queue, 持久化消息的发布队列名称); DEFINE_string(mq_msg_binding_key, msg_queue, 持久化消息的发布队列名称);int main(int argc, char *argv[]) {google::ParseCommandLineFlags(argc, argv, true);init_logger(FLAGS_run_mode, FLAGS_log_file, FLAGS_log_level);MyTest::MessageServerBuilder msb;msb.make_mq_object(FLAGS_mq_user, FLAGS_mq_pswd, FLAGS_mq_host,FLAGS_mq_msg_exchange, FLAGS_mq_msg_queue, FLAGS_mq_msg_binding_key);msb.make_es_object({FLAGS_es_host});msb.make_mysql_object(FLAGS_mysql_user, FLAGS_mysql_pswd, FLAGS_mysql_host, FLAGS_mysql_db, FLAGS_mysql_cset, FLAGS_mysql_port, FLAGS_mysql_pool_count);msb.make_discovery_object(FLAGS_registry_host, FLAGS_base_service, FLAGS_file_service, FLAGS_user_service);msb.make_rpc_server(FLAGS_listen_port, FLAGS_rpc_timeout, FLAGS_rpc_threads);msb.make_registry_object(FLAGS_registry_host, FLAGS_base_service FLAGS_instance_name, FLAGS_access_host);auto server msb.build();server-start();return 0; }5cmake构建代码

1. 添加cmake版本说明

cmake_minimum_required(VERSION 3.1.3)

2. 声明工程名称

project(message_server)set(target message_server)set(CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS} -g)# 3. 检测并生成ODB框架代码

1. 添加所需的proto映射代码文件名称

set(proto_path ${CMAKE_CURRENT_SOURCE_DIR}/../proto) set(proto_files base.proto user.proto file.proto message.proto)

2. 检测框架代码文件是否已经生成

set(proto_hxx ) set(proto_cxx ) set(proto_srcs ) foreach(proto_file ${proto_files})

3. 如果没有生成则预定义生成指令 – 用于在构建项目之间先生成框架代码string(REPLACE .proto .pb.cc proto_cc \({proto_file})string(REPLACE .proto .pb.h proto_hh \){proto_file})if (NOT EXISTS \({CMAKE_CURRENT_BINARY_DIR}\){proto_cc})add_custom_command(PRE_BUILDCOMMAND protocARGS –cpp_out\({CMAKE_CURRENT_BINARY_DIR} -I \){proto_path} –experimental_allow_proto3_optional \({proto_path}/\){proto_file}DEPENDS \({proto_path}/\){proto_file}OUTPUT \({CMAKE_CURRENT_BINARY_DIR}/\){proto_cc}COMMENT 生成Protobuf框架代码文件: \({CMAKE_CURRENT_BINARY_DIR}/\){proto_cc})endif()list(APPEND proto_srcs \({CMAKE_CURRENT_BINARY_DIR}/\){proto_cc})

endforeach()# 3. 检测并生成ODB框架代码

1. 添加所需的odb映射代码文件名称

set(odb_path ${CMAKE_CURRENT_SOURCE_DIR}/../odb) set(odb_files message.hxx)

2. 检测框架代码文件是否已经生成

set(odb_hxx ) set(odb_cxx ) set(odb_srcs ) foreach(odb_file ${odb_files})

3. 如果没有生成则预定义生成指令 – 用于在构建项目之间先生成框架代码string(REPLACE .hxx -odb.hxx odb_hxx \({odb_file})string(REPLACE .hxx -odb.cxx odb_cxx \){odb_file})if (NOT EXISTS \({CMAKE_CURRENT_BINARY_DIR}\){odb_cxx})add_custom_command(PRE_BUILDCOMMAND odbARGS -d mysql –std c11 –generate-query –generate-schema –profile boost/date-time \({odb_path}/\){odb_file}DEPENDS \({odb_path}/\){odb_file}OUTPUT \({CMAKE_CURRENT_BINARY_DIR}/\){odb_cxx}COMMENT 生成ODB框架代码文件: \({CMAKE_CURRENT_BINARY_DIR}/\){odb_cxx})endif()

4. 将所有生成的框架源码文件名称保存起来 student-odb.cxx classes-odb.cxxlist(APPEND odb_srcs \({CMAKE_CURRENT_BINARY_DIR}/\){odb_cxx})

endforeach()# 4. 获取源码目录下的所有源码文件 set(src_files ) aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR}/source src_files)

5. 声明目标及依赖

add_executable(\({target} \){src_files} \({proto_srcs} \){odb_srcs})

7. 设置需要连接的库

target_link_libraries(\({target} -lgflags -lspdlog -lfmt -lbrpc -lssl -lcrypto -lprotobuf -lleveldb -letcd-cpp-api -lcpprest -lcurl -lodb-mysql -lodb -lodb-boost/usr/lib/x86_64-linux-gnu/libjsoncpp.so.19-lcpr -lelasticlient-lamqpcpp -lev)set(test_client message_client) set(test_files ) aux_source_directory(\){CMAKE_CURRENT_SOURCE_DIR}/test test_files) add_executable(\({test_client} \){test_files} \({proto_srcs}) target_link_libraries(\){test_client} -pthread -lgtest -lgflags -lspdlog -lfmt -lbrpc -lssl -lcrypto -lprotobuf -lleveldb -letcd-cpp-api -lcpprest -lcurl /usr/lib/x86_64-linux-gnu/libjsoncpp.so.19)# 6. 设置头文件默认搜索路径 include_directories(\({CMAKE_CURRENT_BINARY_DIR}) include_directories(\){CMAKE_CURRENT_SOURCE_DIR}/../common) include_directories(\({CMAKE_CURRENT_SOURCE_DIR}/../odb) include_directories(\){CMAKE_CURRENT_SOURCE_DIR}/../third/include)#8. 设置安装路径 INSTALL(TARGETS \({target} \){test_client} RUNTIME DESTINATION bin)2. 好友管理子服务的实现 2.1 功能设计 1好友管理子服务主要用于管理好友相关的数据与操作因此主要负责以下接口 好友列表的获取当用户登录成功之后获取自己好友列表进行展示。申请好友搜索用户之后点击申请好友向对方发送好友申请。待处理申请的获取当用户登录成功之后会获取离线的好友申请请求以待处理。好友申请的处理针对收到的好友申请进行同意/拒绝的处理。删除好友删除当前好友列表中的好友。用户搜索可以进行用户的搜索用于申请好友。聊天会话列表的获取每个单人/多人聊天都有一个聊天会话在登录成功后可以获取聊天会话查看历史的消息以及对方的各项信息。多人聊天会话的创建单人聊天会话在对方同意好友时创建而多人会话需要调用该接口进行手动创建。聊天成员列表的获取多人聊天会话中可以点击查看群成员按钮查看群成员信息。 2.2 模块划分 2以下是该服务的模块划分 参数/配置文件解析模块基于 gflags 框架直接使用进行参数/配置文件解析。日志模块基于 spdlog 框架封装的模块直接使用进行日志输出。服务注册模块基于 etcd 框架封装的注册模块直接使用进行聊天消息存储子服务的注册。数据库数据操作模块基于 odb-mysql 数据管理封装的模块实现数据库中数据的操作 申请好友的时候根据数据库中的数据判断两人是否已经是好友关系。申请好友的时候根据数据库中的数据判断是否已经申请过好友。申请好友的时候针对两位用户 ID 建立好友申请事件信息。好友信息处理的时候找到申请事件进行删除。获取待处理好友申请事件的时候从数据库根据用户 ID 查询出所有的申请信息。同意好友申请的时候需要创建单聊会话向数据库中插入会话信息。从数据库根据指定用户 ID 获取所有好友 ID。创建群聊的时候需要创建群聊会话向数据库中插入会话信息。查看群聊成员的时候从数据库根据会话 ID 获取所有会话成员 ID。获取会话列表的时候从数据库根据用户 ID 获取到所有会话信息。删除好友的时候从数据库中删除两人的好友关系以及单聊会话以及会 话成员信息。 rpc 服务模块基于 brpc 框架搭建 rpc 服务器。rpc 服务发现与调用模块基于 etcd 框架与 brpc 框架封装的服务发现与调用模块 连接用户管理子服务获取好友列表会话成员好友申请事件的时候获取用户信息。连接消息管理子服务在打开聊天会话的时候需要获取最近的一条消息进行展示。 ES 客户端模块基于 elasticsearch 框架实现访问客户端从 es 服务器进行用户的关键字搜索用户信息由用户子服务在用户注册的时候添加进去。 2.3 功能模块示意图 1如下是模块功能图
2.4. 数据库数据管理 根据好友相关操作分析好友操作相关所需要有以下数据表 2.4.1 用户关系表的实现 1用户信息表和关系表 用户信息表由用户操作服务进行创建并在用户注册时添加数据好友这里只进行查询通过用户 ID 获取详细用户信息。 因为本身用户服务器已经管理了用户个人信息因此没必要再整一份用户信息出来也因为当前用户之间只有好友关系目前未实现黑名单陌生人…因此这里是一个好友关系表表示谁和谁是好友 包含的字段 ID作为主键用户 ID好友 ID 需要注意的是两个用户结为好友时需要添加 (1,2),(2,1) 两条数据 提供的操作 新增用户关系新增好友通常伴随着新增会话新增会话伴随着新增会话成员移除用户关系移除好友通常伴随着移除会话移除会话伴随着移除会话成员判断两人是否是好友关系以用户 ID 获取用户的所有好友 ID与用户表连接以用户 ID 获取所有好友详细信息
2ODB映射数据结构relation.hxx的实现 #pragma once #include string #include cstddef #include odb/nullable.hxx #include odb/core.hxxnamespace MyTest {#pragma db object table(relation)class Relation {public:Relation(){}Relation(const std::string uid, const std::string pid):_user_id(uid),_peer_id(pid){}std::string user_id() const { return _user_id; }void user_id(std::string uid) { _user_id uid; }std::string peer_id() const { return _peer_id; }void peer_id(std::string uid) { _peer_id uid; }~Relation(){}private:friend class odb::access;#pragma db id autounsigned long _id;#pragma db type(varchar(64)) indexstd::string _user_id;#pragma db type(varchar(64))std::string _peer_id;}; }3运行如下命令可以通过odb生成mysql代码 odb -d mysql –std c11 –generate-query –generate-schema –profile boost/date-time relation.hxx # 最后所要填写的取决与文件所在的路径4生成的relation.sql代码 /* This file was generated by ODB, object-relational mapping (ORM)* compiler for C./CREATE DATABASE IF NOT EXISTS bite_im; USE bite_im; DROP TABLE IF EXISTS relation;CREATE TABLE relation (id BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,user_id varchar(64) NOT NULL,peer_id varchar(64) NOT NULL)ENGINEInnoDB;CREATE INDEX user_id_iON relation (user_id);2.4.2 会话信息表的实现 1在多人聊天中舍弃了群的概念添加了聊天会话的概念因为会话既可以是两人单聊会话也可以是多人聊天会话这样就可以统一管理了。 包含字段 ID作为主键会话 ID会话标识会话名称 单聊会话则设置为’单聊会话’或直接为空就行因为单聊会话名称就是对方名称头像就是对方头像会话类型 SINGLE-单聊 / GROUP-多人单聊由服务器在同意好友时创建多人由用户申请创建 提供的操作 新增会话向会话成员表中新增会话成员信息和向会话表中新增会话信息。删除会话删除会话成员表中的所有会话成员信息和删除会话表中的会话信息。通过会话 ID获取会话的详细信息。通过用户 ID 获取所有的好友单聊会话连接会话成员表和用户表所需字段 会话 ID。会话名称好友的昵称。会话类型单聊类型。会话头像 ID好友的头像 ID。好友 ID。 通过用户 ID 获取所有自己的群聊会话连接会话成员表和用户表所需字段 会话 ID。会话名称。会话类型群聊类型。
2ODB映射数据结构chat_session.hxx的实现 #pragma once #include string #include cstddef #include odb/nullable.hxx #include odb/core.hxx #include chat_session_member.hxxnamespace MyTest {enum class ChatSessionType {SINGLE 1,GROUP 2};#pragma db object table(chat_session)class ChatSession {public:ChatSession(){}ChatSession(const std::string ssid, const std::string ssname, const ChatSessionType sstype):_chat_session_id(ssid),_chat_session_name(ssname),_chat_session_type(sstype){}std::string chat_session_id() const { return _chat_session_id; }void chat_session_id(std::string ssid) { _chat_session_id ssid; }std::string chat_session_name() const { return _chat_session_name; }void chat_session_name(std::string ssname) { _chat_session_name ssname; }ChatSessionType chat_session_type() const { return _chat_session_type; }void chat_session_type(ChatSessionType val) { _chat_session_type val; }private:friend class odb::access;#pragma db id autounsigned long _id;#pragma db type(varchar(64)) index uniquestd::string _chat_session_id;#pragma db type(varchar(64))std::string _chat_session_name;#pragma db type(tinyint)ChatSessionType _chat_session_type; //1-单聊 2-群聊};// 这里条件必须是指定条件 css::chat_session_type1 csm1.user_iduid csm2.user_id ! csm1.user_id#pragma db view object(ChatSession css) \object(ChatSessionMember csm1 : css::_chat_session_id csm1::_session_id) \object(ChatSessionMember csm2 : css::_chat_session_id csm2::_session_id) \query((?))struct SingleChatSession{#pragma db column(css::_chat_session_id)std::string chat_session_id;#pragma db column(csm2::_user_id)std::string friend_id;};// 这里条件必须是指定条件 css::chat_session_type2 csm.user_iduid#pragma db view object(ChatSession css) \object(ChatSessionMember csm : css::_chat_session_id csm::_session_id) \query((?))struct GroupChatSession{#pragma db column(css::_chat_session_id)std::string chat_session_id;#pragma db column(css::_chat_session_name)std::string chat_session_name;}; }3运行如下命令可以通过odb生成mysql代码 odb -d mysql –std c11 –generate-query –generate-schema –profile boost/date-time chat_session.hxx # 最后所要填写的取决与文件所在的路径4生成的chat_session.sql代码 /
This file was generated by ODB, object-relational mapping (ORM)* compiler for C./CREATE DATABASE IF NOT EXISTS bite_im; USE bite_im; DROP TABLE IF EXISTS chat_session;CREATE TABLE chat_session (id BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,chat_session_id varchar(64) NOT NULL,chat_session_name varchar(64) NOT NULL,chat_session_type tinyint NOT NULL)ENGINEInnoDB;CREATE UNIQUE INDEX chat_session_id_iON chat_session (chat_session_id);2.4.3 会话成员表的实现 1每个会话中都会有两个及以上的成员只有两个成员的会话是单聊会话超过两个是多人聊天会话为了明确哪个用户属于哪个会话或者说会话中有哪些成员因此需要有会话成员的数据管理 包含字段 ID作为主键。会话 ID会话标识。用户 ID用户标识。 有了这张表就可以轻松的找出哪个用户属于哪个会话了也可以根据会话 ID 获取所有成员 ID。提供的操作向指定会话中添加单个成员。向指定会话中添加多个成员。从指定会话中删除单个成员。通过会话 ID获取会话的所有成员 ID。删除会话所有成员在删除会话的时候使用。 2ODB映射数据结构chat_session_member.hxx的实现 #pragma once #include string #include cstddef #include odb/core.hxx// 聊天会话成员表映射对象 namespace MyTest {#pragma db object table(chat_session_member)class ChatSessionMember{public:ChatSessionMember() {}ChatSessionMember(const std::string ssid, const std::string uid) :_session_id(ssid),_user_id(uid) {}~ChatSessionMember() {}std::string session_id() const { return _session_id; }void session_id(std::string ssid) { _session_id ssid; }std::string user_id() const { return _user_id; }void user_id(std::string uid) { _user_id uid; }private:friend class odb::access;#pragma db id autounsigned long _id;#pragma db type(varchar(64)) indexstd::string _session_id;#pragma db type(varchar(64))std::string _user_id;}; }3运行如下命令可以通过odb生成mysql代码 odb -d mysql –std c11 –generate-query –generate-schema –profile boost/date-time chat_session_member.hxx # 最后所要填写的取决与文件所在的路径4生成的chat_session_member.sql代码 / This file was generated by ODB, object-relational mapping (ORM)* compiler for C./ CREATE DATABASE IF NOT EXISTS bite_im; USE bite_im;DROP TABLE IF EXISTS chat_session_member;CREATE TABLE chat_session_member (id BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,session_id varchar(64) NOT NULL,user_id varchar(64) NOT NULL)ENGINEInnoDB;CREATE INDEX session_id_iON chat_session_member (session_id);2.4.4 好友申请事件表的实现 1在好友的操作中有个操作需要额外的管理那就是申请好友的事件因为用户 A 申请用户 B 为好友并非一次性完成需要用户 B 对本次申请进行处理同意后才算是一次完整的流程。而在两次操作之间我们就需要为两次操作建立起相匹配的关系映射。 包含字段 ID作为主键。事件 ID。请求者用户 ID。响应者用户 ID。状态用于表示本次请求的处理阶段其包含三种状态待处理-todo同意-accept拒绝-reject。 提供的操作新增好友申请事件申请的时候新增。删除好友申请事件处理完毕同意/拒绝的时候删除。获取指定用户的所有待处理事件及关联申请者用户信息连接用户表。 2ODB映射数据结构friend_apply.hxx的实现 #pragma once #include string #include cstddef #include odb/core.hxxnamespace MyTest {#pragma db object table(friend_apply)class FriendApply{public:FriendApply(){}FriendApply(const std::string eid, const std::string uid, const std::string pid):_user_id(uid),_peer_id(pid),_event_id(eid){}std::string event_id() const { return _event_id; }void event_id(std::string eid) { _event_id eid; }std::string user_id() const { return _user_id; }void user_id(std::string uid) { _user_id uid; }std::string peer_id() const { return _peer_id; }void peer_id(std::string uid) { _peer_id uid; }private:friend class odb::access;#pragma db id autounsigned long _id;#pragma db type(varchar(64)) index uniquestd::string _event_id;#pragma db type(varchar(64)) index std::string _user_id;#pragma db type(varchar(64)) index std::string _peer_id;}; }3运行如下命令可以通过odb生成mysql代码 odb -d mysql –std c11 –generate-query –generate-schema –profile boost/date-time chat_session_member.hxx # 最后所要填写的取决与文件所在的路径4生成的friend_apply.sql代码 / This file was generated by ODB, object-relational mapping (ORM)* compiler for C.*/CREATE DATABASE IF NOT EXISTS bite_im; USE bite_im; DROP TABLE IF EXISTS friend_apply;CREATE TABLE friend_apply (id BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,event_id varchar(64) NOT NULL,user_id varchar(64) NOT NULL,peer_id varchar(64) NOT NULL)ENGINEInnoDB;CREATE UNIQUE INDEX event_id_iON friend_apply (event_id);CREATE INDEX user_id_iON friend_apply (user_id);CREATE INDEX peer_id_iON friend_apply (peer_id);2.5 ES用户信息管理 1对ES的用户消息封装在前面的用户管理子服务模块当中已经实现了具体封装实现见博客https://blog.csdn.net/m0_65558082/article/details/144042168?spm1001.2014.3001.5502#341__832当中的3.4.3小节文档数据库的数据管理。 2.6 接口的实现 2.6.1 消息存储子服务所用到的protobuf接口实现 syntax proto3; package bite_im; import base.proto;option cc_generic_services true;//————————————– //好友列表获取 message GetFriendListReq {string request_id 1; // 请求标识IDoptional string user_id 2; // 当前请求的发起者用户IDoptional string session_id 3; //登录会话ID–用于网关进行身份识别–其他子服务用不到 } message GetFriendListRsp {string request_id 1;bool success 2;string errmsg 3; repeated UserInfo friend_list 4; //要返回的用户信息 }//————————————– //好友删除 message FriendRemoveReq {string request_id 1;optional string user_id 2; //当前用户IDoptional string session_id 3;string peer_id 4; //要删除的好友ID } message FriendRemoveRsp {string request_id 1;bool success 2;string errmsg 3; } //————————————– //添加好友–发送好友申请 message FriendAddReq {string request_id 1;optional string session_id 2;optional string user_id 3;//申请人idstring respondent_id 4;//被申请人id } message FriendAddRsp {string request_id 1;bool success 2;string errmsg 3; string notify_event_id 4;//通知事件id } //————————————– //好友申请的处理 message FriendAddProcessReq {string request_id 1;string notify_event_id 2;//通知事件idbool agree 3;//是否同意好友申请string apply_user_id 4; //申请人的用户idoptional string session_id 5;optional string user_id 6; // 被申请人 } //
message FriendAddProcessRsp {string request_id 1;bool success 2;string errmsg 3; // 同意后会创建会话向网关返回会话信息用于通知双方会话的建立这个字段客户端不需要关注optional string new_session_id 4; } //————————————– //获取待处理的申请自己好友的信息列表 message GetPendingFriendEventListReq {string request_id 1;optional string session_id 2;optional string user_id 3; }message FriendEvent {optional string event_id 1;UserInfo sender 3; } message GetPendingFriendEventListRsp {string request_id 1;bool success 2;string errmsg 3; repeated FriendEvent event 4; }//————————————– //好友搜索 message FriendSearchReq {string request_id 1;string search_key 2;//就是名称模糊匹配关键字optional string session_id 3;optional string user_id 4; } message FriendSearchRsp {string request_id 1;bool success 2;string errmsg 3; repeated UserInfo user_info 4; }//————————————– //会话列表获取 message GetChatSessionListReq {string request_id 1;optional string session_id 2;optional string user_id 3; } message GetChatSessionListRsp {string request_id 1;bool success 2;string errmsg 3; repeated ChatSessionInfo chat_session_info_list 4; } //————————————– //创建会话 message ChatSessionCreateReq {string request_id 1;optional string session_id 2;optional string user_id 3;string chat_session_name 4;//需要注意的是这个列表中也必须包含创建者自己的用户IDrepeated string member_id_list 5; } message ChatSessionCreateRsp {string request_id 1;bool success 2;string errmsg 3; //这个字段属于后台之间的数据给前端回复的时候不需要这个字段会话信息通过通知进行发送optional ChatSessionInfo chat_session_info 4; } //————————————– //获取会话成员列表 message GetChatSessionMemberReq {string request_id 1;optional string session_id 2;optional string user_id 3;string chat_session_id 4; } message GetChatSessionMemberRsp {string request_id 1;bool success 2;string errmsg 3; repeated UserInfo member_info_list 4; }service FriendService {rpc GetFriendList(GetFriendListReq) returns (GetFriendListRsp);rpc FriendRemove(FriendRemoveReq) returns (FriendRemoveRsp);rpc FriendAdd(FriendAddReq) returns (FriendAddRsp);rpc FriendAddProcess(FriendAddProcessReq) returns (FriendAddProcessRsp);rpc FriendSearch(FriendSearchReq) returns (FriendSearchRsp);rpc GetChatSessionList(GetChatSessionListReq) returns (GetChatSessionListRsp);rpc ChatSessionCreate(ChatSessionCreateReq) returns (ChatSessionCreateRsp);rpc GetChatSessionMember(GetChatSessionMemberReq) returns (GetChatSessionMemberRsp);rpc GetPendingFriendEventList(GetPendingFriendEventListReq) returns (GetPendingFriendEventListRsp); }2.6.2 获取好友列表和申请添加好友接口实现 1获取好友列表 获取请求中的用户 ID。根据用户 ID从数据库的好友关系表用户表中取出该用户所有的好友简息。根据好友简息中的好友头像 ID批量获取头像数据组织完整用户信息结构。组织响应将好友列表返回给网关。 #pragma once #include brpc/server.h #include butil/logging.h#include data_es.hpp // es数据管理客户端封装 #include mysql_chat_session_member.hpp // mysql数据管理客户端封装 #include mysql_chat_session.hpp // mysql数据管理客户端封装 #include mysql_relation.hpp // mysql数据管理客户端封装 #include mysql_apply.hpp // mysql数据管理客户端封装 #include etcd.hpp // 服务注册模块封装 #include logger.hpp // 日志模块封装 #include utils.hpp // 基础工具接口 #include channel.hpp // 信道管理模块封装#include friend.pb.h // protobuf框架代码 #include base.pb.h // protobuf框架代码 #include user.pb.h // protobuf框架代码 #include message.pb.h // protobuf框架代码//实现好友管理子服务namespace MyTest {class FriendServiceImpl : public bite_im::FriendService {public:FriendServiceImpl(const std::shared_ptrelasticlient::Client es_client,const std::shared_ptrodb::core::database mysql_client,const ServiceManager::ptr channel_manager,const std::string user_service_name,const std::string message_service_name):_es_user(std::make_sharedESUser(es_client)),_mysql_apply(std::make_sharedFriendApplyTable(mysql_client)),_mysql_chat_session(std::make_sharedChatSessionTable(mysql_client)),_mysql_chat_session_member(std::make_sharedChatSessionMemeberTable(mysql_client)),_mysql_relation(std::make_sharedRelationTable(mysql_client)),_user_service_name(user_service_name),_message_service_name(message_service_name),_mm_channels(channel_manager){}virtual void GetFriendList(::google::protobuf::RpcController *controller,const ::bite_im::GetFriendListReq *request,::bite_im::GetFriendListRsp *response,::google::protobuf::Closure *done){brpc::ClosureGuard rpc_guard(done);// 1. 定义错误回调auto err_response this, response - void{response-set_request_id(rid);response-set_success(false);response-set_errmsg(errmsg);return;};//1. 提取请求中的关键要素用户IDstd::string rid request-request_id();std::string uid request-user_id();//2. 从数据库中查询获取用户的好友IDauto friend_id_lists _mysql_relation-friends(uid);std::unordered_setstd::string user_id_lists;for(auto id : friend_id_lists) {user_id_lists.insert(id);}//3. 从用户子服务批量获取用户信息std::unordered_mapstd::string, bite_im::UserInfo user_list;bool ret GetUserInfo(rid, user_id_lists, user_list);if(ret false){LOG_ERROR({} - 批量获取用户信息失败!, rid);return err_response(rid, 批量获取用户信息失败!);}//4. 组织响应response-set_request_id(rid);response-set_success(true);for(const auto user_it : user_list) {auto user_info response-add_friend_list();user_info-CopyFrom(user_it.second);}}private:bool GetUserInfo(const std::string rid, const std::unordered_setstd::string uid_list,std::unordered_mapstd::string, UserInfo user_list){auto channel _mm_channels-choose(_user_service_name);if(!channel) {LOG_ERROR({} - 获取用户子服务信道失败, rid);}bite_im::GetMultiUserInfoReq req;bite_im::GetMultiUserInfoRsp rsp;req.set_request_id(rid);for(auto id : uid_list) {req.add_users_id(id);}brpc::Controller cntl;bite_im::UserService_Stub stub(channel.get());stub.GetMultiUserInfo(cntl, req, rsp, nullptr);if(cntl.Failed() true) {LOG_ERROR({} - 用户子服务调用失败: {}, rid, cntl.ErrorText());return false;}if( rsp.success() false) {LOG_ERROR({} - 批量获取用户信息失败: {}, rid, rsp.errmsg());return false;}for(const auto user_it : rsp.users_info()) {user_list.insert(std::make_pair(user_it.first, user_it.second));}return true;}}; }2申请添加好友 取出请求中的请求者 ID和被请求者 ID。判断两人是否已经是好友。判断该用户是否已经申请过好友关系。向好友申请事件表中新增申请信息。组织响应将事件 ID 信息响应给网关。 namespace MyTest {class FriendServiceImpl : public bite_im::FriendService {public:virtual void FriendAdd(::google::protobuf::RpcController *controller,const ::bite_im::FriendAddReq *request,::bite_im::FriendAddRsp *response,::google::protobuf::Closure *done){brpc::ClosureGuard rpc_guard(done);// 1. 定义错误回调auto err_response this, response - void{response-set_request_id(rid);response-set_success(false);response-set_errmsg(errmsg);return;};//1. 提取请求中的关键要素申请人用户ID 被申请人用户IDstd::string rid request-request_id();std::string uid request-user_id();std::string pid request-respondent_id();//2. 判断两人是否已经是好友bool ret _mysql_relation-exists(uid, pid);if(ret true) {LOG_ERROR({}- 申请好友失败-两者{}-{}已经是好友关系, rid, uid, pid);return err_response(rid, 两者已经是好友关系);}//3. 当前是否已经申请过好友ret _mysql_apply-exists(uid, pid);if(ret false){LOG_ERROR({}- 申请好友失败-已经申请过对方好友, rid, uid, pid);return err_response(rid, 已经申请过对方好友);}//4. 向好友申请表中新增申请信息std::string eid uuid();FriendApply ev(eid, uid, pid);ret _mysql_apply-insert(ev);if(ret false){LOG_ERROR({} - 向数据库新增好友申请事件失败, rid);return err_response(rid, 向数据库新增好友申请事件失败);}//5. 组织响应response-set_request_id(rid);response-set_success(true);response-set_notify_event_id(eid);}}; }2.6.3 获取待处理好友申请事件和处理好友申请接口实现 1获取待处理好友申请事件 取出请求中的用户 ID。根据用户 ID从申请事件表用户表中找到该用户所有状态为 PENDING 的待处理事件关联申请人用户简息。根据申请人用户头像 ID从文件存储子服务器获取所有用户头像信息组织用户信息结构。组织响应将申请事件列表响应给网关。 namespace MyTest {class FriendServiceImpl : public bite_im::FriendService {public:virtual void GetPendingFriendEventList(::google::protobuf::RpcController *controller,const ::bite_im::GetPendingFriendEventListReq *request,::bite_im::GetPendingFriendEventListRsp *response,::google::protobuf::Closure *done){brpc::ClosureGuard rpc_guard(done);// 1. 定义错误回调auto err_response this, response - void{response-set_request_id(rid);response-set_success(false);response-set_errmsg(errmsg);return;};//1. 提取关键要素当前用户IDstd::string rid request-request_id();std::string uid request-user_id();//2. 从数据库获取待处理的申请事件信息 — 申请人用户ID列表auto res _mysql_apply-applyUsers(uid);std::unordered_setstd::string user_id_lists;for(auto id : res) {user_id_lists.insert(id);}//3. 批量获取申请人用户信息、std::unordered_mapstd::string, UserInfo user_list;bool ret GetUserInfo(rid, user_id_lists, user_list);if(ret false) {LOG_ERROR({} - 批量获取用户信息失败!, rid);return err_response(rid, 批量获取用户信息失败!);}//4. 组织响应response-set_request_id(rid);response-set_success(true);for(const auto user_it : user_list) {auto ev response-add_event();ev-mutable_sender()-CopyFrom(user_it.second);}}}; }2处理好友申请 取出请求中的申请人 ID和被申请人 ID以及处理结果根据两人 ID 在申请事件表中查询判断是否存在申请事件判断两人是否已经是好友互相加好友的情况不管拒绝还是同意删除申请事件表中的事件信息该事件处理完毕若同意申请则向用户关系表中添加好友关系数据向会话表中新增会话信息向会话成员表中新增成员信息组织响应将新生成的会话 ID 响应给网关。 namespace MyTest {class FriendServiceImpl : public bite_im::FriendService {public:virtual void FriendAddProcess(::google::protobuf::RpcController *controller,const ::bite_im::FriendAddProcessReq *request,::bite_im::FriendAddProcessRsp *response,::google::protobuf::Closure *done){brpc::ClosureGuard rpc_guard(done);// 1. 定义错误回调auto err_response this, response - void{response-set_request_id(rid);response-set_success(false);response-set_errmsg(errmsg);return;};// 1. 提取请求中的关键要素申请人用户ID被申请人用户ID处理结果事件IDstd::string rid request-request_id();std::string eid request-notify_event_id();std::string uid request-user_id(); //被申请人std::string pid request-apply_user_id();//申请人bool agree request-agree();//2. 判断有没有该申请事件bool ret _mysql_apply-exists(pid, uid);if(ret false){LOG_ERROR({}- 没有找到{}-{}对应的好友申请事件, rid, pid, uid);return err_response(rid, 没有找到对应的好友申请事件!);}//3. 如果有 可以处理 — 删除申请事件–事件已经处理完毕ret _mysql_apply-remove(pid, uid);if(ret false){LOG_ERROR({}- 从数据库删除申请事件 {}-{} 失败, rid, pid, uid);return err_response(rid, 从数据库删除申请事件失败!);}//4. 如果处理结果是同意向数据库新增好友关系信息新增单聊会话信息及会话成员std::string cssid;if(agree true){ret _mysql_relation-insert(uid, pid);if(ret false){LOG_ERROR({}- 新增好友关系信息-{}-{}, rid, uid, pid);return err_response(rid, 新增好友关系信息!);}cssid uuid();ChatSession cs(cssid, , ChatSessionType::SINGLE);ret _mysql_chat_session-insert(cs);if(ret false){LOG_ERROR({}- 新增单聊会话信息-{}, rid, cssid);return err_response(rid, 新增单聊会话信息失败!);}ChatSessionMember csm1(cssid, uid);ChatSessionMember csm2(cssid, pid);std::vectorChatSessionMember mlist {csm1, csm2};ret _mysql_chat_session_member-append(mlist);if(ret false) {LOG_ERROR({}- 没有找到{}-{}对应的好友申请事件, rid, pid, uid);return err_response(rid, 没有找到对应的好友申请事件!);}}//5. 组织响应response-set_request_id(rid);response-set_success(true);response-set_new_session_id(cssid);}}; }2.6.4 删除好友和搜索好友接口实现 1删除好友 取出请求中的删除者 ID 和被删除者 ID。从用户好友关系表中删除相关关系数据从会话表中删除单聊会话从会话成员表中删除会话成员信息。组织响应返回给网关。 namespace MyTest {class FriendServiceImpl : public bite_im::FriendService {public:virtual void FriendRemove(::google::protobuf::RpcController *controller,const ::bite_im::FriendRemoveReq *request,::bite_im::FriendRemoveRsp *response,::google::protobuf::Closure *done){brpc::ClosureGuard rpc_guard(done);// 1. 定义错误回调auto err_response this, response - void{response-set_request_id(rid);response-set_success(false);response-set_errmsg(errmsg);return;};//1. 提取关键要素当前用户ID要删除的好友IDstd::string rid request-request_id();std::string uid request-user_id();std::string pid request-peer_id();//2. 从好友关系表中删除好友关系信息bool ret _mysql_relation-remove(uid, pid);if(ret false) {LOG_ERROR({} - 从数据库删除好友信息失败, rid);return err_response(rid, 从数据库删除好友信息失败);}//3. 从会话信息表中删除对应的聊天会话 – 同时删除会话成员表中的成员信息ret _mysql_chat_session-remove(uid, pid);if(ret false) {LOG_ERROR({}- 从数据库删除好友会话信息失败, rid);return err_response(rid, 从数据库删除好友会话信息失败);}//4. 组织响应response-set_request_id(rid);response-set_success(true);}}; }2搜索好友 取出请求中的用户 ID和搜索关键字。从好友关系表中取出该用户所有好友 ID。根据关键字从 ES 服务器中进行用户搜索搜索的时候需要将关键字作为用户 ID/手机号/昵称的搜索关键字进行搜索且需要根据自己的 ID 和好友 ID 过滤掉自己和自己的好友。根据搜索到的用户简息中的头像 ID从文件服务器批量获取用户头像数据。组织响应将搜索到的用户列表响应给网关。 namespace MyTest {class FriendServiceImpl : public bite_im::FriendService {public:virtual void FriendSearch(::google::protobuf::RpcController *controller,const ::bite_im::FriendSearchReq *request,::bite_im::FriendSearchRsp *response,::google::protobuf::Closure *done){brpc::ClosureGuard rpc_guard(done);// 1. 定义错误回调auto err_response this, response - 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 uid request-user_id();std::string skey request-search_key();LOG_DEBUG({} 好友搜索 {}, uid, skey);//2. 根据用户ID获取用户的好友ID列表auto friend_id_lists _mysql_relation-friends(uid);//3. 从ES搜索引擎进行用户信息搜索 — 过滤掉当前的好友std::unordered_setstd::string user_id_lists;friend_id_lists.push_back(uid);// 把自己也过滤掉auto search_res _es_user-search(skey, friend_id_lists);for(auto iter : search_res) {user_id_lists.insert(iter.user_id());}//4. 根据获取到的用户ID 从用户子服务器进行批量用户信息获取std::unordered_mapstd::string, UserInfo user_list;bool ret GetUserInfo(rid, user_id_lists, user_list);if(ret false){LOG_ERROR({} - 批量获取用户信息失败!, rid);return err_response(rid, 批量获取用户信息失败!);}//5. 组织响应response-set_request_id(rid);response-set_success(true);for(const auto user_it : user_list) {auto user_info response-add_user_info();user_info-CopyFrom(user_it.second);}}}; }2.6.5 会话操作接口实现 1创建会话 从请求中取出用户 ID 与会话名称以及会话的成员 ID 列表。生成会话 ID并向会话表中新增会话信息数据会话为群聊会话单聊会话是同意好友申请的时候创建的。向会话成员表中新增所有的成员信息。组织响应将组织好的会话信息响应给网关。 namespace MyTest {class FriendServiceImpl : public bite_im::FriendService {public:virtual void ChatSessionCreate(::google::protobuf::RpcController *controller,const ::bite_im::ChatSessionCreateReq *request,::bite_im::ChatSessionCreateRsp *response,::google::protobuf::Closure *done){brpc::ClosureGuard rpc_guard(done);// 1. 定义错误回调auto err_response this, response - void{response-set_request_id(rid);response-set_success(false);response-set_errmsg(errmsg);return;};// 创建会话其实针对的是用户要创建一个群聊会话// 1. 提取请求关键要素会话名称会话成员std::string rid request-request_id();std::string uid request-user_id();std::string cssname request-chat_session_name();//2. 生成会话ID向数据库添加会话信息添加会话成员信息std::string cssid uuid();ChatSession cs(cssid, cssname, ChatSessionType::GROUP);bool ret _mysql_chat_session-insert(cs);if(ret false){LOG_ERROR({} - 向数据库添加会话信息失败: {}, rid, cssname);return err_response(rid, 向数据库添加会话信息失败!);}std::vectorChatSessionMember member_list;for(int i 0; i request-member_id_list_size(); i){ChatSessionMember csm(cssid, request-member_id_list(i));member_list.push_back(csm);}ret _mysql_chat_session_member-append(member_list);if(ret false){LOG_ERROR({} - 向数据库添加会话成员信息失败: {}, rid, cssname);return err_response(rid, 向数据库添加会话成员信息失败!);}//3. 组织响应—组织会话信息response-set_request_id(rid);response-set_success(true);response-mutable_chat_session_info()-set_chat_session_id(cssid);response-mutable_chat_session_info()-set_chat_session_name(cssname);}}; }2获取会话列表 从请求中取出用户 ID。根据用户 ID从会话表会话成员表用户表中取出好友的单聊会话列表会话 ID好友用户 ID好友昵称好友头像 ID并组织会话信息结构对象。单聊会话中对方的昵称就是会话名称对方的头像就是会话头像会话类型为单聊类型。根据单聊会话 ID从消息存储子服务获取会话的最后一条消息。根据好友头像 ID从文件存储子服务批量获取好友头像数据。组织好单聊会话结构数据。根据用户 ID从会话表会话成员表中取出群聊会话列表会话 ID会话名称。根据群聊会话 ID从消息存储子服务获取会话的最后一条消息。组织好群聊会话结构数据。将单聊会话数据和群聊会话数据组织到一起响应给网关。 namespace MyTest {class FriendServiceImpl : public bite_im::FriendService {public:virtual void GetChatSessionList(::google::protobuf::RpcController *controller,const ::bite_im::GetChatSessionListReq *request,::bite_im::GetChatSessionListRsp *response,::google::protobuf::Closure *done){brpc::ClosureGuard rpc_guard(done);// 1. 定义错误回调auto err_response this, response - void{response-set_request_id(rid);response-set_success(false);response-set_errmsg(errmsg);return;};//获取聊天会话的作用一个用户登录成功后能够展示自己的历史聊天信息//1. 提取请求中的关键要素当前请求用户IDstd::string rid request-request_id();std::string uid request-user_id();//2. 从数据库中查询出用户的单聊会话列表auto sf_list _mysql_chat_session-singleChatSession(uid);// 1). 从单聊会话列表中取出所有的好友ID从用户子服务获取用户信息std::unordered_setstd::string users_id_list;for(const auto f : sf_list) {users_id_list.insert(f.friend_id);}std::unordered_mapstd::string, bite_im::UserInfo user_list;bool ret GetUserInfo(rid, users_id_list, user_list);if(ret false){LOG_ERROR({} - 批量获取用户信息失败, rid);return err_response(rid, 批量获取用户信息失败!);}// 2). 设置响应会话信息会话名称就是好友名称会话头像就是好友头像//3. 从数据库中查询出用户的群聊会话列表auto gc_list _mysql_chat_session-groupChatSession(uid);//4. 根据所有的会话ID从消息存储子服务获取会话最后一条消息//5. 组织响应for(const auto f : sf_list){auto chat_session_info response-add_chat_session_info_list();chat_session_info-set_single_chat_friend_id(f.friend_id);chat_session_info-set_chat_session_id(f.chat_session_id);chat_session_info-set_chat_session_name(user_list[f.friend_id].nickname());chat_session_info-set_avatar(user_list[f.friend_id].avatar());MessageInfo msg;ret GetRecentMsg(rid, f.chat_session_id, msg);if(ret false){continue;}chat_session_info-mutable_prev_message()-CopyFrom(msg);}for (const auto f : gc_list){auto chat_session_info response-add_chat_session_info_list();chat_session_info-set_chat_session_id(f.chat_session_id);chat_session_info-set_chat_session_name(f.chat_session_name);MessageInfo msg;ret GetRecentMsg(rid, f.chat_session_id, msg);if(ret false){continue;}chat_session_info-mutable_prev_message()-CopyFrom(msg);}response-set_request_id(rid);response-set_success(true);}private:bool GetRecentMsg(const std::string rid,const std::string cssid, MessageInfo msg){auto channel _mm_channels-choose(_message_service_name);if(!channel){LOG_ERROR({} - 获取消息子服务信道失败, rid);return false;}GetRecentMsgReq req;GetRecentMsgRsp rsp;req.set_request_id(rid);req.set_chat_session_id(cssid);req.set_msg_count(1);brpc::Controller cntl;bite_im::MsgStorageService_Stub stub(channel.get());stub.GetRecentMsg(cntl, req, rsp, nullptr);if(cntl.Failed() true){LOG_ERROR({} - 消息存储子服务调用失败: {}, rid, cntl.ErrorText());return false;}if(rsp.success() false){LOG_ERROR({} - 获取会话 {} 最近消息失败: {}, rid, cssid, rsp.errmsg());return false;}if(rsp.msg_list_size() 0){msg.CopyFrom(rsp.msg_list(0));return true;}return false;}}; }3获取会话成员 取出请求中用户 ID和会话 ID根据会话 ID从会话成员表用户表中取出所有的成员用户信息根据成员信息中的头像 ID从文件存储子服务批量获取头像数据组织用户信息结 构组织响应将会话的成员用户信息列表响应给网关 namespace MyTest {class FriendServiceImpl : public bite_im::FriendService {public:virtual void GetChatSessionMember(::google::protobuf::RpcController *controller,const ::bite_im::GetChatSessionMemberReq *request,::bite_im::GetChatSessionMemberRsp *response,::google::protobuf::Closure *done){brpc::ClosureGuard rpc_guard(done);// 1. 定义错误回调auto err_response this, response - void{response-set_request_id(rid);response-set_success(false);response-set_errmsg(errmsg);return;};//用于用户查看群聊成员信息的时候进行成员信息展示//1. 提取关键要素聊天会话IDstd::string rid request-request_id();std::string uid request-user_id();std::string cssid request-chat_session_id();//2. 从数据库获取会话成员ID列表auto member_id_lists _mysql_chat_session_member-members(cssid);std::unordered_setstd::string uid_list;for(const auto id : member_id_lists) {uid_list.insert(id);}//3. 从用户子服务批量获取用户信息std::unordered_mapstd::string, UserInfo user_list;bool ret GetUserInfo(rid, uid_list, user_list);if(ret false) {LOG_ERROR({} - 从用户子服务获取用户信息失败, rid);return err_response(rid, 从用户子服务获取用户信息失败!);}//4. 组织响应response-set_request_id(rid);response-set_success(true);for(const auto uit : user_list) {auto user_info response-add_member_info_list();user_info-CopyFrom(uit.second);}}~FriendServiceImpl(){}private:ESUser::ptr _es_user;FriendApplyTable::ptr _mysql_apply;ChatSessionTable::ptr _mysql_chat_session;ChatSessionMemeberTable::ptr _mysql_chat_session_member;RelationTable::ptr _mysql_relation;//这边是rpc调用客户端相关对象std::string _user_service_name;std::string _message_service_name;ServiceManager::ptr _mm_channels;}; }2.6.6 搭建Rpc服务和创建用户子服务的工厂类 1创建FriendServer类来搭建RPC服务器 namespace MyTest {class FriendServer {public:using ptr std::shared_ptrFriendServer;FriendServer(const Discovery::ptr service_discoverer, const Registry::ptr reg_client,const std::shared_ptrelasticlient::Client es_client,const std::shared_ptrodb::core::database mysql_client,const std::shared_ptrbrpc::Server server):_service_discoverer(service_discoverer),_registry_client(reg_client),_es_client(es_client),_mysql_client(mysql_client),_rpc_server(server){}//搭建RPC服务器并启动服务器void start(){_rpc_server-RunUntilAskedToQuit();}~FriendServer(){}private:Discovery::ptr _service_discoverer;Registry::ptr _registry_client;std::shared_ptrelasticlient::Client _es_client;std::shared_ptrodb::core::database _mysql_client;std::shared_ptrbrpc::Server _rpc_server;}; }2创建工厂类MessageServerBuilder来实现用户子服务的创建以及Rpc服务器的创建 namespace MyTest {class FriendServerBuilder{public:// 构造es客户端对象void make_es_object(const std::vectorstd::string host_list){_es_client ESClientFactory::create(host_list);}// 构造mysql客户端对象void make_mysql_object(const std::string user,const std::string pswd,const std::string host,const std::string db,const std::string cset,int port,int conn_pool_count){_mysql_client ODBFactory::create(user, pswd, host, db, cset, port, conn_pool_count);}// 用于构造服务发现客户端信道管理对象void make_discovery_object(const std::string reg_host,const std::string base_service_name,const std::string user_service_name,const std::string message_service_name){_user_service_name user_service_name;_message_service_name message_service_name;_mm_channels std::make_sharedServiceManager();_mm_channels-declared(user_service_name);_mm_channels-declared(message_service_name);LOG_DEBUG(设置用户子服务为需添加管理的子服务{}, user_service_name);LOG_DEBUG(设置消息子服务为需添加管理的子服务{}, message_service_name);auto put_cb std::bind(ServiceManager::onServiceOnline, _mm_channels.get(), std::placeholders::_1, std::placeholders::_2);auto del_cb std::bind(ServiceManager::onServiceOffline, _mm_channels.get(), std::placeholders::_1, std::placeholders::_2);_service_discoverer std::make_sharedDiscovery(reg_host, base_service_name, put_cb, del_cb);}// 用于构造服务注册客户端对象void make_registry_object(const std::string reg_host,const std::string service_name,const std::string access_host){_registry_client std::make_sharedRegistry(reg_host);_registry_client-registry(service_name, access_host);}void make_rpc_server(uint16_t port, int32_t timeout, uint8_t num_threads){if(!_es_client){LOG_ERROR(还未初始化ES搜索引擎模块);abort();}if(!_mysql_client){LOG_ERROR(还未初始化Mysql数据库模块);abort();}if(!_mm_channels){LOG_ERROR(还未初始化信道管理模块);abort();}_rpc_server std::make_sharedbrpc::Server();FriendServiceImpl *friend_service new FriendServiceImpl(_es_client,_mysql_client, _mm_channels, _user_service_name, _message_service_name);int ret _rpc_server-AddService(friend_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();}}// 构造RPC服务器对象FriendServer::ptr build(){if (!_service_discoverer){LOG_ERROR(还未初始化服务发现模块);abort();}if (!_registry_client){LOG_ERROR(还未初始化服务注册模块);abort();}if (!_rpc_server){LOG_ERROR(还未初始化RPC服务器模块);abort();}FriendServer::ptr server std::make_sharedFriendServer(_service_discoverer, _registry_client,_es_client, _mysql_client, _rpc_server);return server;}private:Registry::ptr _registry_client;std::shared_ptrelasticlient::Client _es_client;std::shared_ptrodb::core::database _mysql_client;std::string _user_service_name;std::string _message_service_name;ServiceManager::ptr _mm_channels;Discovery::ptr _service_discoverer;std::shared_ptrbrpc::Server _rpc_server;}; }3实现好友管理子服务的服务器的搭建 #include friend_server.hpp//主要实现语音识别子服务的服务器的搭建DEFINE_bool(run_mode, false, 程序的运行模式false-调试 true-发布); DEFINE_string(log_file, , 发布模式下用于指定日志的输出文件); DEFINE_int32(log_level, 0, 发布模式下用于指定日志输出等级);DEFINE_string(registry_host, http://127.0.0.1:2379, 服务注册中心地址); DEFINE_string(instance_name, /friend_service/instance, 当前实例名称); DEFINE_string(access_host, 127.0.0.1:10006, 当前实例的外部访问地址);DEFINE_int32(listen_port, 10006, Rpc服务器监听端口); DEFINE_int32(rpc_timeout, -1, Rpc调用超时时间); DEFINE_int32(rpc_threads, 1, Rpc的IO线程数量);DEFINE_string(base_service, /service, 服务监控根目录); DEFINE_string(user_service, /service/user_service, 用户管理子服务名称); DEFINE_string(message_service, /service/message_service, 消息存储子服务名称);DEFINE_string(es_host, http://127.0.0.1:9200/, ES搜索引擎服务器URL);DEFINE_string(mysql_host, 127.0.0.1, Mysql服务器访问地址); DEFINE_string(mysql_user, root, Mysql服务器访问用户名); DEFINE_string(mysql_pswd, 123456, Mysql服务器访问密码); DEFINE_string(mysql_db, bite_im, Mysql默认库名称); DEFINE_string(mysql_cset, utf8, Mysql客户端字符集); DEFINE_int32(mysql_port, 0, Mysql服务器访问端口); DEFINE_int32(mysql_pool_count, 4, Mysql连接池最大连接数量);int main(int argc, char *argv[]) {google::ParseCommandLineFlags(argc, argv, true);bite_im::init_logger(FLAGS_run_mode, FLAGS_log_file, FLAGS_log_level);bite_im::FriendServerBuilder fsb;fsb.make_es_object({FLAGS_es_host});fsb.make_mysql_object(FLAGS_mysql_user, FLAGS_mysql_pswd, FLAGS_mysql_host, FLAGS_mysql_db, FLAGS_mysql_cset, FLAGS_mysql_port, FLAGS_mysql_pool_count);fsb.make_discovery_object(FLAGS_registry_host, FLAGS_base_service, FLAGS_user_service, FLAGS_message_service);fsb.make_rpc_server(FLAGS_listen_port, FLAGS_rpc_timeout, FLAGS_rpc_threads);fsb.make_registry_object(FLAGS_registry_host, FLAGS_base_service FLAGS_instance_name, FLAGS_access_host);auto server fsb.build();server-start();return 0; }4cmake构建代码

1. 添加cmake版本说明

cmake_minimum_required(VERSION 3.1.3)

2. 声明工程名称

project(friend_server)set(target friend_server)set(CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS} -g)# 3. 检测并生成ODB框架代码

1. 添加所需的proto映射代码文件名称

set(proto_path ${CMAKE_CURRENT_SOURCE_DIR}/../proto) set(proto_files base.proto user.proto message.proto friend.proto)

2. 检测框架代码文件是否已经生成

set(proto_hxx ) set(proto_cxx ) set(proto_srcs ) foreach(proto_file ${proto_files})

3. 如果没有生成则预定义生成指令 – 用于在构建项目之间先生成框架代码string(REPLACE .proto .pb.cc proto_cc \({proto_file})string(REPLACE .proto .pb.h proto_hh \){proto_file})if (NOT EXISTS \({CMAKE_CURRENT_BINARY_DIR}\){proto_cc})add_custom_command(PRE_BUILDCOMMAND protocARGS –cpp_out\({CMAKE_CURRENT_BINARY_DIR} -I \){proto_path} –experimental_allow_proto3_optional \({proto_path}/\){proto_file}DEPENDS \({proto_path}/\){proto_file}OUTPUT \({CMAKE_CURRENT_BINARY_DIR}/\){proto_cc}COMMENT 生成Protobuf框架代码文件: \({CMAKE_CURRENT_BINARY_DIR}/\){proto_cc})endif()list(APPEND proto_srcs \({CMAKE_CURRENT_BINARY_DIR}/\){proto_cc})

endforeach()# 3. 检测并生成ODB框架代码

1. 添加所需的odb映射代码文件名称

set(odb_path ${CMAKE_CURRENT_SOURCE_DIR}/../odb) set(odb_files chat_session_member.hxx chat_session.hxx friend_apply.hxx relation.hxx)

2. 检测框架代码文件是否已经生成

set(odb_hxx ) set(odb_cxx ) set(odb_srcs ) foreach(odb_file ${odb_files})

3. 如果没有生成则预定义生成指令 – 用于在构建项目之间先生成框架代码string(REPLACE .hxx -odb.hxx odb_hxx \({odb_file})string(REPLACE .hxx -odb.cxx odb_cxx \){odb_file})if (NOT EXISTS \({CMAKE_CURRENT_BINARY_DIR}\){odb_cxx})add_custom_command(PRE_BUILDCOMMAND odbARGS -d mysql –std c11 –generate-query –generate-schema –profile boost/date-time \({odb_path}/\){odb_file}DEPENDS \({odb_path}/\){odb_file}OUTPUT \({CMAKE_CURRENT_BINARY_DIR}/\){odb_cxx}COMMENT 生成ODB框架代码文件: \({CMAKE_CURRENT_BINARY_DIR}/\){odb_cxx})endif()

4. 将所有生成的框架源码文件名称保存起来 student-odb.cxx classes-odb.cxxlist(APPEND odb_srcs \({CMAKE_CURRENT_BINARY_DIR}/\){odb_cxx})

endforeach()# 4. 获取源码目录下的所有源码文件 set(src_files ) aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR}/source src_files)

5. 声明目标及依赖

add_executable(\({target} \){src_files} \({proto_srcs} \){odb_srcs})

7. 设置需要连接的库

target_link_libraries(\({target} -lgflags -lspdlog -lfmt -lbrpc -lssl -lcrypto -lprotobuf -lleveldb -letcd-cpp-api -lcpprest -lcurl -lodb-mysql -lodb -lodb-boost/usr/lib/x86_64-linux-gnu/libjsoncpp.so.19-lcpr -lelasticlient)set(test_client friend_client) set(test_files ) aux_source_directory(\){CMAKE_CURRENT_SOURCE_DIR}/test test_files) add_executable(\({test_client} \){test_files} \({proto_srcs}) target_link_libraries(\){test_client} -pthread -lgtest -lgflags -lspdlog -lfmt -lbrpc -lssl -lcrypto -lprotobuf -lleveldb -letcd-cpp-api -lcpprest -lcurl /usr/lib/x86_64-linux-gnu/libjsoncpp.so.19)# 6. 设置头文件默认搜索路径 include_directories(\({CMAKE_CURRENT_BINARY_DIR}) include_directories(\){CMAKE_CURRENT_SOURCE_DIR}/../common) include_directories(\({CMAKE_CURRENT_SOURCE_DIR}/../odb) include_directories(\){CMAKE_CURRENT_SOURCE_DIR}/../third/include)#8. 设置安装路径 INSTALL(TARGETS \({target} \){test_client} RUNTIME DESTINATION bin)3. 入口网关子服务的实现 3.1 功能设计 1网关服务器在设计中最重要的两个功能 作为入口服务器接收客户端的所有请求进行请求的子服务分发得到响应后进行响应。对客户端进行事件通知好友申请和处理及删除单聊/群聊会话创建新消息。 2基于以上的两个功能因此网关服务器包含两项通信 HTTP 通信进行业务处理。WEBSOCKET 通信进行事件通知。 3.2 模块划分 1以下是模块划分 参数/配置文件解析模块基于 gflags 框架直接使用进行参数/配置文件解析。日志模块基于 spdlog 框架封装的模块直接使用进行日志输出。rpc 服务发现与调用模块基于 etcd 框架与 brpc 框架封装的服务发现与调用模块。因为要分发处理所有请求因此所有的子服务都需要进行服务发现。redis 客户端模块基于 redis封装的客户端进行内存数据库数据操作。根据用户子服务添加的会话信息进行用户连接身份识别与鉴权。HTTP 通信服务器模块基于 cpp-httplib 库搭建 HTTP 服务器接收 HTTP 请求进行业务处理。WEBSOCKET 服务器模块基于 Websocketpp 库搭建 websocket 服务器进行事件通知。客户端长连接管理模块建议用户 ID 与长连接句柄映射关系便于后续根据用户ID 找到连接进行事件通知 3.3 模块功能示意图 1如下是模块功能图
3.4 接口的实现 1用户名注册 取出 HTTP 请求正文进行 ProtoBuf 反序列化查找用户子服务调用子服务对应接口进行业务处理将处理结果响应给客户端。 2用户名登录 取出 HTTP 请求正文进行 ProtoBuf 反序列化查找用户子服务调用子服务对应接口进行业务处理将处理结果响应给客户端。 3短信验证码获取 取出 HTTP 请求正文进行 ProtoBuf 反序列化查找用户子服务调用子服务对应接口进行业务处理将处理结果响应给客户端。 4手机号码注册 取出 HTTP 请求正文进行 ProtoBuf 反序列化查找用户子服务调用子服务对应接口进行业务处理将处理结果响应给客户端。 5手机号码登录 取出 HTTP 请求正文进行 ProtoBuf 反序列化查找用户子服务调用子服务对应接口进行业务处理将处理结果响应给客户端。 6用户信息获取 取出 HTTP 请求正文进行 ProtoBuf 反序列化根据请求中的会话 ID 进行鉴权并获取用户 ID向请求中设置用户 ID查找用户子服务调用子服务对应接口进行业务处理将处理结果响应给客户端。 7修改用户头像 取出 HTTP 请求正文进行 ProtoBuf 反序列化根据请求中的会话 ID 进行鉴权并获取用户 ID向请求中设置用户 ID查找用户子服务调用子服务对应接口进行业务处理将处理结果响应给客户端。 8修改用户签名 取出 HTTP 请求正文进行 ProtoBuf 反序列化根据请求中的会话 ID 进行鉴权并获取用户 ID向请求中设置用户 ID查找用户子服务调用子服务对应接口进行业务处理将处理结果响应给客户端。 9修改用户昵称 取出 HTTP 请求正文进行 ProtoBuf 反序列化根据请求中的会话 ID 进行鉴权并获取用户 ID向请求中设置用户 ID查找用户子服务调用子服务对应接口进行业务处理将处理结果响应给客户端。 10修改用户绑定手机号 取出 HTTP 请求正文进行 ProtoBuf 反序列化根据请求中的会话 ID 进行鉴权并获取用户 ID向请求中设置用户 ID查找用户子服务调用子服务对应接口进行业务处理将处理结果响应给客户端。 11获取好友列表
取出 HTTP 请求正文进行 ProtoBuf 反序列化根据请求中的会话 ID 进行鉴权并获取用户 ID向请求中设置用户 ID查找好友子服务调用子服务对应接口进行业务处理将处理结果响应给客户端。 12发送好友申请
取出 HTTP 请求正文进行 ProtoBuf 反序列化根据请求中的会话 ID 进行鉴权并获取用户 ID向请求中设置用户 ID查找用户子服务根据请求中的用户 ID调用用户子服务获取用户的详细信息查找好友子服务调用子服务对应接口进行业务处理若处理成功则通过被申请人 ID查找对方长连接。若长连接存在对方在线则组织好友申请通知进行事件通知将处理结果响应给客户端。 13获取待处理好友申请 取出 HTTP 请求正文进行 ProtoBuf 反序列化根据请求中的会话 ID 进行鉴权并获取用户 ID向请求中设置用户 ID查找好友子服务调用子服务对应接口进行业务处理将处理结果响应给客户端。 14好友申请处理
取出 HTTP 请求正文进行 ProtoBuf 反序列化根据请求中的会话 ID 进行鉴权并获取用户 ID向请求中设置用户 ID查找用户子服务根据请求中的用户 ID调用用户子服务获取申请人与被申请人的详细信息查找好友子服务调用子服务对应接口进行业务处理若处理成功则通过申请人 ID查找申请人长连接进行申请处理结果的通知 若处理结果是同意则意味着新聊天会话的创建则对申请人继续进行聊天 会话创建通知。则从处理结果中取出会话 ID使用对方的昵称作为会话名称对方的头像作为会话头像组织会话信息若处理结果是同意则对当前处理者用户 ID 查找长连接进行聊天会话创建的通知。则从处理结果中取出会话 ID使用对方的昵称作为会话名称对方的头像作为会话头像组织会话信息
清理响应中的会话 ID 信息。 将处理结果响应给客户端 15删除好友 取出 HTTP 请求正文进行 ProtoBuf 反序列化根据请求中的会话 ID 进行鉴权并获取用户 ID向请求中设置用户 ID查找好友子服务调用子服务对应接口进行业务处理若处理成功则通过被删除者用户 ID查找对方长连接。若长连接存在对方在线则组织好友删除通知进行事件通知将处理结果响应给客户端。 16搜索用户 取出 HTTP 请求正文进行 ProtoBuf 反序列化根据请求中的会话 ID 进行鉴权并获取用户 ID向请求中设置用户 ID查找好友子服务调用子服务对应接口进行业务处理将处理结果响应给客户端。 17获取用户聊天会话列表 取出 HTTP 请求正文进行 ProtoBuf 反序列化根据请求中的会话 ID 进行鉴权并获取用户 ID向请求中设置用户 ID查找好友子服务调用子服务对应接口进行业务处理将处理结果响应给客户端。 18创建多人聊天会话 取出 HTTP 请求正文进行 ProtoBuf 反序列化根据请求中的会话 ID 进行鉴权并获取用户 ID向请求中设置用户 ID查找好友子服务调用子服务对应接口进行业务处理若处理成功循环根据会话成员的 ID 找到他们的长连接 根据响应中的会话信息逐个进行会话创建的通知清理响应中的会话信息 将处理结果响应给客户端。 19获取消息会话成员列表 取出 HTTP 请求正文进行 ProtoBuf 反序列化根据请求中的会话 ID 进行鉴权并获取用户 ID向请求中设置用户 ID查找好友子服务调用子服务对应接口进行业务处理将处理结果响应给客户端。 20发送新消息 取出 HTTP 请求正文进行 ProtoBuf 反序列化根据请求中的会话 ID 进行鉴权并获取用户 ID向请求中设置用户 ID查找消息转发子服务调用子服务对应接口进行业务处理若处理成功则根据处理结果中的用户 ID 列表循环找到目标长连接根据处理结果中的消息字段组织新消息通知逐个对目标进行新消息通知。若处理失败则根据处理结果中的错误提示信息设置响应内容将处理结果响应给客户端。 21获取指定时间段消息列表 取出 HTTP 请求正文进行 ProtoBuf 反序列化根据请求中的会话 ID 进行鉴权并获取用户 ID向请求中设置用户 ID查找消息存储子服务调用子服务对应接口进行业务处理将处理结果响应给客户端。 22获取最近 N 条消息列表 取出 HTTP 请求正文进行 ProtoBuf 反序列化根据请求中的会话 ID 进行鉴权并获取用户 ID向请求中设置用户 ID查找消息存储子服务调用子服务对应接口进行业务处理将处理结果响应给客户端。 23搜索关键字历史消息 取出 HTTP 请求正文进行 ProtoBuf 反序列化根据请求中的会话 ID 进行鉴权并获取用户 ID向请求中设置用户 ID查找消息存储子服务调用子服务对应接口进行业务处理将处理结果响应给客户端。 24单个文件数据获取 取出 HTTP 请求正文进行 ProtoBuf 反序列化根据请求中的会话 ID 进行鉴权并获取用户 ID向请求中设置用户 ID查找文件子服务调用子服务对应接口进行业务处理将处理结果响应给客户端。 25多个文件数据获取 取出 HTTP 请求正文进行 ProtoBuf 反序列化根据请求中的会话 ID 进行鉴权并获取用户 ID向请求中设置用户 ID查找文件子服务调用子服务对应接口进行业务处理将处理结果响应给客户端。 26单个文件数据上传 取出 HTTP 请求正文进行 ProtoBuf 反序列化根据请求中的会话 ID 进行鉴权并获取用户 ID向请求中设置用户 ID查找文件子服务调用子服务对应接口进行业务处理将处理结果响应给客户端。 27多个文件数据上传 取出 HTTP 请求正文进行 ProtoBuf 反序列化根据请求中的会话 ID 进行鉴权并获取用户 ID向请求中设置用户 ID查找文件子服务调用子服务对应接口进行业务处理将处理结果响应给客户端。 28语音转文字 取出 HTTP 请求正文进行 ProtoBuf 反序列化根据请求中的会话 ID 进行鉴权并获取用户 ID向请求中设置用户 ID查找语音子服务调用子服务对应接口进行业务处理将处理结果响应给客户端。 29具体实现的代码见https://gitee.com/liu-yechi/new_code/tree/master/chat_system/server/gateway/source。 4 Docker的介绍 docker 是一个用 Go 语言实现的应用容器引擎开源项目可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中然后发布到任何流行的 Linux 机器上也可以实现虚拟化。其广泛应用于开发、测试和生产环境中帮助开发者和系统管理员简化应用的部署和管理实现快速的交付测试和部署。 4.1 docker的安装 安装 docker 依赖 SQL sudo apt-get install ca-certificates curl gnupg lsb-release配置加速地址 SQL sudo mkdir -p /etc/docker sudo tee /etc/docker/daemon.json -EOF { registry-mirrors: [https://do.nark.eu.org,https://dc.j8.work,https://docker.m.daocloud.io,https://dockerproxy.com,https://docker.mirrors.ustc.edu.cn,https://docker.nju.edu.cn] } EOFsudo systemctl daemon-reload添加 Docker 官方 GPG 密钥 SQL curl -fsSL http://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg | sudo apt-key add -添加 Docker 软件源 SQL sudo add-apt-repository deb [archamd64] http://mirrors.aliyun.com/docker-ce/linux/ubuntu \((lsb_release -cs) stable安装 Docker SQL sudo apt-get install docker-ce docker-ce-cli containerd.io安装 docker-compose SQL sudo curl -L https://github.com/docker/compose/releases/download/v2.13.0/docker-compose-linux-x86_64 -o /usr/bin/docker-compose sudo chmod x /usr/bin/docker-compose docker-compose --version配置用户组 SQL sudo groupadd docker sudo gpasswd -a \)USER docker newgrp docker测试-查看版本 SQL docker –version4.2 docker常用指令 4.2.1 容器操作 1查看docker Plain Text docker ps -a docker container ls -a

-a 所有容器

-q 仅显示容器 ID2删除docker

Plain Text docker rm container_id docker container rm container_id docker container rm $(docker container ls -a -q)3启动docker Plain Text docker run [options] [image:version]

options:

-d, –detach : 运行容器于后台并打印容器 ID

-e, –env list 设置运行环境变量 -e DB_USERmyuser

-i, –interactive 即使没有连接也要保持 STDIN 打开

-p, –publish list 设置于宿主机端口映射 -p 3306:3306

-t, –tty : 申请终端

-v, –volume list 设置与宿主机文件挂载 -

v ./data:/var/lib/mysqlrw4停止docker Plain Text docker container stop container_id docker container stop $(docker container ls -a -q)4.2.2 镜像操作 1拉取镜像 Plain Text docker pull image_name:version2查看镜像 Plain Text docker images docker image ls -a3创建镜像 Plain Text docker build [options] path

options:

-f, –file string : 指定构建镜像的 dockerfile

-t, –tag stringArray 设置镜像名称与版本 -t myImage:version4删除镜像

Plain Text docker rmi image_id docker image rm image_id5导入/导出镜像 Plain Text docker save -o myimage.tar myimage:latest docker load -i myimage.tar6缓存及镜像清理 Plain Text docker system df docker system prune -a4.3 dockerfile编写规则简介 1样例 YAML FROM ubuntu:22.04 LABEL MAINTAINERbitejiuyeke ENV REDIS_VERSION5.0.3 WORKDIR /im RUN mkdir -p /im/data \ mkdir -p /im/logs \ mkdir -p /im/conf \mkdir -p /im/bin COPY build/file_server /im/bin/ COPY depends/ /usr/lib/x86_64-linux-gnu/ EXPOSE 10001/tcp CMD /im/bin/file_server -flagfile/im/conf/server_file.conf2样例参数解析 FROM 注明所使用的镜像。LABEL 构建镜像时设置键值对这里 MAINTAINER 表示维护人。ENV 用于设置镜像中的环境变量。WORKDIR 设定镜像中的工作路径该目录会在镜像系统中自动创建。RUN 在镜像中执行的指令。COPY 将宿主机中的文件拷贝到镜像系统指定路径下。EXPOSE 对外暴露端口。CMD 设置容器启动时默认执行的命令。 4.4 dockercompose编写规则简介 1样例 YAML version: 3.8 services:mysql:image: mysql:8.0.39container_name: docker-msyql8-servicevolumes: - ./sql/:/docker-entrypoint-initdb.d/- ./middleware/mysql/data:/var/lib/mysql:rw- ./middleware/mysql/logs:/var/log/mysql:rw- ./conf/mysql:/etc/mysql/environment:MYSQL_ROOT_PASSWORD: 123456ports:- 3306:3306restart: alwaysdepends_on:- etcd2样例参数解析 version docker-compose 语法版本。services 要启动的服务。mysql 第 4 行的 mysql 为对应的服务名称在这里其实就是个标识image来源镜像及版本。container_name用户设定的容器名称。volumes挂载卷其实就是将宿主机上的指定文件或目录与镜像机进行挂载。environment设定镜像机中的环境变量。ports宿主机与镜像机的端口映射。restart容器重启策略always 表示无论退出状态如何容器总是重新启动。depends_on启动依赖通过依赖关系控制容器启动顺序。

  1. 项目部署 5.1 编写项目配置文件 1在项目的各个子服务中每个子服务可能都会有不同的配置代码中我们通过 gflags进行了参数解析但是如果改换了部署的机器就需要修改代码中的数据然后重新编译代码这是一件非常麻烦的事情会导致项目的自动部署成为空谈幸好gflags不仅支持参数的解析也支持配置文件的解析因此我们需要将代码中需要的参数通过配置文件来进行配置。 2配置文件的实现见https://gitee.com/liu-yechi/new_code/tree/master/chat_system/server/conf。 5.2 查询程序依赖 1在我们的子服务中所采用的 docker 镜像与我们的开发环境保持一致使用 ubuntu:22.04但是这个镜像是一个空白的镜像因此我们需要针对这个空白的 ubuntu 镜像进行改造搭建我们服务的运行环境。 2注意我们要搭建的是运行环境而不是开发环境也就是说只需要镜像中包含有我们程序运行所需的动态库即可因此我们需要查找到我们子服务程序的依赖库并将其拷贝到镜像中。如下编写depends.sh文件 #!/bin/bash#传递两个参数

    1. 可执行程序的路径名

    2. 目录名称 — 将这个程序的依赖库拷贝到指定目录下

    declare depends get_depends() {depends\((ldd \)1 | awk {if (match(\(3,/)){print \)3}})#mkdir \(2cp -Lr \)depends $2 }get_depends ./gateway/build/gateway_server ./gateway/depends get_depends ./file/build/file_server ./file/depends get_depends ./friend/build/friend_server ./friend/depends get_depends ./message/build/message_server ./message/depends get_depends ./speech/build/speech_server ./speech/depends get_depends ./transmite/build/transmite_server ./transmite/depends get_depends ./user/build/user_server ./user/dependscp /bin/nc ./gateway/ cp /bin/nc ./file/ cp /bin/nc ./friend/ cp /bin/nc ./message/ cp /bin/nc ./speech/ cp /bin/nc ./transmite/ cp /bin/nc ./user/ get_depends /bin/nc ./gateway/depends get_depends /bin/nc ./file/depends get_depends /bin/nc ./friend/depends get_depends /bin/nc ./message/depends get_depends /bin/nc ./speech/depends get_depends /bin/nc ./user/depends get_depends /bin/nc ./transmite/depends3参数解析 ldd该 shell 指令的作用是查看指定程序的库依赖信息。awk这是一个功能复杂的指令当前用于进行字符串分割并获取指定列。cp用于文件拷贝。 -L 跟踪软连接文件即若依赖库是软连接文件则跟踪到实际文件进行拷贝。-r 递归处理。
    5.3 编写每个子服务的dockerfile文件 1文件管理子服务

    声明基础镜像来源

    FROM ubuntu:22.04

    声明工作路径

    WORKDIR /im RUN mkdir -p /im/logs \mkdir -p /im/data \mkdir -p /im/conf \mkdir -p /im/bin

    将可执行程序文件拷贝进入镜像

    COPY ./build/file_server /im/bin/

    将可执行程序依赖拷贝进入镜像

    COPY ./depends/ /lib/x86_64-linux-gnu/ COPY ./nc /bin/

    设置容器的启动默认操作 — 运行程序

    CMD /im/bin/file_server -flagfile/im/conf/file_server.conf2好友管理子服务

    声明基础镜像来源

    FROM ubuntu:22.04

    声明工作路径

    WORKDIR /im RUN mkdir -p /im/logs \mkdir -p /im/data \mkdir -p /im/conf \mkdir -p /im/bin

    将可执行程序文件拷贝进入镜像

    COPY ./build/friend_server /im/bin/

    将可执行程序依赖拷贝进入镜像

    COPY ./depends/ /lib/x86_64-linux-gnu/ COPY ./nc /bin/

    设置容器的启动默认操作 — 运行程序

    CMD /im/bin/friend_server -flagfile/im/conf/friend_server.conf3入口网关子服务 声明基础镜像来源 FROM ubuntu:22.04

    声明工作路径

    WORKDIR /im RUN mkdir -p /im/logs \mkdir -p /im/data \mkdir -p /im/conf \mkdir -p /im/bin

    将可执行程序文件拷贝进入镜像

    COPY ./build/gateway_server /im/bin/

    将可执行程序依赖拷贝进入镜像

    COPY ./depends/ /lib/x86_64-linux-gnu/COPY ./nc /bin/

    设置容器的启动默认操作 — 运行程序

    CMD /im/bin/gateway_server -flagfile/im/conf/gateway_server.conf4消息存储子服务

    声明基础镜像来源

    FROM ubuntu:22.04

    声明工作路径

    WORKDIR /im RUN mkdir -p /im/logs \mkdir -p /im/data \mkdir -p /im/conf \mkdir -p /im/bin

    将可执行程序文件拷贝进入镜像

    COPY ./build/message_server /im/bin/

    将可执行程序依赖拷贝进入镜像

    COPY ./depends/ /lib/x86_64-linux-gnu/COPY ./nc /bin/

    设置容器的启动默认操作 — 运行程序

    CMD /im/bin/message_server -flagfile/im/conf/message_server.conf5语言识别子服务

    声明基础镜像来源

    FROM ubuntu:22.04

    声明工作路径

    WORKDIR /im RUN mkdir -p /im/logs \mkdir -p /im/data \mkdir -p /im/conf \mkdir -p /im/bin

    将可执行程序文件拷贝进入镜像

    COPY ./build/message_server /im/bin/

    将可执行程序依赖拷贝进入镜像

    COPY ./depends/ /lib/x86_64-linux-gnu/COPY ./nc /bin/

    设置容器的启动默认操作 — 运行程序

    CMD /im/bin/message_server -flagfile/im/conf/message_server.conf6消息转发子服务

    声明基础镜像来源

    FROM ubuntu:22.04

    声明工作路径

    WORKDIR /im RUN mkdir -p /im/logs \mkdir -p /im/data \mkdir -p /im/conf \mkdir -p /im/bin

    将可执行程序文件拷贝进入镜像

    COPY ./build/transmite_server /im/bin/

    将可执行程序依赖拷贝进入镜像

    COPY ./depends/ /lib/x86_64-linux-gnu/ COPY ./nc /bin

    设置容器的启动默认操作 — 运行程序

    CMD /im/bin/transmite_server -flagfile/im/conf/transmite_server.conf7用户管理子服务

    声明基础镜像来源

    FROM ubuntu:22.04

    声明工作路径

    WORKDIR /im RUN mkdir -p /im/logs \mkdir -p /im/data \mkdir -p /im/conf \mkdir -p /im/bin

    将可执行程序文件拷贝进入镜像

    COPY ./build/user_server /im/bin/

    将可执行程序依赖拷贝进入镜像

    COPY ./depends/ /lib/x86_64-linux-gnu/COPY ./nc /bin/

    设置容器的启动默认操作 — 运行程序

    CMD /im/bin/user_server -flagfile/im/conf/user_server.conf5.4 编写entrypoint.sh文件 1包含中间件在内我们共有 13 个服务需要启动这些服务之间会存在一些依赖关系比如 user_service 启动之前必须保证 mysqlredisetcd 这些中间件已经启动才可以因此我们需要做一些启动的顺序控制。 2但是单纯 yaml 配置文件中的 depends 无法满足需求因为它只能控制容器的启动顺序无法控制容器内程序的启动顺序因此我们需要通过端口探测的方式进行程序运行的控制。 #!/bin/bash#./entrypoint.sh -h 127.0.0.1 -p 3306,2379,6379 -c /im/bin/file_server -flagfile./xx.conf# 1. 编写一个端口探测函数端口连接不上则循环等待

    wait_for 127.0.0.1 3306

    wait_for() {while ! nc -z \(1 \)2 doecho \(2 端口连接失败休眠等待;sleep 1;doneecho \)1:$2 检测成功; }

    2. 对脚本运行参数进行解析获取到ipportcommand

    declare ip declare ports declare command while getopts h:p:c: arg docase \(arg inh)ip\)OPTARG;;p)ports\(OPTARG;;c)command\)OPTARG;;esac done

    3. 通过执行脚本进行端口检测

    ${port //,/ } 针对port中的内容以空格替换字符串中的, shell中数组–一种以空格间隔的字符串

    for port in \({ports//,/ } dowait_for \)ip $port done

    4. 执行command

    eval 对一个字符串进行二次检测将其当作命令进行执行

    eval $command3参数解析 nc 指令ncNetcat是一个功能强大的网络工具 -z 选项用于扫描监听中的守护进程也就是扫描远程主机上的开放端口。当与 nc 命令一起使用时-z 选项会让 nc 尝试连接到指定的端口但不会发送任何数据或接收任何响应。这通常用于端口扫描以检查远程主机上的端口是否开放。declare声明变量getopts捕获运行参数eval用于将字符串作为命令来执行。 5.5 编写docker-compose文件 1包含中间件在内我们共有 13 个服务需要启动若一 一都需要手动启动会比较麻烦因此我们使用 docker-compose 进行统一管理启动。 version: 3.8services:etcd:image: quay.io/coreos/etcd:v3.3.25container_name: etcd-serviceenvironment:- ETCD_NAMEetcd-s1- ETCD_DATA_DIR/var/lib/etcd- ETCD_LISTEN_CLIENT_URLShttp://0.0.0.0:2379- ETCD_ADVERTISE_CLIENT_URLShttp://0.0.0.0:2379volumes:# 1. 希望容器内的程序能够访问宿主机上的文件# 2. 希望容器内程序运行所产生的数据文件能落在宿主机上- ./middle/data/etcd:/var/lib/etcd:rwports:- 2379:2379restart: alwaysmysql:image: mysql:8.0.39container_name: mysql-serviceenvironment:MYSQL_ROOT_PASSWORD: 123456volumes:# 1. 希望容器内的程序能够访问宿主机上的文件# 2. 希望容器内程序运行所产生的数据文件能落在宿主机上- ./sql:/docker-entrypoint-initdb.d/:rw- ./middle/data/mysql:/var/lib/mysql:rwports:- 3306:3306restart: alwaysredis:image: redis:6.0.16container_name: redis-servicevolumes:# 1. 希望容器内的程序能够访问宿主机上的文件# 2. 希望容器内程序运行所产生的数据文件能落在宿主机上- ./middle/data/redis:/var/lib/redis:rwports:- 6379:6379restart: alwayselasticsearch:image: elasticsearch:7.17.21container_name: elasticsearch-serviceenvironment:- discovery.typesingle-nodevolumes:# 1. 希望容器内的程序能够访问宿主机上的文件# 2. 希望容器内程序运行所产生的数据文件能落在宿主机上- ./middle/data/elasticsearch:/data:rwports:- 9200:9200- 9300:9300restart: alwaysrabbitmq:image: rabbitmq:3.9.13container_name: rabbitmq-serviceenvironment:RABBITMQ_DEFAULT_USER: rootRABBITMQ_DEFAULT_PASS: 123456volumes:# 1. 希望容器内的程序能够访问宿主机上的文件# 2. 希望容器内程序运行所产生的数据文件能落在宿主机上- ./middle/data/rabbitmq:/var/lib/rabbitmq:rwports:- 5672:5672restart: alwaysfile_server:build: ./file#image: server-user_servercontainer_name: file_server-servicevolumes:# 1. 希望容器内的程序能够访问宿主机上的文件# 2. 希望容器内程序运行所产生的数据文件能落在宿主机上# 挂载的信息 entrypoint.sh文件 数据目录(im/logs, im/data), 配置文件- ./conf/file_server.conf:/im/conf/file_server.conf- ./middle/data/logs:/im/logs:rw- ./middle/data/data:/im/data:rw- ./entrypoint.sh:/im/bin/entrypoint.shports:- 10002:10002restart: alwaysentrypoint:# 跟dockerfile中的cmd比较类似都是容器启动后的默认操作–替代dockerfile中的cmd/im/bin/entrypoint.sh -h 10.0.0.235 -p 2379 -c /im/bin/file_server -flagfile/im/conf/file_server.confdepends_on:- etcdfriend_server:build: ./friend#image: file-server:v1container_name: friend_server-servicevolumes:# 1. 希望容器内的程序能够访问宿主机上的文件# 2. 希望容器内程序运行所产生的数据文件能落在宿主机上# 挂载的信息 entrypoint.sh文件 数据目录(im/logs, im/data), 配置文件- ./conf/friend_server.conf:/im/conf/friend_server.conf- ./middle/data/logs:/im/logs:rw- ./middle/data/data:/im/data:rw- ./entrypoint.sh:/im/bin/entrypoint.shports:- 10006:10006restart: alwaysdepends_on:- etcd- mysql- elasticsearchentrypoint:# 跟dockerfile中的cmd比较类似都是容器启动后的默认操作–替代dockerfile中的cmd/im/bin/entrypoint.sh -h 10.0.0.235 -p 2379,3306,9200 -c /im/bin/friend_server -flagfile/im/conf/friend_server.confgateway_server:build: ./gateway#image: file-server:v1container_name: gateway_server-servicevolumes:# 1. 希望容器内的程序能够访问宿主机上的文件# 2. 希望容器内程序运行所产生的数据文件能落在宿主机上# 挂载的信息 entrypoint.sh文件 数据目录(im/logs, im/data), 配置文件- ./conf/gateway_server.conf:/im/conf/gateway_server.conf- ./middle/data/logs:/im/logs:rw- ./middle/data/data:/im/data:rw- ./entrypoint.sh:/im/bin/entrypoint.shports:- 9000:9000- 9001:9001restart: alwaysdepends_on:- etcd- redisentrypoint:# 跟dockerfile中的cmd比较类似都是容器启动后的默认操作–替代dockerfile中的cmd/im/bin/entrypoint.sh -h 10.0.0.235 -p 2379,6379 -c /im/bin/gateway_server -flagfile/im/conf/gateway_server.confmessage_server:build: ./message#image: file-server:v1container_name: message_server-servicevolumes:# 1. 希望容器内的程序能够访问宿主机上的文件# 2. 希望容器内程序运行所产生的数据文件能落在宿主机上# 挂载的信息 entrypoint.sh文件 数据目录(im/logs, im/data), 配置文件- ./conf/message_server.conf:/im/conf/message_server.conf- ./middle/data/logs:/im/logs:rw- ./middle/data/data:/im/data:rw- ./entrypoint.sh:/im/bin/entrypoint.shports:- 10005:10005restart: alwaysdepends_on:- etcd- mysql- elasticsearch- rabbitmqentrypoint:# 跟dockerfile中的cmd比较类似都是容器启动后的默认操作–替代dockerfile中的cmd/im/bin/entrypoint.sh -h 10.0.0.235 -p 2379,3306,9200,5672 -c /im/bin/message_server -flagfile/im/conf/message_server.confspeech_server:build: ./speech#image: file-server:v1container_name: speech_server-servicevolumes:# 1. 希望容器内的程序能够访问宿主机上的文件# 2. 希望容器内程序运行所产生的数据文件能落在宿主机上# 挂载的信息 entrypoint.sh文件 数据目录(im/logs, im/data), 配置文件- ./conf/speech_server.conf:/im/conf/speech_server.conf- ./middle/data/logs:/im/logs:rw- ./middle/data/data:/im/data:rw- ./entrypoint.sh:/im/bin/entrypoint.shports:- 10001:10001restart: alwaysdepends_on:- etcdentrypoint:# 跟dockerfile中的cmd比较类似都是容器启动后的默认操作–替代dockerfile中的cmd/im/bin/entrypoint.sh -h 10.0.0.235 -p 2379 -c /im/bin/speech_server -flagfile/im/conf/speech_server.conftransmite_server:build: ./transmite#image: file-server:v1container_name: transmite_server-servicevolumes:# 1. 希望容器内的程序能够访问宿主机上的文件# 2. 希望容器内程序运行所产生的数据文件能落在宿主机上# 挂载的信息 entrypoint.sh文件 数据目录(im/logs, im/data), 配置文件- ./conf/transmite_server.conf:/im/conf/transmite_server.conf- ./middle/data/logs:/im/logs:rw- ./middle/data/data:/im/data:rw- ./entrypoint.sh:/im/bin/entrypoint.shports:- 10004:10004restart: alwaysdepends_on:- etcd- mysql- rabbitmqentrypoint:# 跟dockerfile中的cmd比较类似都是容器启动后的默认操作–替代dockerfile中的cmd/im/bin/entrypoint.sh -h 10.0.0.235 -p 2379,3306,5672 -c /im/bin/transmite_server -flagfile/im/conf/transmite_server.confuser_server:build: ./user#image: file-server:v1container_name: user_server-servicevolumes:# 1. 希望容器内的程序能够访问宿主机上的文件# 2. 希望容器内程序运行所产生的数据文件能落在宿主机上# 挂载的信息 entrypoint.sh文件 数据目录(im/logs, im/data), 配置文件- ./conf/user_server.conf:/im/conf/user_server.conf- ./middle/data/logs:/im/logs:rw- ./middle/data/data:/im/data:rw- ./entrypoint.sh:/im/bin/entrypoint.shports:- 10003:10003restart: alwaysdepends_on:- etcd- mysql- redis- elasticsearchentrypoint:# 跟dockerfile中的cmd比较类似都是容器启动后的默认操作–替代dockerfile中的cmd/im/bin/entrypoint.sh -h 10.0.0.235 -p 2379,3306,5672,9200 -c /im/bin/user_server -flagfile/im/conf/user_server.conf6. 服务端总结 1消息存储子服务 2好友管理子服务 3网关子服务 4docket部署 5项目总结
    客户端整体代码链接https://gitee.com/liu-yechi/new_code/tree/master/chat_system/server。