Bootstrap

微服务即时通讯系统的实现(服务端)----(3)

目录

1. 消息存储子服务的实现

1.1 功能设计

(1)消息存储子服务,主要用于管理消息的存储:

  • 文本消息,储存在 ElasticSearch 文档搜索服务中。
  • 文件/语音/图片,需要转储到文件管理子服务中。

(2)除了管理消息的存储,还需要管理消息的搜索获取,因此需要对外提供以下接口:

  1. 获取历史消息
    • 获取最近 N 条消息:用于登录成功后,点击对方头像打开聊天框时显示最近的消息。
    • 获取指定时间段内的消息:用户可以进行聊天消息的按时间搜索。
  2. 关键字消息搜索:用户可以针对指定好友的聊天进行聊天消息的关键字搜索。

1.2 模块划分

(1)以下是消息存储模块划分:

  1. 参数/配置文件解析模块:基于 gflags 框架直接使用进行参数/配置文件解析。
  2. 日志模块:基于 spdlog 框架封装的模块直接使用进行日志输出。
  3. 服务注册模块:基于 etcd 框架封装的注册模块直接使用,进行聊天消息存储子服务的注册。
  4. 数据库数据操作模块:基于 odb-mysql 数据管理封装的模块,进行数据库数据操作,用于从 MQ 中消费到消息后,向数据库中存储一份,以便于通过时间进行范围性查找。从数据库根据指定用户的所有好友信息。
  5. rpc 服务模块:基于 brpc 框架搭建 rpc 服务器。
  6. 服务发现与调用模块:基于 etcd 框架与 brpc 框架封装的服务发现与调用模块,
    • 连接文件管理子服务:将文件/语音/图片类型的消息以及用户头像之类的文件数据转储到文件管理子服务。
    • 连接用户管理子服务:在消息搜索时,根据发送用户的 ID 获取发送者用户信息。
  7. ES 客户端模块:基于 elasticsearch 框架实现访问客户端,向 es 服务器进行文本聊天消息的存储,以便于文本消息的关键字搜索。
  8. MQ 消费模块:基于 rabbitmq-client 封装的消费者模块从消息队列服务器消费获取聊天消息,将文本消息存储到 ElasticSearch 服务,将文件消息转储到文件管理子服务,所有消息的简息都需要向数据库存储一份。

1.3 模块功能示意图

(1)如下是模块功能图:

1.4 数据管理

1.4.1 数据库消息管理

(1)在消息的存储管理中,所有的消息简息都要在数据库中存储一份,进行消息的持久化,以便于进行时间范围性查询和离线消息的实现。消息类型有四种:文本,文件,语音,图片。我们不可能将文件数据也存储到数据库中,因此数据库中只存储文本消息和其他类型消息的元信息即可。

(2)消息存储数据库表结构:

  1. 消息 ID:唯一标识。
  2. 消息产生时间:用于进行时间性搜索。
  3. 消息发送者用户 ID:明确消息的发送者。
  4. 消息产生会话 ID:明确消息属于哪个会话。
  5. 消息类型:明确消息的类型。
  6. 消息内容:只存储文本消息;文件/语音/图片数据不进行存储,或者说是存储在文件子服务中。
  7. 文件 ID:只有文件/语音/图片类消息会用到。
  8. 文件大小:只有文件/语音/图片类消息会用到。
  9. 文件名称:只有文件类消息会用到。

(3)数据库操作:

  1. 新增消息。
  2. 通过消息 ID 获取消息信息。
  3. 通过会话 ID,时间范围,获取指定时间段之内的消息,并按时间进行排序。
  4. 通过会话 ID,消息数量,获取最近的 N 条消息(逆序+limit 即可)。

(4)ODB映射数据结构message.hxx的实现:

#pragma once
#include <string>
#include <cstddef> 
#include <odb/nullable.hxx>
#include <odb/core.hxx>
#include <boost/date_time/posix_time/posix_time.hpp>

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

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

(5)运行如下命令可以通过odb生成mysql代码:

odb -d mysql --std c++11 --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)
 ENGINE=InnoDB;

CREATE UNIQUE INDEX `message_id_i`
  ON `message` (`message_id`);

CREATE INDEX `session_id_i`
  ON `message` (`session_id`);

1.4.2 ES文本消息管理

(1)因为当前聊天室项目中,实现了聊天内容的关键字搜索功能,但是如果在数据库中进行关键字的模糊匹配,则效率会非常低,因此采用 ES 进行消息内容存储与搜索,但是在搜索的时候需要进行会话的过滤,因此这里也要考虑 ES 索引的构造。

(2)对消息ES的封装:

#include "icsearch.hpp"
#include "message.hxx"

namespace MyTest
{
	class ESMessage
    {
    public:
        using ptr = std::shared_ptr<ESMessage>;

        ESMessage(const std::shared_ptr<elasticlient::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::vector<bite_im::Message> search(const std::string &key, const std::string &ssid)
        {
            std::vector<bite_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_ptr<elasticlient::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条消息获取接口实现

  1. 从请求中,获取会话 ID, 要获取的消息数量。
  2. 访问数据库,从数据库中按时间排序,获取指定数量的消息简略信息(消息 ID,会话 ID,消息类型,产生时间,发送者用户 ID,文本消息内容,文件消息元信息)。
  3. 循环构造完整消息(从用户子服务获取消息的发送者用户信息,从文件子服务获取文件/语音/图片数据)。
  4. 组织响应返回给网关服务器。
namespace MyTest
{
    class MessageServiceImpl : public bite_im::MsgStorageService
    {
    public:
        MessageServiceImpl(
            const std::shared_ptr<elasticlient::Client> &es_client,
            const std::shared_ptr<odb::core::database> &mysql_client,
            const ServiceManager::ptr &channel_manager,
            const std::string &file_service_name,
            const std::string &user_service_name) 
            :_es_message(std::make_shared<ESMessage>(es_client))
            ,_mysql_message(std::make_shared<MessageTable>(mysql_client))
            ,_file_service_name(file_service_name)
            ,_user_service_name(user_service_name)
            ,_mm_channels(channel_manager)
        {
            _es_message->createIndex();
        }

        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](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_lsits)
            {
                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("消息类型错误!!");
                    break;
                }
            }
        }
        
    private:
    	bool _GetUser(const std::string &rid,
            const std::unordered_set<std::string> &user_id_lists,
            std::unordered_map<std::string, UserInfo> &user_lists)
        {
            auto channel = _mm_channels->choose(_user_service_name);
            if(!channel) 
            {
                LOG_ERROR("{} 没有可供访问的文件子服务节点!",  _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_set<std::string> &file_id_lists,
            std::unordered_map<std::string, std::string> &file_data_lists)
        {
            auto channel = _mm_channels->choose(_file_service_name);
            if(!channel) 
            {
                LOG_ERROR("{} 没有可供访问的文件子服务节点!",  _file_service_name);
                return false;
            }

            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 指定时间段消息搜索接口实现

  1. 从请求中,获取会话 ID, 要获取的消息的起始时间与结束时间。
  2. 访问数据库,从数据库中按时间进行范围查询,获取消息简略信息(消息 ID,会话 ID,消息类型,产生时间,发送者用户 ID,文本消息内容,文件消息元信息)。
  3. 循环构造完整消息(从用户子服务获取消息的发送者用户信息,从文件子服务获取文件/语音/图片数据)。
  4. 组织响应返回给网关服务器。
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](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_lsits.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_lsits)
            {
                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("消息类型错误!!");
                    break;
                }
            }
        }
    };
}

1.5.4 关键字消息搜索接口实现

  1. 从请求中,获取会话 ID, 搜索关键字。
  2. 基于封装的 ES 客户端,访问 ES 服务器进行文本消息搜索(以消息内容进行搜索,以会话 ID 进行过滤),从 ES 服务器获取到消息简息(消息 ID,会话 ID, 文本消息内容)。
  3. 循环从数据库根据消息 ID 获取消息简息(消息 ID,消息类型,会话 ID,发送者ID,产生时间,文本消息内容,文件消息元数据)。
  4. 循环从用户子服务获取所有消息的发送者用户信息,构造完整消息。
  5. 组织响应返回给网关服务器。
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](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());
            }
        }
        
    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_ptr<MessageServer>;
        MessageServer(const MQClient::ptr &mq_client,
                      const Discovery::ptr service_discoverer,
                      const Registry::ptr &reg_client,
                      const std::shared_ptr<elasticlient::Client> &es_client,
                      const std::shared_ptr<odb::core::database> &mysql_client,
                      const std::shared_ptr<brpc::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_ptr<elasticlient::Client> _es_client;
        std::shared_ptr<odb::core::database> _mysql_client;
        std::shared_ptr<brpc::Server> _rpc_server;
    };
}

(2)创建工厂类MessageServerBuilder来实现用户子服务的创建以及Rpc服务器的创建:

namespace MyTest
{
	class MessageServerBuilder
    {
    public:
        //构造es客户端对象
        void make_es_object(const std::vector<std::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_shared<ServiceManager>();
            _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_shared<Discovery>(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_shared<Registry>(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_shared<MQClient>(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_shared<MessageServer>(
                _mq_client, _service_discoverer, _registry_client,
                _es_client, _mysql_client, _rpc_server);

            return server;
        }

    private:
        Registry::ptr _registry_client;

        std::shared_ptr<elasticlient::Client> _es_client;
        std::shared_ptr<odb::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_ptr<brpc::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. 如果是一个图片/语音/文件消息,则取出数据存储到文件子服务中,并获取文件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("消息类型错误!");
                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;
}

(5)cmake构建代码:

# 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_BUILD
            COMMAND protoc
            ARGS --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_BUILD
            COMMAND odb
            ARGS -d mysql --std c++11 --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.cxx
    list(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)好友管理子服务,主要用于管理好友相关的数据与操作,因此主要负责以下接口:

  1. 好友列表的获取:当用户登录成功之后,获取自己好友列表进行展示。
  2. 申请好友:搜索用户之后,点击申请好友,向对方发送好友申请。
  3. 待处理申请的获取:当用户登录成功之后,会获取离线的好友申请请求以待处理。
  4. 好友申请的处理:针对收到的好友申请进行同意/拒绝的处理。
  5. 删除好友:删除当前好友列表中的好友。
  6. 用户搜索:可以进行用户的搜索用于申请好友。
  7. 聊天会话列表的获取:每个单人/多人聊天都有一个聊天会话,在登录成功后可以获取聊天会话,查看历史的消息以及对方的各项信息。
  8. 多人聊天会话的创建:单人聊天会话在对方同意好友时创建,而多人会话需要调用该接口进行手动创建。
  9. 聊天成员列表的获取:多人聊天会话中,可以点击查看群成员按钮,查看群成员信息。

2.2 模块划分

(2)以下是该服务的模块划分:

  1. 参数/配置文件解析模块:基于 gflags 框架直接使用进行参数/配置文件解析。
  2. 日志模块:基于 spdlog 框架封装的模块直接使用进行日志输出。
  3. 服务注册模块:基于 etcd 框架封装的注册模块直接使用,进行聊天消息存储子服务的注册。
  4. 数据库数据操作模块:基于 odb-mysql 数据管理封装的模块,实现数据库中数据的操作:
    • 申请好友的时候,根据数据库中的数据判断两人是否已经是好友关系。
    • 申请好友的时候,根据数据库中的数据判断是否已经申请过好友。
    • 申请好友的时候,针对两位用户 ID 建立好友申请事件信息。
    • 好友信息处理的时候,找到申请事件,进行删除。
    • 获取待处理好友申请事件的时候,从数据库根据用户 ID 查询出所有的申请信息。
    • 同意好友申请的时候,需要创建单聊会话,向数据库中插入会话信息。
    • 从数据库根据指定用户 ID 获取所有好友 ID。
    • 创建群聊的时候,需要创建群聊会话,向数据库中插入会话信息。
    • 查看群聊成员的时候,从数据库根据会话 ID 获取所有会话成员 ID。
    • 获取会话列表的时候,从数据库根据用户 ID 获取到所有会话信息。
    • 删除好友的时候,从数据库中删除两人的好友关系,以及单聊会话,以及会
      话成员信息。
  5. rpc 服务模块:基于 brpc 框架搭建 rpc 服务器。
  6. rpc 服务发现与调用模块:基于 etcd 框架与 brpc 框架封装的服务发现与调用模块:
    • 连接用户管理子服务:获取好友列表,会话成员,好友申请事件的时候获取用户信息。
    • 连接消息管理子服务:在打开聊天会话的时候,需要获取最近的一条消息进行展示。
  7. ES 客户端模块:基于 elasticsearch 框架实现访问客户端,从 es 服务器进行用户的关键字搜索(用户信息由用户子服务在用户注册的时候添加进去)。

2.3 功能模块示意图

(1)如下是模块功能图:

2.4. 数据库数据管理

根据好友相关操作分析,好友操作相关所需要有以下数据表:

2.4.1 用户关系表的实现

(1)用户信息表和关系表:

  • 用户信息表由用户操作服务进行创建,并在用户注册时添加数据,好友这里只进行查询:通过用户 ID 获取详细用户信息。

  • 因为本身用户服务器已经管理了用户个人信息,因此没必要再整一份用户信息出来,,也因为当前用户之间只有好友关系(目前未实现:黑名单,陌生人…),因此这里是一个好友关系表,表示谁和谁是好友

  • 包含的字段:

    • ID:作为主键
    • 用户 ID:
    • 好友 ID:
  • 需要注意的是两个用户结为好友时,需要添加 (1,2),(2,1) 两条数据

  • 提供的操作:

    • 新增用户关系:新增好友,通常伴随着新增会话,新增会话伴随着新增会话成员
    • 移除用户关系:移除好友,通常伴随着移除会话,移除会话伴随着移除会话成员
    • 判断两人是否是好友关系
    • 以用户 ID 获取用户的所有好友 ID
    • 与用户表连接,以用户 ID 获取所有好友详细信息

(2)ODB映射数据结构relation.hxx的实现:

#pragma once
#include <string>
#include <cstddef> 
#include <odb/nullable.hxx>
#include <odb/core.hxx>

namespace 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 auto
        unsigned long _id;
        #pragma db type("varchar(64)") index
        std::string _user_id;
        #pragma db type("varchar(64)")
        std::string _peer_id;
    };
}

(3)运行如下命令可以通过odb生成mysql代码:

odb -d mysql --std c++11 --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)
 ENGINE=InnoDB;

CREATE INDEX `user_id_i`
  ON `relation` (`user_id`);

2.4.2 会话信息表的实现

(1)在多人聊天中,舍弃了群的概念,添加了聊天会话的概念,因为会话既可以是两人单聊会话,也可以是多人聊天会话,这样就可以统一管理了。

  • 包含字段:
    • ID:作为主键
    • 会话 ID:会话标识
    • 会话名称: 单聊会话则设置为’单聊会话’或直接为空就行,因为单聊会话名称就是对方名称,头像就是对方头像
    • 会话类型: SINGLE-单聊 / GROUP-多人(单聊由服务器在同意好友时创建,多人由用户申请创建)
  • 提供的操作:
    • 新增会话:向会话成员表中新增会话成员信息和向会话表中新增会话信息。
    • 删除会话:删除会话成员表中的所有会话成员信息和删除会话表中的会话信息。
    • 通过会话 ID,获取会话的详细信息。
    • 通过用户 ID 获取所有的好友单聊会话(连接会话成员表和用户表)所需字段:
      • 会话 ID。
      • 会话名称:好友的昵称。
      • 会话类型:单聊类型。
      • 会话头像 ID:好友的头像 ID。
      • 好友 ID。
    • 通过用户 ID 获取所有自己的群聊会话(连接会话成员表和用户表)所需字段:
      • 会话 ID。
      • 会话名称。
      • 会话类型:群聊类型。

(2)ODB映射数据结构chat_session.hxx的实现:

#pragma once
#include <string>
#include <cstddef> 
#include <odb/nullable.hxx>
#include <odb/core.hxx>
#include "chat_session_member.hxx"

namespace 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 auto
        unsigned long _id;
        #pragma db type("varchar(64)") index unique
        std::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_type==1 && csm1.user_id=uid && 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_type==2 && csm.user_id=uid
    #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 c++11 --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)
 ENGINE=InnoDB;

CREATE UNIQUE INDEX `chat_session_id_i`
  ON `chat_session` (`chat_session_id`);

2.4.3 会话成员表的实现

(1)每个会话中都会有两个及以上的成员,只有两个成员的会话是单聊会话,超过两个是多人聊天会话,为了明确哪个用户属于哪个会话,或者说会话中有哪些成员,因此需要有会话成员的数据管理:

  • 包含字段:
    • ID:作为主键。
    • 会话 ID:会话标识。
    • 用户 ID:用户标识。
  • 有了这张表就可以轻松的找出哪个用户属于哪个会话了,也可以根据会话 ID 获取所有成员 ID。
  • 提供的操作:
  • 向指定会话中添加单个成员。
  • 向指定会话中添加多个成员。
  • 从指定会话中删除单个成员。
  • 通过会话 ID,获取会话的所有成员 ID。
  • 删除会话所有成员:在删除会话的时候使用。

(2)ODB映射数据结构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 auto
        unsigned long _id;
        #pragma db type("varchar(64)") index
        std::string _session_id;
        #pragma db type("varchar(64)")
        std::string _user_id;
    };
}

(3)运行如下命令可以通过odb生成mysql代码:

odb -d mysql --std c++11 --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)
 ENGINE=InnoDB;

CREATE INDEX `session_id_i`
  ON `chat_session_member` (`session_id`);

2.4.4 好友申请事件表的实现

(1)在好友的操作中有个操作需要额外的管理,那就是申请好友的事件,因为用户 A 申请用户 B 为好友,并非一次性完成,需要用户 B 对本次申请进行处理,同意后才算是一次完整的流程。而在两次操作之间我们就需要为两次操作建立起相匹配的关系映射。

  • 包含字段:
    • ID:作为主键。
    • 事件 ID。
    • 请求者用户 ID。
    • 响应者用户 ID。
    • 状态:用于表示本次请求的处理阶段,其包含三种状态:待处理-todo,同意-accept,拒绝-reject。
  • 提供的操作:
  • 新增好友申请事件:申请的时候新增。
  • 删除好友申请事件:处理完毕(同意/拒绝)的时候删除。
  • 获取指定用户的所有待处理事件及关联申请者用户信息(连接用户表)。

(2)ODB映射数据结构friend_apply.hxx的实现:

#pragma once
#include <string>
#include <cstddef> 
#include <odb/core.hxx>

namespace 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 auto
        unsigned long _id;
        #pragma db type("varchar(64)") index unique
        std::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 c++11 --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)
 ENGINE=InnoDB;

CREATE UNIQUE INDEX `event_id_i`
  ON `friend_apply` (`event_id`);

CREATE INDEX `user_id_i`
  ON `friend_apply` (`user_id`);

CREATE INDEX `peer_id_i`
  ON `friend_apply` (`peer_id`);

2.5 ES用户信息管理

(1)对ES的用户消息封装在前面的用户管理子服务模块当中已经实现了,具体封装实现见博客:https://blog.csdn.net/m0_65558082/article/details/144042168?spm=1001.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;          // 请求标识ID
    optional string user_id = 2;    // 当前请求的发起者用户ID
    optional 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; //当前用户ID
    optional 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;//申请人id
    string 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;//通知事件id
    bool agree = 3;//是否同意好友申请
    string apply_user_id = 4; //申请人的用户id
    optional 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;
    //需要注意的是,这个列表中也必须包含创建者自己的用户ID
    repeated 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)获取好友列表:

  1. 获取请求中的用户 ID。
  2. 根据用户 ID,从数据库的好友关系表&用户表中取出该用户所有的好友简息。
  3. 根据好友简息中的好友头像 ID,批量获取头像数据,组织完整用户信息结构。
  4. 组织响应,将好友列表返回给网关。
#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_ptr<elasticlient::Client> &es_client,
            const std::shared_ptr<odb::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_shared<ESUser>(es_client))
            ,_mysql_apply(std::make_shared<FriendApplyTable>(mysql_client))
            ,_mysql_chat_session(std::make_shared<ChatSessionTable>(mysql_client))
            ,_mysql_chat_session_member(std::make_shared<ChatSessionMemeberTable>(mysql_client))
            ,_mysql_relation(std::make_shared<RelationTable>(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](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 uid = request->user_id();

            //2. 从数据库中查询获取用户的好友ID
            auto friend_id_lists = _mysql_relation->friends(uid);
            std::unordered_set<std::string> user_id_lists;
            for(auto &id : friend_id_lists) 
            {
                user_id_lists.insert(id);
            }

            //3. 从用户子服务批量获取用户信息
            std::unordered_map<std::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_set<std::string> &uid_list,
            std::unordered_map<std::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)申请添加好友:

  1. 取出请求中的请求者 ID,和被请求者 ID。
  2. 判断两人是否已经是好友。
  3. 判断该用户是否已经申请过好友关系。
  4. 向好友申请事件表中,新增申请信息。
  5. 组织响应,将事件 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](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 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)获取待处理好友申请事件:

  1. 取出请求中的用户 ID。
  2. 根据用户 ID,从申请事件表&用户表中找到该用户所有状态为 PENDING 的待处理事件关联申请人用户简息。
  3. 根据申请人用户头像 ID,从文件存储子服务器获取所有用户头像信息,组织用户信息结构。
  4. 组织响应,将申请事件列表响应给网关。
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](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 uid = request->user_id();

            //2. 从数据库获取待处理的申请事件信息 --- 申请人用户ID列表
            auto res = _mysql_apply->applyUsers(uid);
            std::unordered_set<std::string> user_id_lists;
            for(auto &id : res) 
            {
                user_id_lists.insert(id);
            }

            //3. 批量获取申请人用户信息、
            std::unordered_map<std::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)处理好友申请:

  1. 取出请求中的申请人 ID,和被申请人 ID,以及处理结果
  2. 根据两人 ID 在申请事件表中查询判断是否存在申请事件
  3. 判断两人是否已经是好友(互相加好友的情况)
  4. 不管拒绝还是同意,删除申请事件表中的事件信息(该事件处理完毕)
  5. 若同意申请,则向用户关系表中添加好友关系数据,向会话表中新增会话信息,向会话成员表中新增成员信息
  6. 组织响应,将新生成的会话 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](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;处理结果;事件ID
            std::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::vector<ChatSessionMember> 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)删除好友:

  1. 取出请求中的删除者 ID 和被删除者 ID。
  2. 从用户好友关系表中删除相关关系数据,从会话表中删除单聊会话,从会话成员表中删除会话成员信息。
  3. 组织响应,返回给网关。
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](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 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)搜索好友:

  1. 取出请求中的用户 ID,和搜索关键字。
  2. 从好友关系表中取出该用户所有好友 ID。
  3. 根据关键字从 ES 服务器中进行用户搜索,搜索的时候需要将关键字作为用户 ID/手机号/昵称的搜索关键字进行搜索,且需要根据自己的 ID 和好友 ID 过滤掉自己和自己的好友。
  4. 根据搜索到的用户简息中的头像 ID,从文件服务器批量获取用户头像数据。
  5. 组织响应,将搜索到的用户列表响应给网关。
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](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 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_set<std::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_map<std::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)创建会话:

  1. 从请求中取出用户 ID 与会话名称,以及会话的成员 ID 列表。
  2. 生成会话 ID,并向会话表中新增会话信息数据,会话为群聊会话(单聊会话是同意好友申请的时候创建的)。
  3. 向会话成员表中新增所有的成员信息。
  4. 组织响应,将组织好的会话信息响应给网关。
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](const std::string &rid,
                const std::string &errmsg) -> 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::vector<ChatSessionMember> 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)获取会话列表:

  1. 从请求中取出用户 ID。
  2. 根据用户 ID,从会话表&会话成员表&用户表中取出好友的单聊会话列表(会话 ID,好友用户 ID,好友昵称,好友头像 ID),并组织会话信息结构对象。单聊会话中,对方的昵称就是会话名称,对方的头像就是会话头像,会话类型为单聊类型。
  3. 根据单聊会话 ID,从消息存储子服务获取会话的最后一条消息。
  4. 根据好友头像 ID,从文件存储子服务批量获取好友头像数据。
  5. 组织好单聊会话结构数据。
  6. 根据用户 ID,从会话表&会话成员表中取出群聊会话列表(会话 ID,会话名称)。
  7. 根据群聊会话 ID,从消息存储子服务获取会话的最后一条消息。
  8. 组织好群聊会话结构数据。
  9. 将单聊会话数据和群聊会话数据组织到一起,响应给网关。
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](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 uid = request->user_id();

            //2. 从数据库中查询出用户的单聊会话列表
            auto sf_list = _mysql_chat_session->singleChatSession(uid);

            //  1). 从单聊会话列表中,取出所有的好友ID,从用户子服务获取用户信息
            std::unordered_set<std::string> users_id_list;
            for(const auto &f : sf_list) 
            {
                users_id_list.insert(f.friend_id);
            }

            std::unordered_map<std::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)获取会话成员:

  1. 取出请求中用户 ID,和会话 ID
  2. 根据会话 ID,从会话成员表&用户表中取出所有的成员用户信息
  3. 根据成员信息中的头像 ID,从文件存储子服务批量获取头像数据组织用户信息结
  4. 组织响应,将会话的成员用户信息列表响应给网关
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](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 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_set<std::string> uid_list;
            for(const auto &id : member_id_lists) 
            {
                uid_list.insert(id);
            }

            //3. 从用户子服务批量获取用户信息
            std::unordered_map<std::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_ptr<FriendServer>;

        FriendServer(const Discovery::ptr service_discoverer, 
            const Registry::ptr &reg_client,
            const std::shared_ptr<elasticlient::Client> &es_client,
            const std::shared_ptr<odb::core::database> &mysql_client,
            const std::shared_ptr<brpc::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_ptr<elasticlient::Client> _es_client;
        std::shared_ptr<odb::core::database> _mysql_client;
        std::shared_ptr<brpc::Server> _rpc_server;
    };
}

(2)创建工厂类MessageServerBuilder来实现用户子服务的创建以及Rpc服务器的创建:

namespace MyTest
{
	class FriendServerBuilder
    {
    public:
        // 构造es客户端对象
        void make_es_object(const std::vector<std::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_shared<ServiceManager>();
            _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_shared<Discovery>(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_shared<Registry>(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_shared<brpc::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_shared<FriendServer>(
                _service_discoverer, _registry_client,
                _es_client, _mysql_client, _rpc_server);
            return server;
        }

    private:
        Registry::ptr _registry_client;

        std::shared_ptr<elasticlient::Client> _es_client;
        std::shared_ptr<odb::core::database> _mysql_client;

        std::string _user_service_name;
        std::string _message_service_name;
        ServiceManager::ptr _mm_channels;
        Discovery::ptr _service_discoverer;

        std::shared_ptr<brpc::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;
}

(4)cmake构建代码:

# 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_BUILD
            COMMAND protoc
            ARGS --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_BUILD
            COMMAND odb
            ARGS -d mysql --std c++11 --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.cxx
    list(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)以下是模块划分:

  1. 参数/配置文件解析模块:基于 gflags 框架直接使用进行参数/配置文件解析。
  2. 日志模块:基于 spdlog 框架封装的模块直接使用进行日志输出。
  3. rpc 服务发现与调用模块:基于 etcd 框架与 brpc 框架封装的服务发现与调用模块。因为要分发处理所有请求,因此所有的子服务都需要进行服务发现。
  4. redis 客户端模块:基于 redis++封装的客户端进行内存数据库数据操作。根据用户子服务添加的会话信息进行用户连接身份识别与鉴权。
  5. HTTP 通信服务器模块:基于 cpp-httplib 库搭建 HTTP 服务器,接收 HTTP 请求进行业务处理。
  6. WEBSOCKET 服务器模块:基于 Websocketpp 库,搭建 websocket 服务器,进行事件通知。
  7. 客户端长连接管理模块:建议用户 ID 与长连接句柄映射关系,便于后续根据用户ID 找到连接进行事件通知

3.3 模块功能示意图

(1)如下是模块功能图:

3.4 接口的实现

(1)用户名注册:

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 查找用户子服务
  3. 调用子服务对应接口进行业务处理
  4. 将处理结果响应给客户端。

(2)用户名登录:

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 查找用户子服务
  3. 调用子服务对应接口进行业务处理
  4. 将处理结果响应给客户端。

(3)短信验证码获取:

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 查找用户子服务
  3. 调用子服务对应接口进行业务处理
  4. 将处理结果响应给客户端。

(4)手机号码注册:

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 查找用户子服务
  3. 调用子服务对应接口进行业务处理
  4. 将处理结果响应给客户端。

(5)手机号码登录:

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 查找用户子服务
  3. 调用子服务对应接口进行业务处理
  4. 将处理结果响应给客户端。

(6)用户信息获取:

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
  3. 查找用户子服务
  4. 调用子服务对应接口进行业务处理
  5. 将处理结果响应给客户端。

(7)修改用户头像:

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
  3. 查找用户子服务
  4. 调用子服务对应接口进行业务处理
  5. 将处理结果响应给客户端。

(8)修改用户签名:

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
  3. 查找用户子服务
  4. 调用子服务对应接口进行业务处理
  5. 将处理结果响应给客户端。

(9)修改用户昵称:

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
  3. 查找用户子服务
  4. 调用子服务对应接口进行业务处理
  5. 将处理结果响应给客户端。

(10)修改用户绑定手机号:

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
  3. 查找用户子服务
  4. 调用子服务对应接口进行业务处理
  5. 将处理结果响应给客户端。

(11)获取好友列表 :

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
  3. 查找好友子服务
  4. 调用子服务对应接口进行业务处理
  5. 将处理结果响应给客户端。

(12)发送好友申请 :

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
  3. 查找用户子服务
  4. 根据请求中的用户 ID,调用用户子服务,获取用户的详细信息
  5. 查找好友子服务
  6. 调用子服务对应接口进行业务处理
  7. 若处理成功,则通过被申请人 ID,查找对方长连接。若长连接存在(对方在线),则组织好友申请通知进行事件通知
  8. 将处理结果响应给客户端。

(13)获取待处理好友申请:

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
  3. 查找好友子服务
  4. 调用子服务对应接口进行业务处理
  5. 将处理结果响应给客户端。

(14)好友申请处理 :

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
  3. 查找用户子服务
  4. 根据请求中的用户 ID,调用用户子服务,获取申请人与被申请人的详细信息
  5. 查找好友子服务
  6. 调用子服务对应接口进行业务处理
  7. 若处理成功,则通过申请人 ID,查找申请人长连接,进行申请处理结果的通知
    • 若处理结果是同意,则意味着新聊天会话的创建,则对申请人继续进行聊天
      会话创建通知。则从处理结果中取出会话 ID,使用对方的昵称作为会话名称,对方的头像作为会话头像组织会话信息
    • 若处理结果是同意,则对当前处理者用户 ID 查找长连接,进行聊天会话创建的通知。则从处理结果中取出会话 ID,使用对方的昵称作为会话名称,对方的头像作为会话头像组织会话信息
  • 清理响应中的会话 ID 信息。
  1. 将处理结果响应给客户端

(15)删除好友:

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
  3. 查找好友子服务
  4. 调用子服务对应接口进行业务处理
  5. 若处理成功,则通过被删除者用户 ID,查找对方长连接。若长连接存在(对方在线),则组织好友删除通知进行事件通知
  6. 将处理结果响应给客户端。

(16)搜索用户:

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
  3. 查找好友子服务
  4. 调用子服务对应接口进行业务处理
  5. 将处理结果响应给客户端。

(17)获取用户聊天会话列表:

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
  3. 查找好友子服务
  4. 调用子服务对应接口进行业务处理
  5. 将处理结果响应给客户端。

(18)创建多人聊天会话:

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
  3. 查找好友子服务
  4. 调用子服务对应接口进行业务处理
  5. 若处理成功,循环根据会话成员的 ID 找到他们的长连接,
    • 根据响应中的会话信息,逐个进行会话创建的通知
    • 清理响应中的会话信息
  6. 将处理结果响应给客户端。

(19)获取消息会话成员列表:

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
  3. 查找好友子服务
  4. 调用子服务对应接口进行业务处理
  5. 将处理结果响应给客户端。

(20)发送新消息:

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
  3. 查找消息转发子服务
  4. 调用子服务对应接口进行业务处理
  5. 若处理成功,则根据处理结果中的用户 ID 列表,循环找到目标长连接,根据处理结果中的消息字段组织新消息通知,逐个对目标进行新消息通知。
  6. 若处理失败,则根据处理结果中的错误提示信息,设置响应内容
  7. 将处理结果响应给客户端。

(21)获取指定时间段消息列表:

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
  3. 查找消息存储子服务
  4. 调用子服务对应接口进行业务处理
  5. 将处理结果响应给客户端。

(22)获取最近 N 条消息列表:

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
  3. 查找消息存储子服务
  4. 调用子服务对应接口进行业务处理
  5. 将处理结果响应给客户端。

(23)搜索关键字历史消息:

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
  3. 查找消息存储子服务
  4. 调用子服务对应接口进行业务处理
  5. 将处理结果响应给客户端。

(24)单个文件数据获取:

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
  3. 查找文件子服务
  4. 调用子服务对应接口进行业务处理
  5. 将处理结果响应给客户端。

(25)多个文件数据获取:

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
  3. 查找文件子服务
  4. 调用子服务对应接口进行业务处理
  5. 将处理结果响应给客户端。

(26)单个文件数据上传:

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
  3. 查找文件子服务
  4. 调用子服务对应接口进行业务处理
  5. 将处理结果响应给客户端。

(27)多个文件数据上传:

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
  3. 查找文件子服务
  4. 调用子服务对应接口进行业务处理
  5. 将处理结果响应给客户端。

(28)语音转文字:

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
  3. 查找语音子服务
  4. 调用子服务对应接口进行业务处理
  5. 将处理结果响应给客户端。

(29)具体实现的代码见https://gitee.com/liu-yechi/new_code/tree/master/chat_system/server/gateway/source。

4 Docker的介绍

docker 是一个用 Go 语言实现的应用容器引擎开源项目,可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化。其广泛应用于开发、测试和生产环境中,帮助开发者和系统管理员简化应用的部署和管理,实现快速的交付,测试和部署。

4.1 docker的安装

  1. 安装 docker 依赖:
SQL
sudo apt-get install ca-certificates curl gnupg lsb-release
  1. 配置加速地址:
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"
 	] 
}
EOF

sudo systemctl daemon-reload
  1. 添加 Docker 官方 GPG 密钥:
SQL
curl -fsSL http://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg | sudo apt-key add -
  1. 添加 Docker 软件源:
SQL
sudo add-apt-repository "deb [arch=amd64] http://mirrors.aliyun.com/docker-ce/linux/ubuntu $(lsb_release -cs) stable"
  1. 安装 Docker:
SQL
sudo apt-get install docker-ce docker-ce-cli containerd.io
  1. 安装 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
  1. 配置用户组:
SQL
sudo groupadd docker
sudo gpasswd -a $USER docker
newgrp docker
  1. 测试-查看版本:
SQL
docker --version

4.2 docker常用指令

4.2.1 容器操作

(1)查看docker:

Plain Text
docker ps -a
docker container ls -a 
# -a 所有容器
# -q 仅显示容器 ID

(2)删除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_USER=myuser
# -i, --interactive : 即使没有连接,也要保持 STDIN 打开
# -p, --publish list : 设置于宿主机端口映射 -p 3306:3306
# -t, --tty : 申请终端
# -v, --volume list : 设置与宿主机文件挂载 -
v ./data:/var/lib/mysql:rw

(4)停止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:version

(2)查看镜像:

Plain Text
docker images
docker image ls -a

(3)创建镜像:

Plain Text
docker build [options] path
# options:
# -f, --file string : 指定构建镜像的 dockerfile
# -t, --tag stringArray : 设置镜像名称与版本 -t myImage:version

(4)删除镜像:

Plain Text
docker rmi image_id
docker image rm image_id

(5)导入/导出镜像:

Plain Text
docker save -o myimage.tar myimage:latest
docker load -i myimage.tar

(6)缓存及镜像清理:

Plain Text
docker system df
docker system prune -a

4.3 dockerfile编写规则简介

(1)样例:

YAML
FROM ubuntu:22.04
LABEL MAINTAINER="bitejiuyeke"
ENV REDIS_VERSION=5.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.conf"

(2)样例参数解析:

  • FROM : 注明所使用的镜像。
  • LABEL :构建镜像时设置键值对,这里 MAINTAINER 表示维护人。
  • ENV :用于设置镜像中的环境变量。
  • WORKDIR :设定镜像中的工作路径(该目录会在镜像系统中自动创建)。
  • RUN :在镜像中执行的指令。
  • COPY :将宿主机中的文件拷贝到镜像系统指定路径下。
  • EXPOSE :对外暴露端口。
  • CMD : 设置容器启动时默认执行的命令。

4.4 dockercompose编写规则简介

(1)样例:

YAML
version: "3.8"
services:
 mysql:
 image: mysql:8.0.39
 container_name: docker-msyql8-service
 volumes: 
 - ./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: 123456
 ports:
 - 3306:3306
 restart: always
 depends_on:
 - etcd

(2)样例参数解析:

  • version: docker-compose 语法版本。
  • services: 要启动的服务。
  • mysql: 第 4 行的 mysql 为对应的服务名称,在这里其实就是个标识
  • image:来源镜像及版本。
  • container_name:用户设定的容器名称。
  • volumes:挂载卷,其实就是将宿主机上的指定文件或目录与镜像机进行挂载。
  • environment:设定镜像机中的环境变量。
  • ports:宿主机与镜像机的端口映射。
  • restart:容器重启策略,always 表示无论退出状态如何,容器总是重新启动。
  • depends_on:启动依赖,通过依赖关系控制容器启动顺序。

5. 项目部署

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 $2
    cp -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/depends

cp /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/depends

(3)参数解析:

  • 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.conf

(2)好友管理子服务:

# 声明基础镜像来源
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.conf

(3)入口网关子服务:

 声明基础镜像来源
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.conf

(4)消息存储子服务:

# 声明基础镜像来源
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.conf

(5)语言识别子服务:

# 声明基础镜像来源
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.conf

(6)消息转发子服务:

# 声明基础镜像来源
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.conf

(7)用户管理子服务:

# 声明基础镜像来源
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.conf

5.4 编写entrypoint.sh文件

(1)包含中间件在内,我们共有 13 个服务需要启动,这些服务之间会存在一些依赖关系,比如 user_service 启动之前,必须保证 mysql,redis,etcd 这些中间件已经启动才可以,因此我们需要做一些启动的顺序控制。

(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 
    do
        echo "$2 端口连接失败,休眠等待!";
        sleep 1;
    done
    echo "$1:$2 检测成功!";
}
# 2. 对脚本运行参数进行解析,获取到ip,port,command
declare ip
declare ports
declare command
while getopts "h:p:c:" arg
do
    case $arg in
        h)
            ip=$OPTARG;;
        p)
            ports=$OPTARG;;
        c)
            command=$OPTARG;;
    esac
done
# 3. 通过执行脚本进行端口检测
# ${port //,/ } 针对port中的内容,以空格替换字符串中的,  shell中数组--一种以空格间隔的字符串
for port in ${ports//,/ }
do
    wait_for $ip $port
done
# 4. 执行command
#  eval 对一个字符串进行二次检测,将其当作命令进行执行
eval $command

(3)参数解析:

  • nc 指令:nc(Netcat)是一个功能强大的网络工具, -z 选项用于扫描监听中的守护进程,也就是扫描远程主机上的开放端口。当与 nc 命令一起使用时,-z 选项会让 nc 尝试连接到指定的端口,但不会发送任何数据或接收任何响应。这通常用于端口扫描,以检查远程主机上的端口是否开放。
  • declare:声明变量
  • getopts:捕获运行参数
  • eval:用于将字符串作为命令来执行。

5.5 编写docker-compose文件

(1)包含中间件在内,我们共有 13 个服务需要启动,若一 一都需要手动启动,会比较麻烦,因此我们使用 docker-compose 进行统一管理启动。

version: "3.8"

services:
  etcd:
    image: quay.io/coreos/etcd:v3.3.25
    container_name: etcd-service
    environment:
      - ETCD_NAME=etcd-s1
      - ETCD_DATA_DIR=/var/lib/etcd
      - ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:2379
      - ETCD_ADVERTISE_CLIENT_URLS=http://0.0.0.0:2379
    volumes:
      # 1. 希望容器内的程序能够访问宿主机上的文件
      # 2. 希望容器内程序运行所产生的数据文件能落在宿主机上
      - ./middle/data/etcd:/var/lib/etcd:rw
    ports:
      - 2379:2379
    restart: always
  mysql:
    image: mysql:8.0.39
    container_name: mysql-service
    environment:
      MYSQL_ROOT_PASSWORD: 123456
    volumes:
      # 1. 希望容器内的程序能够访问宿主机上的文件
      # 2. 希望容器内程序运行所产生的数据文件能落在宿主机上
      - ./sql:/docker-entrypoint-initdb.d/:rw
      - ./middle/data/mysql:/var/lib/mysql:rw
    ports:
      - 3306:3306
    restart: always
  redis:
    image: redis:6.0.16
    container_name: redis-service
    volumes:
      # 1. 希望容器内的程序能够访问宿主机上的文件
      # 2. 希望容器内程序运行所产生的数据文件能落在宿主机上
      - ./middle/data/redis:/var/lib/redis:rw
    ports:
      - 6379:6379
    restart: always
  elasticsearch:
    image: elasticsearch:7.17.21
    container_name: elasticsearch-service
    environment:
      - "discovery.type=single-node"
    volumes:
      # 1. 希望容器内的程序能够访问宿主机上的文件
      # 2. 希望容器内程序运行所产生的数据文件能落在宿主机上
      - ./middle/data/elasticsearch:/data:rw
    ports:
      - 9200:9200
      - 9300:9300
    restart: always
  rabbitmq:
    image: rabbitmq:3.9.13
    container_name: rabbitmq-service
    environment:
      RABBITMQ_DEFAULT_USER: root
      RABBITMQ_DEFAULT_PASS: 123456
    volumes:
      # 1. 希望容器内的程序能够访问宿主机上的文件
      # 2. 希望容器内程序运行所产生的数据文件能落在宿主机上
      - ./middle/data/rabbitmq:/var/lib/rabbitmq:rw
    ports:
      - 5672:5672
    restart: always

  file_server:
    build: ./file
    #image: server-user_server
    container_name: file_server-service
    volumes:
      # 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.sh
    ports:
      - 10002:10002
    restart: always
    entrypoint:
      # 跟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.conf"
    depends_on:
      - etcd
  friend_server:
    build: ./friend
    #image: file-server:v1
    container_name: friend_server-service
    volumes:
      # 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.sh
    ports:
      - 10006:10006
    restart: always
    depends_on:
      - etcd
      - mysql
      - elasticsearch
    entrypoint:
      # 跟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.conf"
  gateway_server:
    build: ./gateway
    #image: file-server:v1
    container_name: gateway_server-service
    volumes:
      # 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.sh
    ports:
      - 9000:9000
      - 9001:9001
    restart: always
    depends_on:
      - etcd
      - redis
    entrypoint:
      # 跟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.conf"
  message_server:
    build: ./message
    #image: file-server:v1
    container_name: message_server-service
    volumes:
      # 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.sh
    ports:
      - 10005:10005
    restart: always
    depends_on:
      - etcd
      - mysql
      - elasticsearch
      - rabbitmq
    entrypoint:
      # 跟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.conf"
  speech_server:
    build: ./speech
    #image: file-server:v1
    container_name: speech_server-service
    volumes:
      # 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.sh
    ports:
      - 10001:10001
    restart: always
    depends_on:
      - etcd
    entrypoint:
      # 跟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.conf"
  transmite_server:
    build: ./transmite
    #image: file-server:v1
    container_name: transmite_server-service
    volumes:
      # 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.sh
    ports:
      - 10004:10004
    restart: always
    depends_on:
      - etcd
      - mysql
      - rabbitmq
    entrypoint:
      # 跟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.conf"
  user_server:
    build: ./user
    #image: file-server:v1
    container_name: user_server-service
    volumes:
      # 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.sh
    ports:
      - 10003:10003
    restart: always
    depends_on:
      - etcd
      - mysql
      - redis
      - elasticsearch
    entrypoint:
      # 跟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.conf"

6. 服务端总结

(1)消息存储子服务:

(2)好友管理子服务:

(3)网关子服务:

(4)docket部署:

(5)项目总结:

客户端整体代码链接https://gitee.com/liu-yechi/new_code/tree/master/chat_system/server。

;