【C++项目】微服务即时通讯系统:服务端

🌈 个人主页:Zfox_
🔥 系列专栏:Qt

目录
- 一:🔥 服务器功能设计
- 二:🔥 框架与微服务拆分设计
- 🦋 网关服务
- 🎀 HTTP 通信:
- 🎀 WEBSOCKET 通信:
- 🦋 用户管理子服务
- 🦋 好友管理子服务
- 🦋 文件管理子服务
- 🦋 消息管理子服务
- 🦋 转发管理子服务
- 🦋 语音转换子服务
- 🦋 项目所使用到的框架/库 (点击跳转对应的教学):
- 🦋 项目所使用到的框架/库使用教学
- 🦋 后台服务的模块层次图:
- 🦋 后台服务的通信流程图:
- 🎀 入口网关子服务业务接口:
- 🎀 用户管理子服务业务接口:
- 🎀 好友管理子服务业务接口:
- 三:🔥 微服务通信接口设计
- 🦋 网关服务
- 🦋 网关 HTTP 接口
- 🦋 网关 Websocket 接口:
- 四:🔥 微服务通信实现
- 🦋 语音管理子服务
- 🎀 功能设计
- 🎀 模块划分
- 模块功能示意图:
- 🎀 接口实现流程
- 🦋 文件管理子服务
- 🎀 功能设计
- 🎀 模块划分
- 模块功能示意图
- 🎀 接口实现流程
- 🦋 用户管理子服务
- 🎀 功能设计
- 🎀 模块划分
- 功能模块示意图:
- 🎀 数据管理
- 🎀 内存数据库数据管理:
- 会话信息映射键值对:
- 验证码信息映射键值对:
- 文档数据库数据管理:
- 🎀 接口实现流程
- 🦋 消息转发子服务
- 🎀 功能设计
- 🎀 模块划分
- 功能模块示意图:
- 🎀 接口实现流程
- 🦋 消息存储子服务
- 🎀 功能设计
- 🎀 功能设计模块划分
- 模块功能示意图:
- 🎀 数据管理
- 数据库消息管理:
- 数据库表结构:
- 数据库操作:
- ES 文本消息管理:
- 🎀 接口实现流程
- 最近 N 条消息获取:
- 指定时间段消息搜索:
- 关键字消息搜索:
- 🦋 好友管理子服务
- 🎀 功能设计
- 🎀 模块划分
- 功能模块示意图
- 🎀 数据管理
- 数据库数据管理:
- 用户信息表
- 用户关系表
- 会话信息
- 会话成员
- 好友申请事件
- 🎀 接口实现流程
- 获取好友列表
- 申请添加好友
- 获取待处理好友申请事件
- 处理好友申请
- 删除好友
- 搜索好友
- 创建会话
- 获取会话列表
- 获取会话成员
- 🦋 网关子服务
- 🎀 功能设计
- 模块功能示意图:
- 🎀 接口实现流程:
- 用户名注册
- 用户名登录
- 短信验证码获取
- 手机号码注册
- 手机号码登录
- 用户信息获取
- 修改用户头像
- 修改用户签名
- 修改用户昵称
- 修改用户绑定手机号
- 获取好友列表
- 发送好友申请
- 获取待处理好友申请
- 好友申请处理
- 删除好友
- 搜索用户
- 获取用户聊天会话列表
- 创建多人聊天会话
- 获取消息会话成员列表
- 发送新消息
- 获取指定时间段消息列表
- 获取最近 N 条消息列表
- 搜索关键字历史消息
- 单个文件数据获取
- 多个文件数据获取
- 单个文件数据上传
- 多个文件数据上传
- 语音转文字
- 五:🔥 后台服务器部署
- 🦋 Docker 简介与安装
- 🎀 docker 安装
- 🎀 docker 常用指令
- 容器操作
- 镜像操作
- 🎀 dockerfile 编写规则简介
- docker compose 编写规则简介
- 🦋 项目部署
- 🎀 编写项目配置文件
- 🎀 查询程序依赖
- 🎀 编写 dockerfile
- 🎀 编写 entrypoint.sh
- 🎀 编写 docker-compose
- 六:🔥 共勉
一:🔥 服务器功能设计
在聊天室项目的功能设计中,包含了以下功能:
- 用户注册:用户输入用户名(昵称),以及密码进行用户名的注册
- 用户登录:用户通过用户名和密码进行登录
- 短信验证码获取:当用户通过手机号注册或登录的时候,需要获取短信验证码
- 手机号注册:用户输入手机号和短信验证码进行手机号的用户注册
- 手机号登录:用户输入手机号和短信验证码进行手机号的用户登录
- 用户信息获取:当用户登录之后,获取个人信息进行展示
- 头像修改:设置用户头像
- 昵称修改:设置用户昵称
- 签名修改:设置用户签名
- 手机号修改:修改用户的绑定手机号
- 好友列表的获取:当用户登录成功之后,获取自己好友列表进行展示
- 申请好友:搜索用户之后,点击申请好友,向对方发送好友申请
- 待处理申请的获取:当用户登录成功之后,会获取离线的好友申请请求以待处理
- 好友申请的处理:针对收到的好友申请进行同意/拒绝的处理
- 删除好友:删除当前好友列表中的好友
- 用户搜索:可以进行用户的搜索用于申请好友
- 聊天会话列表的获取:每个单人/多人聊天都有一个聊天会话,在登录成功后可以获取聊天会话,查看历史的消息以及对方的各项信息
- 多人聊天会话的创建:单人聊天会话在对方同意好友时创建,而多人会话需要调用该接口进行手动创建
- 聊天成员列表的获取:多人聊天会话中,可以点击查看群成员按钮,查看群成员信息
- 发送聊天消息:在聊天框输入内容后点击发送,则向服务器发送消息聊天请求
- 获取历史消息:
- 获取最近 N 条消息:用于登录成功后,点击对方头像打开聊天框时显示最近的消息
- 获取指定时间段内的消息:用户可以进行聊天消息的按时间搜索
- 消息搜索:用户可以进行聊天消息的关键字搜索
- 文件的上传
- 单个文件的上传:这个接口基本用于后台部分,收到文件消息后将文件数据转发给文件子服务进行存储
- 多个文件的上传:这个接口基本用于后台部分,收到文件消息后将文件数据转发给文件子服务进行存储
- 文件的下载
- 单个文件的下载:在后台用于获取用户头像文件数据,以及客户端用于获取文件/语音/图片消息的文件数据
- 多个文件的下载:在后台用于大批量获取用户头像数据(比如获取用户列表的时候),以及前端的批量文件下载
- 语音消息的文字转换:客户端进行语音消息的文字转换。
除了以上的与客户端之间交互的功能之外,还包含一些服务器后台内部所需的功能:
- 消息的存储:用于将文本消息进行存储起来,以便于进行消息的搜索,以及离线消息的存储。
- 文件的存储:用于存储用户的头像文件,以及消息中的文件/图片/语音文件数据。
- 各项用户,好友,会话数据的存储管理
二:🔥 框架与微服务拆分设计
该项目在设计的时候采用微服务框架设计,指将一个大的业务拆分称为多个子业务分别在多台不同的机器节点上提供对应的服务,由网关服务统一接收多个客户端的各种不同请求,然后将请求分发到不同的子服务节点上进行处理,获取响应后,再转发给客户端。
微服务架构设计的思想主要包括以下几个方面:
- 服务拆分:将应用程序拆分成多个小型服务,每个服务负责一部分业务功能,具有独立的生命周期和部署。
- 独立部署:每个微服务可以独立于其他服务进行部署、更新和扩展。
- 语言和数据的多样性:不同的服务可以使用不同的编程语言和数据库,根据服务的特定需求进行技术选型。
- 轻量级通信:服务之间通过定义良好的 API 进行通信,通常使用 HTTP / REST、gRPC 等协议。
- 去中心化治理:每个服务可以有自己的开发团队,拥有自己的技术栈和开发流程。
- 弹性和可扩展性:微服务架构支持服务的动态扩展和收缩,以适应负载的变化。
- 容错性:设计时考虑到服务可能会失败,通过断路器、重试机制等手段提高系统的容错性。
- 去中心化数据管理:每个服务管理自己的数据库,数据在服务之间是私有的,这有助于保持服务的独立性。
- 自动化部署:通过持续集成和持续部署(CI/CD)流程自动化服务的构建、测试和部署。
- 监控和日志:对每个服务进行监控和日志记录,以便于跟踪问题和性能瓶颈。
- 服务发现:服务实例可能动态变化,需要服务发现机制来动态地找到服务实例。
- 安全:每个服务需要考虑安全问题,包括认证、授权和数据传输的安全性。
基于微服务的思想,以及聊天室项目的业务功能,将聊天室项目进行服务拆分为以下几个子服务:
🦋 网关服务
网关服务,提供与客户端进行直接交互的作用,用于接收客户端的各项不同的请求,进行用户鉴权通过后,将请求分发到各个不同的子服务进行处理,接收到响应后,发送给客户端。
用户鉴权:客户端在登录成功后,后台会为客户端创建登录会话,并向客户端返回一个登录会话 ID,往后,客户端发送的所有请求中都必须带有对应的会话 ID 进行身份识别,否则视为未登录,不予提供除注册/登录/验证码获取以外的所有服务。
在网关服务中,基于不同的使用目的,向客户端提供两种不同的通信:
🎀 HTTP 通信:
在项目的设计中客户端的大部分业务都是基于请求-响应模式进行的,因此基于便于扩展,设计简单的目的,采用 HTTP 协议作为与客户端进行基础的业务请求的通信协议,在 HTTP 中涵盖了上述所有的功能接口请求。
🎀 WEBSOCKET 通信:
在聊天室项目中,不仅仅包含客户端主动请求的业务,还包含了一些需要服务器主动推送的通知,因为 HTTP 不支持服务器主动推送数据,因此采用 Websocket 协议进行长连接的通信,向客户端发送通知类型的数据。
- 好友申请的通知
- 好友申请处理结果的通知
- 好友删除的通知
- 聊天会话建立的通知
- 聊天新消息的通知
🦋 用户管理子服务
用户管理子服务,主要用于管理用户的数据,以及关于用户信息的各项操作,因此在上述项目功能中,用户子服务需要提供以下接口:
- 用户注册:用户输入用户名(昵称),以及密码进行用户名的注册
- 用户登录:用户通过用户名和密码进行登录
- 短信验证码获取:当用户通过手机号注册或登录的时候,需要获取短信验证码
- 手机号注册:用户输入手机号和短信验证码进行手机号的用户注册
- 手机号登录:用户输入手机号和短信验证码进行手机号的用户登录
- 用户信息获取:当用户登录之后,获取个人信息进行展示
- 头像修改:设置用户头像
- 昵称修改:设置用户昵称
- 签名修改:设置用户签名
- 手机号修改:修改用户的绑定手机号
🦋 好友管理子服务
好友管理子服务,主要用于管理好友相关的数据与操作,因此主要负责以下接口:
- 好友列表的获取:当用户登录成功之后,获取自己好友列表进行展示
- 申请好友:搜索用户之后,点击申请好友,向对方发送好友申请
- 待处理申请的获取:当用户登录成功之后,会获取离线的好友申请请求以待处理
- 好友申请的处理:针对收到的好友申请进行同意/拒绝的处理
- 删除好友:删除当前好友列表中的好友
- 用户搜索:可以进行用户的搜索用于申请好友
- 聊天会话列表的获取:每个单人/多人聊天都有一个聊天会话,在登录成功后可以获取聊天会话,查看历史的消息以及对方的各项信息
- 多人聊天会话的创建:单人聊天会话在对方同意好友时创建,而多人会话需要调用该接口进行手动创建
- 聊天成员列表的获取:多人聊天会话中,可以点击查看群成员按钮,查看群成员信息
🦋 文件管理子服务
文件管理子服务,主要用于管理用户的头像,以及消息中的文件存储,因此需要提供以下接口:
- 文件的上传
- 单个文件的上传:这个接口基本用于后台部分,收到文件消息后将文件数据转发给文件子服务进行存储
- 多个文件的上传:这个接口基本用于后台部分,收到文件消息后将文件数据转发给文件子服务进行存储
- 文件的下载
- 单个文件的下载:在后台用于获取用户头像文件数据,以及客户端用于获取文件 / 语音 / 图片消息的文件数据
- 多个文件的下载:在后台用于大批量获取用户头像数据(比如获取用户列表的时候),以及前端的批量文件下载
🦋 消息管理子服务
消息管理子服务,主要用于管理消息元信息的存储,因此需要提供以下接口:
- 获取历史消息:
- 获取最近 N 条消息:用于登录成功后,点击对方头像打开聊天框时显示最近的消息
- 获取指定时间段内的消息:用户可以进行聊天消息的按时间搜索
- 消息搜索:用户可以进行聊天消息的关键字搜索
🦋 转发管理子服务
转发子服务,主要用于针对一条消息内容,组织消息的 ID 以及各项所需要素,然后告诉网关服务器一条消息应该发给谁。
通常消息都是以聊天会话为基础进行发送的,根据会话找到它的所有成员,这就是转发的目标。
除此之外,转发子服务将收到的消息,放入消息队列中,由文件子服务/消息子服务进行消费存储
- 获取消息转发目标:针对消息内容,组织消息,并告知网关转发目标。
🦋 语音转换子服务
语音转换子服务,用于调用语音识别 SDK,进行语音识别,将语音转为文字后返回给网关。
- 语音消息的文字转换:客户端进行语音消息的文字转换。
🦋 项目所使用到的框架/库 (点击跳转对应的教学):
- gflags:针对程序运行所需的运行参数解析/配置文件解析框架
- gtest:针对程序编写到一定阶段后,进行的单元测试框架
- spdlog:针对项目中进行日志输出的框架
- protobuf:针对项目中的网络通信数据所采用的序列化和反序列化框架
- brpc:项目中的 rpc 调用使用的框架
- redis:高性能键值存储系统,用于项目中进行用户登录会话信息的存储管理
- mysql:关系型数据库系统,用于项目中的业务数据的存储管理
- ODB:项目中 mysql 数据库操作的 ORM 框架(Object-Relational Mapping,对象关系映射)
- Etcd:分布式、高可用的一致性键值存储系统,用于项目中实现服务注册与发现功能的框架
- cpp-httplib:用于搭建简单轻量 HTTP 服务器的框架
- websocketpp:用于搭建 Websocket 服务器的框架
- rabbitMQ:用于搭建消息队列服务器,用于项目中持久化消息的转发消费
- elasticsearch:用于搭建文档存储/搜索服务器,用于项目中历史消息的存储管理
- 语音云平台:采用百度语音识别技术云平台实现语音转文字功能。
- 短信云平台:采用阿里云短信云平台实现手机短信验证码通知功能。
- cmake:项目工程的构建工具。
- docker:项目工程的一键式部署工具。
🦋 项目所使用到的框架/库使用教学
后台服务技术框架图:
🦋 后台服务的模块层次图:
🦋 后台服务的通信流程图:
🎀 入口网关子服务业务接口:
🎀 用户管理子服务业务接口:
🎀 好友管理子服务业务接口:
三:🔥 微服务通信接口设计
因为微服务框架的思想是将业务拆分到不同的节点主机上提供服务,因此主机节点之间的通信就尤为重要,而在进行开发之前,首先要做的就是将通信接口定义出来,这样只要双方遵循约定,即可实现业务往来。
🦋 网关服务
网关负责直接与客户端进行通信,其基础业务请求使用 HTTP 协议进行通信,通知类业务使用 Websocket 协议进行通信,接口定义如下:
🦋 网关 HTTP 接口
HTTP 通信,分为首行,头部和正文三部分,首行中的 URI 明确了业务请求目标,头部进行正文或连接描述,正文中包含请求或响应的内容,在约定的内容中,首先需要定义出来的就是 URI:
//在客户端与网关服务器的通信中,使用HTTP协议进行通信
// 通信时采用POST请求作为请求方法
// 通信时,正文采用protobuf作为正文协议格式,具体内容字段以前边各个文件中定义的字段格式为准
/* 以下是HTTP请求的功能与接口路径对应关系:SERVICE HTTP PATH:{获取随机验证码 /service/user/get_random_verify_code获取短信验证码 /service/user/get_phone_verify_code用户名密码注册 /service/user/username_register用户名密码登录 /service/user/username_login手机号码注册 /service/user/phone_register手机号码登录 /service/user/phone_login获取个人信息 /service/user/get_user_info修改头像 /service/user/set_avatar修改昵称 /service/user/set_nickname修改签名 /service/user/set_description修改绑定手机 /service/user/set_phone获取好友列表 /service/friend/get_friend_list获取好友信息 /service/friend/get_friend_info发送好友申请 /service/friend/add_friend_apply好友申请处理 /service/friend/add_friend_process删除好友 /service/friend/remove_friend搜索用户 /service/friend/search_friend获取指定用户的消息会话列表 /service/friend/get_chat_session_list创建消息会话 /service/friend/create_chat_session获取消息会话成员列表 /service/friend/get_chat_session_member获取待处理好友申请事件列表 /service/friend/get_pending_friend_events获取历史消息/离线消息列表 /service/message_storage/get_history获取最近N条消息列表 /service/message_storage/get_recent搜索历史消息 /service/message_storage/search_history发送消息 /service/message_transmit/new_message获取单个文件数据 /service/file/get_single_file获取多个文件数据 /service/file/get_multi_file发送单个文件 /service/file/put_single_file发送多个文件 /service/file/put_multi_file语音转文字 /service/speech/recognition}*/
其次,在 HTTP 请求正文中,将采用 protobuf 协议作为正文的序列化方式,不同的请求正文与后台的请求基本上吻合,因此请求正文结构,将与后台服务之间复用同一套接口,具体接口格式在下列各项子服务中给出
🦋 网关 Websocket 接口:
websocket 通信接口中,包含两方面内容:
- 连接的身份识别:
当用户登录成功后,向服务器发起 websocket 长连接请求,建立长连接。
长连接建立成功后,向服务器发送身份鉴权请求,请求内容为 protobuf 结构数据,主要内容为:
- 请求 ID
- 登录会话 ID: 用于进行身份识别
该请求不需要服务端进行回复,鉴权成功则长连接保持,鉴权失败则断开长连接即可。
syntax = "proto3";
package bite_im;
import "base.proto";
option cc_generic_services = true;message ClientAuthenticationReq {string request_id = 1;string session_id = 2;
}
- 事件通知的内容
因为事件通知在 websocket 长连接通信中进行,因此只需要定义出消息结构即可:先将一些公共结构给提取出来进行定义,定义到一个 base.proto 文件中
syntax = "proto3";
package bite_im;option cc_generic_services = true;//用户信息结构
message UserInfo {string user_id = 1;//用户IDstring nickname = 2;//昵称string description = 3;//个人签名/描述string phone = 4; //绑定手机号bytes avatar = 5;//头像照片,文件内容使用二进制
}//聊天会话信息
message ChatSessionInfo {optional string single_chat_friend_id = 1;//群聊会话不需要设置,单聊会话设置为对方IDstring chat_session_id = 2; //会话IDstring chat_session_name = 3;//会话名称git optional MessageInfo prev_message = 4;//会话上一条消息,新建的会话没有最新消息optional bytes avatar = 5;//会话头像 --群聊会话不需要,直接由前端固定渲染,单聊就是对方的头像
}//消息类型
enum MessageType {STRING = 0;IMAGE = 1;FILE = 2;SPEECH = 3;
}
message StringMessageInfo {string content = 1;//文字聊天内容
}
message ImageMessageInfo {optional string file_id = 1;//图片文件id,客户端发送的时候不用设置,由transmit服务器进行设置后交给storage的时候设置optional bytes image_content = 2;//图片数据,在ES中存储消息的时候只要id不要文件数据, 服务端转发的时候需要原样转发
}
message FileMessageInfo {optional string file_id = 1;//文件id,客户端发送的时候不用设置int64 file_size = 2;//文件大小string file_name = 3;//文件名称optional bytes file_contents = 4;//文件数据,在ES中存储消息的时候只要id和元信息,不要文件数据, 服务端转发的时候也不需要填充
}
message SpeechMessageInfo {optional string file_id = 1;//语音文件id,客户端发送的时候不用设置optional bytes file_contents = 2;//文件数据,在ES中存储消息的时候只要id不要文件数据, 服务端转发的时候也不需要填充
}
message MessageContent {MessageType message_type = 1; //消息类型oneof msg_content {StringMessageInfo string_message = 2;//文字消息FileMessageInfo file_message = 3;//文件消息SpeechMessageInfo speech_message = 4;//语音消息ImageMessageInfo image_message = 5;//图片消息};
}
//消息结构
message MessageInfo {string message_id = 1;//消息IDstring chat_session_id = 2;//消息所属聊天会话IDint64 timestamp = 3;//消息产生时间UserInfo sender = 4;//消息发送者信息MessageContent message = 5;
}message Message {string request_id = 1;MessageInfo message = 2;
}message FileDownloadData {string file_id = 1;bytes file_content = 2;
}message FileUploadData {string file_name = 1;int64 file_size = 2;bytes file_content = 3;
}
然后,开始定义通知内容结构:
syntax = "proto3";
package bite_im;
import "base.proto";option cc_generic_services = true;enum NotifyType {FRIEND_ADD_APPLY_NOTIFY = 0;FRIEND_ADD_PROCESS_NOTIFY = 1;CHAT_SESSION_CREATE_NOTIFY = 2;CHAT_MESSAGE_NOTIFY = 3;FRIEND_REMOVE_NOTIFY = 4;
}message NotifyFriendAddApply {UserInfo user_info = 1; //申请人信息
}
message NotifyFriendAddProcess {bool agree = 1;UserInfo user_info = 2; //处理人信息
}
message NotifyFriendRemove {string user_id = 1; //删除自己的用户ID
}
message NotifyNewChatSession {ChatSessionInfo chat_session_info = 1; //新建会话信息
}
message NotifyNewMessage {MessageInfo message_info = 1; //新消息
}message NotifyMessage {optional string notify_event_id = 1;//通知事件操作id(有则填无则忽略)NotifyType notify_type = 2;//通知事件类型oneof notify_remarks { //事件备注信息NotifyFriendAddApply friend_add_apply = 3;NotifyFriendAddProcess friend_process_result = 4;NotifyFriendRemove friend_remove = 7;NotifyNewChatSession new_chat_session_info = 5;//会话信息NotifyNewMessage new_message_info = 6;//消息信息}
}
用户管理子服务
/*用户操作服务器的子服务注册信息: /service/user/instance_id服务名称:/service/user实例ID: instance_id 每个能够提供用户操作服务的子服务器唯一ID当服务发现的时候,通过 /service/user 进行服务发现,就可以发现所有的能够提供用户操作的实例信息了
*/
syntax = "proto3";
package bite_im;
import "base.proto";option cc_generic_services = true;//----------------------------
//用户名注册
message UserRegisterReq {string request_id = 1;string nickname = 2;string password = 3;string verify_code_id = 4;string verify_code = 5;
}
message UserRegisterRsp {string request_id = 1;bool success = 2;string errmsg = 3;
}
//----------------------------
//用户名登录
message UserLoginReq {string request_id = 1;string nickname = 2;string password = 3;string verify_code_id = 4;string verify_code = 5;
}
message UserLoginRsp {string request_id = 1;bool success = 2;string errmsg = 3;string login_session_id = 4;
}
//----------------------------
//手机号验证码获取
message PhoneVerifyCodeReq {string request_id = 1;string phone_number = 2;
}
message PhoneVerifyCodeRsp {string request_id = 1;bool success = 2;string errmsg = 3;string verify_code_id = 4;
}
//----------------------------
//手机号注册
message PhoneRegisterReq {string request_id = 1;string phone_number = 2;string verify_code_id = 3;string verify_code = 4;
}
message PhoneRegisterRsp {string request_id = 1;bool success = 2;string errmsg = 3;
}
//----------------------------
//手机号登录
message PhoneLoginReq {string request_id = 1;string phone_number = 2;string verify_code_id = 3;string verify_code = 4;
}
message PhoneLoginRsp {string request_id = 1;bool success = 2;string errmsg = 3; string login_session_id = 4;
}
//个人信息获取-这个只用于获取当前登录用户的信息
// 客户端传递的时候只需要填充session_id即可
//其他个人/好友信息的获取在好友操作中完成
message GetUserInfoReq {string request_id = 1;optional string user_id = 2;optional string session_id = 3;
}
message GetUserInfoRsp {string request_id = 1;bool success = 2;string errmsg = 3; UserInfo user_info = 4;
}
//----------------------------
//用户头像修改
message SetUserAvatarReq {string request_id = 1;optional string user_id = 2;optional string session_id = 3;bytes avatar = 4;
}
message SetUserAvatarRsp {string request_id = 1;bool success = 2;string errmsg = 3;
}
//----------------------------
//用户昵称修改
message SetUserNicknameReq {string request_id = 1;optional string user_id = 2;optional string session_id = 3;string nickname = 4;
}
message SetUserNicknameRsp {string request_id = 1;bool success = 2;string errmsg = 3;
}
//----------------------------
//用户签名修改
message SetUserDescriptionReq {string request_id = 1;optional string user_id = 2;optional string session_id = 3;string description = 4;
}
message SetUserDescriptionRsp {string request_id = 1;bool success = 2;string errmsg = 3;
}
//----------------------------
//用户手机修改
message SetUserPhoneNumberReq {string request_id = 1;optional string user_id = 2;optional string session_id = 3;string phone_number = 4;string phone_verify_code_id = 5;string phone_verify_code = 6;
}
message SetUserPhoneNumberRsp {string request_id = 1;bool success = 2;string errmsg = 3;
}service UserService {rpc UserRegister(UserRegisterReq) returns (UserRegisterRsp);rpc UserLogin(UserLoginReq) returns (UserLoginRsp);rpc GetPhoneVerifyCode(PhoneVerifyCodeReq) returns (PhoneVerifyCodeRsp);rpc PhoneRegister(PhoneRegisterReq) returns (PhoneRegisterRsp);rpc PhoneLogin(PhoneLoginReq) returns (PhoneLoginRsp);rpc GetUserInfo(GetUserInfoReq) returns (GetUserInfoRsp);rpc SetUserAvatar(SetUserAvatarReq) returns (SetUserAvatarRsp);rpc SetUserNickname(SetUserNicknameReq) returns (SetUserNicknameRsp);rpc SetUserDescription(SetUserDescriptionReq) returns (SetUserDescriptionRsp);rpc SetUserPhoneNumber(SetUserPhoneNumberReq) returns (SetUserPhoneNumberRsp);
}
好友管理子服务
/*好友操作服务器的子服务注册信息: /service/friend/instance_id服务名称:/service/friend实例ID: instance_id 每个能够提供用户操作服务的子服务器唯一ID当服务发现的时候,通过 /service/friend 进行服务发现,就可以发现所有的能够提供用户操作的实例信息了
*/
syntax = "proto3";
package bite_im;
import "base.proto";option cc_generic_services = true;//--------------------------------------
//好友列表获取
message GetFriendListReq {string request_id = 1;optional string user_id = 2;optional string session_id = 3;
}
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;optional string session_id = 3;string peer_id = 4;
}
message FriendRemoveRsp {string request_id = 1;bool success = 2;string errmsg = 3;
}
//--------------------------------------
//添加好友--发送好友申请
message FriendAddReq {string request_id = 1;optional string session_id = 2;optional string user_id = 3;//申请人idstring respondent_id = 4;//被申请人id
}
message FriendAddRsp {string request_id = 1;bool success = 2;string errmsg = 3; string notify_event_id = 4;//通知事件id
}
//--------------------------------------
//好友申请的处理
message FriendAddProcessReq {string request_id = 1;string notify_event_id = 2;//通知事件idbool agree = 3;//是否同意好友申请string apply_user_id = 4; //申请人的用户idoptional string session_id = 5;optional string user_id = 6;
}
// +++++++++++++++++++++++++++++++++
message FriendAddProcessRsp {string request_id = 1;bool success = 2;string errmsg = 3; optional string new_session_id = 4; // 同意后会创建会话,向网关返回会话信息,用于通知双方会话的建立,这个字段客户端不需要关注
}
//--------------------------------------
//获取待处理的,申请自己好友的信息列表
message GetPendingFriendEventListReq {string request_id = 1;optional string session_id = 2;optional string user_id = 3;
}message FriendEvent {string event_id = 1;UserInfo sender = 3;
}
message GetPendingFriendEventListRsp {string request_id = 1;bool success = 2;string errmsg = 3; repeated FriendEvent event = 4;
}//--------------------------------------
//好友搜索
message FriendSearchReq {string request_id = 1;string search_key = 2;//就是名称模糊匹配关键字optional string session_id = 3;optional string user_id = 4;
}
message FriendSearchRsp {string request_id = 1;bool success = 2;string errmsg = 3; repeated UserInfo user_info = 4;
}//--------------------------------------
//会话列表获取
message GetChatSessionListReq {string request_id = 1;optional string session_id = 2;optional string user_id = 3;
}
message GetChatSessionListRsp {string request_id = 1;bool success = 2;string errmsg = 3; repeated ChatSessionInfo chat_session_info_list = 4;
}
//--------------------------------------
//创建会话
message ChatSessionCreateReq {string request_id = 1;optional string session_id = 2;optional string user_id = 3;string chat_session_name = 4;//需要注意的是,这个列表中也必须包含创建者自己的用户IDrepeated string member_id_list = 5;
}
message ChatSessionCreateRsp {string request_id = 1;bool success = 2;string errmsg = 3; //这个字段属于后台之间的数据,给前端回复的时候不需要这个字段,会话信息通过通知进行发送optional ChatSessionInfo chat_session_info = 4;
}
//--------------------------------------
//获取会话成员列表
message GetChatSessionMemberReq {string request_id = 1;optional string session_id = 2;optional string user_id = 3;string chat_session_id = 4;
}
message GetChatSessionMemberRsp {string request_id = 1;bool success = 2;string errmsg = 3; repeated UserInfo member_info_list = 4;
}service FriendService {rpc GetFriendList(GetFriendListReq) returns (GetFriendListRsp);rpc FriendRemove(FriendRemoveReq) returns (FriendRemoveRsp);rpc FriendAdd(FriendAddReq) returns (FriendAddRsp);rpc FriendAddProcess(FriendAddProcessReq) returns (FriendAddProcessRsp);rpc FriendSearch(FriendSearchReq) returns (FriendSearchRsp);rpc GetChatSessionList(GetChatSessionListReq) returns (GetChatSessionListRsp);rpc ChatSessionCreate(ChatSessionCreateReq) returns (ChatSessionCreateRsp);rpc GetChatSessionMember(GetChatSessionMemberReq) returns (GetChatSessionMemberRsp);rpc GetPendingFriendEventList(GetPendingFriendEventListReq) returns (GetPendingFriendEventListRsp);
}
文件管理子服务
/*文件操作服务器的子服务注册信息: /service/file/instance_id服务名称:/service/file实例ID: instance_id 每个能够提供用户操作服务的子服务器唯一ID当服务发现的时候,通过 /service/file 进行服务发现,就可以发现所有的能够提供用户操作的实例信息了
*/
syntax = "proto3";
package bite_im;
import "base.proto";option cc_generic_services = true;message GetSingleFileReq {string request_id = 1;string file_id = 2;optional string user_id = 3;optional string session_id = 4;
}
message GetSingleFileRsp {string request_id = 1;bool success = 2;string errmsg = 3; FileDownloadData file_data = 4;
}message GetMultiFileReq {string request_id = 1;optional string user_id = 2;optional string session_id = 3;repeated string file_id_list = 4;
}
message GetMultiFileRsp {string request_id = 1;bool success = 2;string errmsg = 3; repeated FileDownloadData file_data = 4;
}message PutSingleFileReq {string request_id = 1;optional string user_id = 2;optional string session_id = 3;FileUploadData file_data = 4;
}
message PutSingleFileRsp {string request_id = 1;bool success = 2;string errmsg = 3;FileMessageInfo file_info = 4;
}message PutMultiFileReq {string request_id = 1;optional string user_id = 2;optional string session_id = 3;repeated FileUploadData file_data = 4;
}
message PutMultiFileRsp {string request_id = 1;bool success = 2;string errmsg = 3; repeated FileMessageInfo file_info = 4;
}service FileService {rpc GetSingleFile(GetSingleFileReq) returns (GetSingleFileRsp);rpc GetMultiFile(GetMultiFileReq) returns (GetMultiFileRsp);rpc PutSingleFile(PutSingleFileReq) returns (PutSingleFileRsp);rpc PutMultiFile(PutMultiFileReq) returns (PutMultiFileRsp);
}
消息管理子服务
/*消息存储服务器的子服务注册信息: /service/message_storage/instance_id服务名称:/service/message_storage实例ID: instance_id 每个能够提供用户操作服务的子服务器唯一ID当服务发现的时候,通过 /service/message_storage 进行服务发现,就可以发现所有的能够提供用户操作的实例信息了
*/
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);}
转发管理子服务
/*消息转发服务器的子服务注册信息: /service/message_transmit/instance_id服务名称:/service/message_transmit实例ID: instance_id 每个能够提供用户操作服务的子服务器唯一ID当服务发现的时候,通过 /service/message_transmit 进行服务发现,就可以发现所有的能够提供用户操作的实例信息了
*/
//消息转发服务器接口
syntax = "proto3";
package bite_im;
import "base.proto";option cc_generic_services = true;//这个用于和网关进行通信
message NewMessageReq {string request_id = 1;optional string user_id = 2;optional string session_id = 3;string chat_session_id = 4;MessageContent message = 5;
}
message NewMessageRsp {string request_id = 1;bool success = 2;string errmsg = 3;
}//这个用于内部的通信,生成完整的消息信息,并获取消息的转发人员列表
message GetTransmitTargetRsp {string request_id = 1;bool success = 2;string errmsg = 3; MessageInfo message = 4;repeated string target_id_list = 5;
}service MsgTransmitService {rpc GetTransmitTarget(NewMessageReq) returns (GetTransmitTargetRsp);
}
语音转换子服务
/*语音识别服务器的子服务注册信息: /service/speech/instance_id服务名称:/service/speech实例ID: instance_id 每个能够提供用户操作服务的子服务器唯一ID当服务发现的时候,通过 /service/speech 进行服务发现,就可以发现所有的能够提供用户操作的实例信息了
*/
syntax = "proto3";
package bite_im;option cc_generic_services = true;message SpeechRecognitionReq {string request_id = 1;bytes speech_content = 2;optional string user_id = 3;optional string session_id = 4;
}message SpeechRecognitionRsp {string request_id = 1;bool success = 2;string errmsg = 3; string recognition_result = 4;
}service SpeechService {rpc SpeechRecognition(SpeechRecognitionReq) returns (SpeechRecognitionRsp);
}
四:🔥 微服务通信实现
🦋 语音管理子服务
🎀 功能设计
语音转换子服务,用于调用语音识别 SDK,进行语音识别,将语音转为文字后返回给网关即可,因此提供的功能性接口只有一个:
- 语音消息的文字转换:客户端进行语音消息的文字转换。
🎀 模块划分
- 参数/配置文件解析模块:基于 gflags 框架直接使用进行参数/配置文件解析。
- 日志模块:基于 spdlog 框架封装的模块直接使用进行日志输出。
- 服务注册模块:基于 etcd 框架封装的注册模块直接使用进行语音识别子服务的服务注册。
- rpc 服务模块:基于 brpc 框架搭建 rpc 服务器。
- 语音识别 SDK 模块:基于语音识别平台提供的 sdk 直接使用,完成语音的识别转文字。
模块功能示意图:
🎀 接口实现流程
语音识别:
- 接收请求,从请求中取出语音数据
- 基于语音识别 sdk 进行语音识别,获取识别后的文本内容
- 组织响应进行返回
speech_server.hpp
// 实现语音识别子服务
#include <brpc/server.h>
#include <butil/logging.h>#include "asr.hpp" // 语音识别模块封装
#include "etcd.hpp" // 服务注册模块封装
#include "logger.hpp" // 日志模块封装
#include "speech.pb.h" // protobuf 框架代码namespace bite_im
{// 语音识别服务对象class SpeechServiceImpl : public bite_im::SpeechService{public:SpeechServiceImpl(const ASRClient::ptr &asr_client) : _asr_client(asr_client){}~SpeechServiceImpl() {}virtual void SpeechRecognition(google::protobuf::RpcController *controller, const bite_im::SpeechRecognitionReq *request,bite_im::SpeechRecognitionRsp *response, google::protobuf::Closure *done) override{ // done 通常用于通知框架 RPC 调用已完成,框架会负责清理资源并返回响应给客户端brpc::ClosureGuard rpc_guard(done); // done->run();// 1. 取出请求中的语音数据// 2. 调用语音 sdk 模块进行语音识别,得到响应 std::string err;std::string res = _asr_client->recognize(request->speech_content(), err);if (res.empty()) {LOG_ERROR("{} 语音识别失败!", request->request_id());response->set_request_id(request->request_id());response->set_success(false);response->set_errmsg("语音识别失败: " + err);return ;}// 3. 组织响应response->set_request_id(request->request_id());response->set_success(true);response->set_recognition_result(res);}private:ASRClient::ptr _asr_client;};class SpeechServer {public:using ptr = std::shared_ptr<SpeechServer>;SpeechServer(const ASRClient::ptr &asr_client, const Registry::ptr ®_client, const std::shared_ptr<brpc::Server> &rpc_server) : _asr_client(asr_client),_reg_client(reg_client),_rpc_server(rpc_server){}~SpeechServer() {}// 用于搭建 RPC 服务器,并启动服务器void start() {_rpc_server->RunUntilAskedToQuit();}private:ASRClient::ptr _asr_client;Registry::ptr _reg_client; // 服务注册客户端std::shared_ptr<brpc::Server> _rpc_server;};// 建造者模式class SpeechServerBuilder {public:// 构造语音识别客户端对象void make_asr_object(const std::string &appid, const std::string &api_key, const std::string &secret_key) {_asr_client = std::make_shared<ASRClient>(appid, api_key, secret_key);}// 构造服务注册客户端对象 注册中心地址 服务名称 服务地址void make_reg_object(const std::string ®_host, const std::string &service_name, const std::string &access_host) {_reg_client = std::make_shared<Registry>(reg_host);_reg_client->registry(service_name, access_host);}// 构造RPC服务器对象void make_rpc_server(uint16_t port, uint32_t timeout, uint8_t num_threads) {if (!_asr_client) {LOG_ERROR("还未初始化语音识别模块!");abort();}_rpc_server = std::make_shared<brpc::Server>();// 关闭 brpc 的默认日志输出logging::LoggingSettings settings;settings.logging_dest = logging::LoggingDestination::LOG_TO_NONE;logging::InitLogging(settings);SpeechServiceImpl *speech_service = new SpeechServiceImpl(_asr_client);int ret = _rpc_server->AddService(speech_service, brpc::ServiceOwnership::SERVER_OWNS_SERVICE); // SERVER_OWNS_SERVICE 添加服务失败时或者服务退出,负责删除服务对象if (ret == -1) {LOG_ERROR("添加Rpc服务失败!");abort();}brpc::ServerOptions options;options.idle_timeout_sec = timeout; // 连接超时销毁options.num_threads = num_threads; // io线程数ret = _rpc_server->Start(port, &options);if (ret == -1) {LOG_ERROR("启动服务器失败!");abort();}} SpeechServer::ptr build() {if (!_asr_client) {LOG_ERROR("还未初始化语音识别模块!");abort();}if (!_asr_client) {LOG_ERROR("还未初始化服务注册模块!");abort();}if (!_asr_client) {LOG_ERROR("还未初始化RPC服务器模块!");abort();} SpeechServer::ptr server = std::make_shared<SpeechServer>(_asr_client, _reg_client, _rpc_server);return server;}private:ASRClient::ptr _asr_client;Registry::ptr _reg_client; // 服务注册客户端std::shared_ptr<brpc::Server> _rpc_server;};
}
speech_server.cc
// 主要实现语音识别子服务的服务器搭建
#include "speech_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(base_service, "/service", "服务监控根目录");
DEFINE_string(instance_name, "/speech_service/instance", "当前实例名称");
DEFINE_string(access_host, "127.0.0.1:10001", "当前实例的外部访问地址");DEFINE_int32(listen_port, 10001, "Rpc服务器监听端口");
DEFINE_int32(rpc_timeout, -1, "Rpc调用超时时间");
DEFINE_int32(rpc_threads, 4, "Rpc的IO线程数量");DEFINE_string(app_id, "119870892", "语音平台应用ID");
DEFINE_string(api_key, "zPRZarRjsUYpIxcRcTnlbBdM", "语音平台API密钥");
DEFINE_string(secret_key, "uyxgk37wpOeebbHNQeFYTPvC6LSkw7Sr", "语音平台加密密钥");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::SpeechServerBuilder ssb;ssb.make_asr_object(FLAGS_app_id, FLAGS_api_key, FLAGS_secret_key);ssb.make_rpc_server(FLAGS_listen_port, FLAGS_rpc_timeout, FLAGS_rpc_threads);ssb.make_reg_object(FLAGS_registry_host, FLAGS_base_service + FLAGS_instance_name, FLAGS_access_host);bite_im::SpeechServer::ptr server = ssb.build();server->start(); // 启动的 rpc 服务器return 0;
}
搭建测试客户端
speech_client.cc
// speech_server 的测试客户端实现
// 1. 进行服务发现--发现 speech_server 的服务器节点地址信息并实例化的通信信道
// 2. 读取语音文件数据
// 3. 发起语音识别 RPC 调用#include "etcd.hpp"
#include "channel.hpp"
#include <gflags/gflags.h>
#include <thread>
#include "aip-cpp-sdk/speech.h"
#include "speech.pb.h"DEFINE_bool(run_mode, false, "程序的运行模式,false-调试; true-发布;");
DEFINE_string(log_file, "", "发布模式下,用于指定日志的输出文件");
DEFINE_int32(log_level, 0, "发布模式下,用于指定日志输出等级");DEFINE_string(etcd_host, "http://127.0.0.1:2379", "服务注册中心地址");
DEFINE_string(base_service, "/service", "服务监控根目录");
DEFINE_string(speech_service, "/service/speech_service", "服务监控根目录");int main(int argc, char *argv[])
{google::ParseCommandLineFlags(&argc, &argv, true);bite_im::init_logger(FLAGS_run_mode, FLAGS_log_file, FLAGS_log_level);// 1. 先构造Rpc信道管理对象auto sm = std::make_shared<bite_im::ServiceManager>();// 声明关心的服务sm->declared(FLAGS_speech_service);auto put_cb = std::bind(&bite_im::ServiceManager::onServiceOnline, sm.get(), std::placeholders::_1, std::placeholders::_2);auto del_cb = std::bind(&bite_im::ServiceManager::onServiceOffline, sm.get(), std::placeholders::_1, std::placeholders::_2);// 2. 构造服务发现对象bite_im::Discovery::ptr dclient = std::make_shared<bite_im::Discovery>(FLAGS_etcd_host, FLAGS_base_service, put_cb, del_cb);// 3. 通过Rpc信道管理对象,获取提供Echo服务的信道auto channel = sm->choose(FLAGS_speech_service);if (!channel) {std::this_thread::sleep_for(std::chrono::seconds(1));return -1;}// 读取语音文件数据std::string file_content;aip::get_file_content("16k.pcm", &file_content);std::cout << file_content.size() << std::endl;// 4. 发起EchoRpc调用bite_im::SpeechService_Stub stub(channel.get());bite_im::SpeechRecognitionReq req;req.set_speech_content(file_content);req.set_request_id("111111");brpc::Controller *cntl = new brpc::Controller();bite_im::SpeechRecognitionRsp *rsp = new bite_im::SpeechRecognitionRsp();stub.SpeechRecognition(cntl, &req, rsp, nullptr);if (cntl->Failed() == true) {std::cout << "Rpc调用失败:" << cntl->ErrorText() << std::endl;delete cntl;delete rsp;std::this_thread::sleep_for(std::chrono::seconds(1));return -1;}if (rsp->success() == false) {std::cout << rsp->errmsg() << std::endl;return -1;}std::cout << "收到响应: " << rsp->request_id() << std::endl;std::cout << "收到响应: " << rsp->recognition_result() << std::endl;return 0;
}
🦋 文件管理子服务
🎀 功能设计
- 文件的上传
- 单个文件的上传:这个接口基本用于后台部分,收到文件消息后将文件数据转发给文件子服务进行存储
- 多个文件的上传:这个接口基本用于后台部分,收到文件消息后将文件数据转发给文件子服务进行存储
- 文件的下载
- 单个文件的下载:在后台用于获取用户头像文件数据,以及客户端用于获取文件 / 语音 / 图片 消息的文件数据
- 多个文件的下载:在后台用于大批量获取用户头像数据(比如获取用户列表的时候),以及前端的批量文件下载
🎀 模块划分
- 参数/配置文件解析模块:基于 gflags 框架直接使用进行参数/配置文件解析。
- 日志模块:基于 spdlog 框架封装的模块直接使用进行日志输出。
- 服务注册模块:基于 etcd 框架封装的注册模块直接使用进行文件存储管理子服务的服务注册。
- rpc 服务模块:基于 brpc 框架搭建 rpc 服务器。
- 文件操作模块:基于标准库的文件流操作实现文件读写的封装。
模块功能示意图
🎀 接口实现流程
- 单个文件的上传:
- 获取文件元数据 (大小、文件名、文件内容)
- 为文件分配文件 ID
- 以文件 ID 为文件名打开文件,并写入数据
- 组织响应进行返回
- 多个文件的上传:
多文件上传,其实相较于单文件上传,就是将处理的过程循环进行了而已
- 从请求中获取文件元数据
- 为文件分配文件 ID
- 以文件 ID 为文件名打开文件,并写入数据
- 回到第一步进行下一个文件的处理
- 当所有文件数据存储完毕,组织响应进行返回
- 单个文件的下载:
- 从请求中获取文件 ID
- 以文件 ID 作为文件名打开文件,获取文件大小,并从中读取文件数据
- 组织响应进行返回
- 多个文件的下载:
多文件下载,其实相较于单文件下载,就是将处理的过程循环进行了而已
- 从请求中获取文件 ID
- 以文件 ID 作为文件名打开文件,获取文件大小,并从中读取文件数据
- 回到第一步进行下一个文件的处理
- 当所有文件数据获取完毕,组织响应进行返回
// 实现文件存储子服务
// 1. 实现文件rpc服务类 --- 实现rpc调用的业务处理接口
// 2. 实现文件存储子服务的服务器类
// 3. 实现文件存储子服务类的构造者
#include <brpc/server.h>
#include <butil/logging.h>#include "etcd.hpp" // 服务注册模块封装
#include "logger.hpp" // 日志模块封装
#include "utils.hpp"
#include "base.pb.h"
#include "file.pb.h"#include <sys/stat.h>
#include <sys/types.h>namespace bite_im
{class FileServiceImpl : public bite_im::FileService{public:FileServiceImpl(const std::string &storage_path) : _storage_path(storage_path) {umask(0);mkdir(storage_path.c_str(), 0775);if (_storage_path.back() != '/') _storage_path.push_back('/');}~FileServiceImpl() {}virtual void GetSingleFile(google::protobuf::RpcController *controller, const bite_im::GetSingleFileReq *request,bite_im::GetSingleFileRsp *response, google::protobuf::Closure *done) override{ brpc::ClosureGuard rpc_guard(done); // done->run(); response->set_request_id(request->request_id());// 1. 取出请求中的文件ID(起始就是文件名)std::string fid = request->file_id();std::string filename = _storage_path + fid;// 2. 将文件ID作为文件名,读取文件数据std::string body;bool ret = readFile(filename, body);if (!ret) {response->set_success(false);response->set_errmsg("读取文件数据失败");LOG_ERROR("{} 读取文件数据失败!", request->request_id());return ;}// 3. 组织响应response->set_success(true);response->mutable_file_data()->set_file_id(fid);response->mutable_file_data()->set_file_content(body);}virtual void GetMultiFile(google::protobuf::RpcController *controller, const bite_im::GetMultiFileReq *request,bite_im::GetMultiFileRsp *response, google::protobuf::Closure *done) override{ brpc::ClosureGuard rpc_guard(done); // done->run();response->set_request_id(request->request_id());// 循环取出请求中的文件ID,读取文件数据进行填充for (int i = 0; i < request->file_id_list_size(); i++) {std::string fid = request->file_id_list(i);std::string filename = _storage_path + fid;std::string body;bool ret = readFile(filename, body);if (!ret) {response->set_success(false);response->set_errmsg("读取文件数据失败");LOG_ERROR("{} 读取文件数据失败!", request->request_id());return ;}FileDownloadData data;data.set_file_id(fid);data.set_file_content(body);response->mutable_file_data()->insert({fid, data});}response->set_success(true);}virtual void PutSingleFile(google::protobuf::RpcController *controller, const bite_im::PutSingleFileReq *request,bite_im::PutSingleFileRsp *response, google::protobuf::Closure *done) override{ brpc::ClosureGuard rpc_guard(done); // done->run();response->set_request_id(request->request_id());// 1. 为文件生成一个唯一uudi作为文件名 以及 文件IDstd::string fid = uuid();std::string filename = _storage_path + fid;// 2. 取出请求中的文件数据,进行文件数据写入bool ret = writeFile(filename, request->file_data().file_content());if (!ret) {response->set_success(false);response->set_errmsg("写入文件数据失败");LOG_ERROR("{} 写入文件数据失败!", request->request_id());return ;}// 3. 组织响应response->set_success(true);response->mutable_file_info()->set_file_id(fid);response->mutable_file_info()->set_file_size(request->file_data().file_size());response->mutable_file_info()->set_file_name(request->file_data().file_name());}virtual void PutMultiFile(google::protobuf::RpcController *controller, const bite_im::PutMultiFileReq *request,bite_im::PutMultiFileRsp *response, google::protobuf::Closure *done) override{ brpc::ClosureGuard rpc_guard(done); // done->run();response->set_request_id(request->request_id());for (int i = 0; i < request->file_data_size(); i++) {std::string fid = uuid();std::string filename = _storage_path + fid;bool ret = writeFile(filename, request->file_data(i).file_content());if (!ret) {response->set_success(false);response->set_errmsg("写入文件数据失败");LOG_ERROR("{} 写入文件数据失败!", request->request_id());return ;}bite_im::FileMessageInfo *info = response->add_file_info();info->set_file_id(fid);info->set_file_size(request->file_data(i).file_size());info->set_file_name(request->file_data(i).file_name());}response->set_success(true);}private:std::string _storage_path;};class FileServer {public:using ptr = std::shared_ptr<FileServer>;FileServer(const Registry::ptr ®_client, const std::shared_ptr<brpc::Server> &rpc_server) : _reg_client(reg_client),_rpc_server(rpc_server){}~FileServer() {}// 用于搭建 RPC 服务器,并启动服务器void start() {_rpc_server->RunUntilAskedToQuit();}private:Registry::ptr _reg_client; // 服务注册客户端std::shared_ptr<brpc::Server> _rpc_server;};// 建造者模式class FileServerBuilder {public:// 构造服务注册客户端对象 注册中心地址 服务名称 服务地址void make_reg_object(const std::string ®_host, const std::string &service_name, const std::string &access_host) {_reg_client = std::make_shared<Registry>(reg_host);_reg_client->registry(service_name, access_host);}// 构造RPC服务器对象void make_rpc_server(uint16_t port, uint32_t timeout, uint8_t num_threads, const std::string &path = "./data") {_rpc_server = std::make_shared<brpc::Server>();// 关闭 brpc 的默认日志输出logging::LoggingSettings settings;settings.logging_dest = logging::LoggingDestination::LOG_TO_NONE;logging::InitLogging(settings);FileServiceImpl *file_service = new FileServiceImpl(path);int ret = _rpc_server->AddService(file_service, brpc::ServiceOwnership::SERVER_OWNS_SERVICE); // SERVER_OWNS_SERVICE 添加服务失败时或者服务退出,负责删除服务对象if (ret == -1) {LOG_ERROR("添加Rpc服务失败!");abort();}brpc::ServerOptions options;options.idle_timeout_sec = timeout; // 连接超时销毁options.num_threads = num_threads; // io线程数ret = _rpc_server->Start(port, &options);if (ret == -1) {LOG_ERROR("启动服务器失败!");abort();}} FileServer::ptr build() {if (!_reg_client) {LOG_ERROR("还未初始化服务注册模块!");abort();}if (!_rpc_server) {LOG_ERROR("还未初始化RPC服务器模块!");abort();} FileServer::ptr server = std::make_shared<FileServer>(_reg_client, _rpc_server);return server;}private:Registry::ptr _reg_client; // 服务注册客户端std::shared_ptr<brpc::Server> _rpc_server;};
}
//按照流程完成服务器的搭建
// 1. 参数解析
// 2. 日志初始化
// 3. 构造服务器对象,启动服务器
#include "file_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(base_service, "/service", "服务监控根目录");
DEFINE_string(instance_name, "/file_service/instance", "当前实例名称");
DEFINE_string(access_host, "127.0.0.1:10002", "当前实例的外部访问地址");DEFINE_string(storage_path, "./data/", "当前实例的外部访问地址");DEFINE_int32(listen_port, 10002, "Rpc服务器监听端口");
DEFINE_int32(rpc_timeout, -1, "Rpc调用超时时间");
DEFINE_int32(rpc_threads, 4, "Rpc的IO线程数量");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::FileServerBuilder fsb;fsb.make_rpc_server(FLAGS_listen_port, FLAGS_rpc_timeout, FLAGS_rpc_threads, FLAGS_storage_path);fsb.make_reg_object(FLAGS_registry_host, FLAGS_base_service + FLAGS_instance_name, FLAGS_access_host);bite_im::FileServer::ptr server = fsb.build();server->start(); // 启动的 rpc 服务器return 0;
}
🦋 用户管理子服务
🎀 功能设计
用户管理子服务,主要用于管理用户的数据,以及关于用户信息的各项操作,因此在上述项目功能中,用户子服务需要提供以下接口:
- 用户注册:用户输入用户名(昵称),以及密码进行用户名的注册
- 用户登录:用户通过用户名和密码进行登录
- 短信验证码获取:当用户通过手机号注册或登录的时候,需要获取短信验证码
- 手机号注册:用户输入手机号和短信验证码进行手机号的用户注册
- 手机号登录:用户输入手机号和短信验证码进行手机号的用户登录
- 用户信息获取:当用户登录之后,获取个人信息进行展示
- 头像修改:设置用户头像
- 昵称修改:设置用户昵称
- 签名修改:设置用户签名
- 手机号修改:修改用户的绑定手机号
🎀 模块划分
- 参数/配置文件解析模块:基于 gflags 框架直接使用进行参数/配置文件解析。
- 日志模块:基于 spdlog 框架封装的模块直接使用进行日志输出。
- 服务注册模块:基于 etcd 框架封装的注册模块直接使用,进行聊天消息存储子服务的注册。
- 数据库数据操作模块:基于 odb-mysql 数据管理封装的模块,实现关系型数据库中数据的操作。
- 用户进行用户名/手机号注册的时候在数据库中进行新增信息
- 用户修改个人信息的时候修改数据库中的记录
- 用户登录的时候,在数据库中进行用户名密码的验证
- redis 客户端模块:基于 redis++封装的客户端进行内存数据库数据操作
- 当用户登录的时候需要为用户创建登录会话,会话信息保存在 redis 服务器中。
- 当用户手机号进行获取/验证验证码的时候,验证码与对应信息保存在 redis 服务器中
- rpc 服务模块:基于 brpc 框架搭建 rpc 服务器。
- rpc 服务发现与调用模块:基于 etcd 框架与 brpc 框架封装的服务发现与调用模块,
- 连接文件管理子服务:获取用户信息的时候,用户头像是通过文件的形式存储在文件子服务中的。
- 连接消息管理子服务:在打开聊天会话的时候,需要获取最近的一条消息进行展示。
- ES 客户端模块:基于 elasticsearch 框架实现访问客户端,向 ES 服务器中存储用户简息,以便于用户的搜索
- 短信平台客户端模块:基于短信平台 SDK 封装使用,用于向用户手机号发送指定验证码。
功能模块示意图:
🎀 数据管理
关系数据库数据管理:
在关系型数据库中,对于用户子服务来说,总体只进行了一个信息数据的存储与管理,那就是用户信息数据,因此只需要构建好用户信息表,提供好对应的操作即可。用户数据表:
包含字段:
- 主键 ID:自动生成
- 用户 ID:用户唯一标识
- 用户昵称:用户的昵称,也可用作登录用户名
- 用户签名:用户对自己的描述吧…
- 登录密码:登录验证
- 绑定手机号:用户可以绑定手机号,绑定后可以通过手机号登录、
- 用户头像文件 ID:头像文件存储的唯一标识,具体头像数据存储在文件子服务器中。
提供的操作:
- 通过昵称获取用户信息
- 通过手机号获取用户信息
- 通过用户 ID 获取用信息
- 新增用户
- 更新用户信息
ODB 映射数据结构:
#include <string>
#include <cstddef>
#include <odb/core.hxx>
#include <odb/nullable.hxx>#pragma db object
class user {
public:user () {}
private:friend class odb::access;#pragma db id autounsigned long _id;#pragma db unique type("VARCHAR(127)")std::string _user_id;#pragma db unique type("VARCHAR(63)")odb::nullable<std::string> _nickname;#pragma db type("VARCHAR(255)")odb::nullable<std::string> _passwd;#pragma db type("VARCHAR(127)")odb::nullable<std::string> _avatar_id;#pragma db unique type("VARCHAR(15)")odb::nullable<std::string> _phone_number;#pragma db type("VARCHAR(255)")odb::nullable<std::string> _description;
};
🎀 内存数据库数据管理:
会话信息映射键值对:
映射类型:字符串键值对映射
映射字段:
- 会话 ID(key) - 用户 ID(val) :便于通过会话 ID 查找用户 ID,进行后续操作时的连接身份识别鉴权
- 在用户登录的时候新增数据,
- 在用户登录后的操作时进行有无验证及查询
- 该映射数据在用户退出登录的时候删除(目前并未提供实现)
- 用户 ID(key) - 空(val) :这是一个用户登录状态的标记,用于避免同时重复登录
- 在用户登录的时候新增数据
- 在用户连接断开的时候删除数据
验证码信息映射键值对:
映射类型:字符串键值对映射
映射字段:
- 验证码 ID(key) - 验证码(val) : 用于生成一个验证码 ID 和验证码,
- 在用户获取短信验证码的时候新增数据。
- 验证码通过短信平台发送给用户手机,
- 而验证码 ID 直接响应发送给用户,用户登录的时候通过这两个信息进行验证。
- 该映射字段需要设置一个 60s 过期自动删除的事件,以及在验证完毕后删除。
文档数据库数据管理:
用户信息的用户 ID,手机号,昵称字段需要在 ES 服务器额外进行一份存储,其目的是因为有用户搜索的功能,用户搜索通常会是一种字符串的模糊匹配方式,用传统的关系型数据库进行模糊匹配效率会极差,因此采用 ES 服务对索引字段进行分词后构建倒排索引,根据关键词进行搜索,效率会大大提升。
🎀 接口实现流程
用户注册
- 从请求中取出昵称和密码
- 检查昵称是否合法(只能包含字母,数字,连字符-,下划线_,长度限制 3~15 之间)
- 检查密码是否合法(只能包含字母,数字,长度限制 6~15 之间)
- 根据昵称在数据库进行判断是否昵称已存在
- 向数据库新增数据
- 向 ES 服务器中新增用户信息
- 组织响应,进行成功与否的响应即可
用户登录
- 从请求中取出昵称和密码
- 通过昵称获取用户信息,进行密码是否一致的判断
- 根据 redis 中的登录标记信息是否存在判断用户是否已经登录。
- 构造会话 ID,生成会话键值对,向 redis 中添加会话信息以及登录标记信息
- 组织响应,返回生成的会话 ID
获取短信验证码
- 从请求中取出手机号码
- 验证手机号码格式是否正确(必须以 1 开始,第二位 3~9 之间,后边 9 个数字字符)
- 生成 4 位随机验证码
- 基于短信平台 SDK 发送验证码
- 构造验证码 ID,添加到 redis 验证码映射键值索引中
- 组织响应,返回生成的验证码 ID
手机号注册
- 从请求中取出手机号码和验证码
- 检查注册手机号码是否合法
- 从 redis 数据库中进行验证码 ID-验证码一致性匹配
- 通过数据库查询判断手机号是否已经注册过
- 向数据库新增用户信息
- 向 ES 服务器中新增用户信息
- 组织响应,返回注册成功与否
手机号登录
- 从请求中取出手机号码和验证码 ID,以及验证码。
- 检查注册手机号码是否合法
- 根据手机号从数据数据进行用户信息查询,判断用用户是否存在
- 从 redis 数据库中进行验证码 ID-验证码一致性匹配
- 根据 redis 中的登录标记信息是否存在判断用户是否已经登录。
- 构造会话 ID,生成会话键值对,向 redis 中添加会话信息以及登录标记信息
- 组织响应,返回生成的会话 ID
获取用户信息
- 从请求中取出用户 ID
- 通过用户 ID,从数据库中查询用户信息
- 根据用户信息中的头像 ID,从文件服务器获取头像文件数据,组织完整用户信息
- 组织响应,返回用户信息
设置头像
- 从请求中取出用户 ID 与头像数据
- 从数据库通过用户 ID 进行用户信息查询,判断用户是否存在
- 上传头像文件到文件子服务,
- 将返回的头像文件 ID 更新到数据库中
- 更新 ES 服务器中用户信息
- 组织响应,返回更新成功与否
设置昵称
- 从请求中取出用户 ID 与新的昵称
- 判断昵称格式是否正确
- 从数据库通过用户 ID 进行用户信息查询,判断用户是否存在
- 将新的昵称更新到数据库中
- 更新 ES 服务器中用户信息
- 组织响应,返回更新成功与否
设置签名
- 从请求中取出用户 ID 与新的签名
- 从数据库通过用户 ID 进行用户信息查询,判断用户是否存在
- 将新的签名更新到数据库中
- 更新 ES 服务器中用户信息
- 组织响应,返回更新成功与否
设置绑定手机号
- 从请求中取出手机号码和验证码 ID,以及验证码。
- 检查注册手机号码是否合法
- 从 redis 数据库中进行验证码 ID-验证码一致性匹配
- 根据手机号从数据数据进行用户信息查询,判断用用户是否存在
- 将新的手机号更新到数据库中6. 更新 ES 服务器中用户信息
- 组织响应,返回更新成功与否
🦋 消息转发子服务
🎀 功能设计
转发子服务,主要用于针对一条消息内容,组织消息的 ID 以及各项所需要素,然后告诉网关服务器一条消息应该发给谁
通常消息都是以聊天会话为基础进行发送的,根据会话找到它的所有成员,这就是转发的目标
除此之外,转发子服务将收到的消息,放入消息队列中,由消息存储管理子服务进行消费存储
- 获取消息转发目标:针对消息内容,组织消息,并告知网关转发目标
🎀 模块划分
- 参数/配置文件解析模块:基于 gflags 框架直接使用进行参数/配置文件解析
- 日志模块:基于 spdlog 框架封装的模块直接使用进行日志输出
- 服务注册模块:基于 etcd 框架封装的注册模块直接使用进行消息转发服务的服务注册
- 数据库数据操作模块:基于 odb-mysql 数据管理封装的模块,从数据库获取会话成员
- 服务发现与调用模块:基于 etcd 框架与 brpc 框架封装的服务发现与调用模块,从用户子服务获取消息发送者的用户信息
- rpc 服务模块:基于 brpc 框架搭建 rpc 服务器
- MQ 发布模块:基于 rabbitmq-client 封装的模块将消息发布到消息队列,让消息存储子服务进行消费,对消息进行存储(离线消息功能)
功能模块示意图:
🎀 接口实现流程
获取消息转发目标与消息处理
- 从请求中取出消息内容,会话 ID, 用户 ID
- 根据用户 ID 从用户子服务获取当前发送者用户信息
- 根据消息内容构造完成的消息结构(分配消息 ID,填充发送者信息,填充消息产生时间)
- 将消息序列化后发布到 MQ 消息队列中,让消息存储子服务对消息进行持久化存储
- 从数据库获取目标会话所有成员 ID
- 组织响应(完整消息+目标用户 ID),发送给网关,告知网关该将消息发送给谁
🦋 消息存储子服务
🎀 功能设计
消息管理子服务,主要用于管理消息的存储:
- 文本消息,储存在 ElasticSearch 文档搜索服务中
- 文件/语音/图片,需要转储到文件管理子服务中
除了管理消息的存储,还需要管理消息的搜索获取,因此需要对外提供以下接口:
- 获取历史消息:
- 获取最近 N 条消息:用于登录成功后,点击对方头像打开聊天框时显示最近的消息
- 获取指定时间段内的消息:用户可以进行聊天消息的按时间搜索
- 关键字消息搜索:用户可以针对指定好友的聊天进行聊天消息的关键字搜索
🎀 功能设计模块划分
- 参数/配置文件解析模块:基于 gflags 框架直接使用进行参数/配置文件解析
- 日志模块:基于 spdlog 框架封装的模块直接使用进行日志输出
- 服务注册模块:基于 etcd 框架封装的注册模块直接使用,进行聊天消息存储子服务的注册
- 数据库数据操作模块:基于 odb-mysql 数据管理封装的模块,进行数据库数据操作,用于从 MQ 中消费到消息后,向数据库中存储一份,以便于通过时间进行范围性查找
- 从数据库根据指定用户的所有好友信息
- rpc 服务模块:基于 brpc 框架搭建 rpc 服务器
- 服务发现与调用模块:基于 etcd 框架与 brpc 框架封装的服务发现与调用模块
- 连接文件管理子服务:将文件/语音/图片类型的消息以及用户头像之类的文件
数据转储到文件管理子服务。 - 连接用户管理子服务:在消息搜索时,根据发送用户的 ID 获取发送者用户信息
- 连接文件管理子服务:将文件/语音/图片类型的消息以及用户头像之类的文件
- ES 客户端模块:基于 elasticsearch 框架实现访问客户端,向 es 服务器进行文本聊天消息的存储,以便于文本消息的关键字搜索。
- MQ 消费模块:基于 rabbitmq-client 封装的消费者模块从消息队列服务器消费获取聊天消息,将文本消息存储到 ElasticSearch 服务,将文件消息转储到文件管理子服务,所有消息的简息都需要向数据库存储一份。
模块功能示意图:
🎀 数据管理
数据库消息管理:
在消息的存储管理中,所有的消息简息都要在数据库中存储一份,进行消息的持久化,以便于进行时间范围性查询和离线消息的实现。
消息类型有四种:文本,文件,语音,图片。
我们不可能将文件数据也存储到数据库中,因此数据库中只存储文本消息和其他类型消息的元信息即可。
数据库表结构:
- 消息 ID:唯一标识
- 消息产生时间:用于进行时间性搜索
- 消息发送者用户 ID:明确消息的发送者
- 消息产生会话 ID:明确消息属于哪个会话• 消息类型:明确消息的类型
- 消息内容:只存储文本消息;文件/语音/图片数据不进行存储,或者说是存储在文件子服务中。
- 文件 ID:只有文件/语音/图片类消息会用到
- 文件大小:只有文件/语音/图片类消息会用到
- 文件名称:只有文件类消息会用到
数据库操作:
- 新增消息
- 通过消息 ID 获取消息信息
- 通过会话 ID,时间范围,获取指定时间段之内的消息,并按时间进行排序
- 通过会话 ID,消息数量,获取最近的 N 条消息(逆序+limit 即可)
ODB 映射数据结构:
#include <string>
#include <cstddef>
#include <odb/core.hxx>
#include <odb/nullable.hxx>
#include <boost/date_time/posix_time/posix_time.hpp>
#pragma db objectclass message
{
public:message (){}
private:friend class odb::access;#pragma db id autounsigned long _id;#pragma db unique type("VARCHAR(127)")std::string _message_id ;#pragma db type("TIMESTAMP") not_nullboost::posix_time::ptime _created_time;#pragma db type("VARCHAR(127)")std::string _from_user_id ;#pragma db type("VARCHAR(127)")std::string _to_session_id ;#pragma db not_nullsigned char _message_type ;odb::nullable<std::string> _content;#pragma db type("VARCHAR(127)")odb::nullable<std::string> _file_id ;#pragma db type("VARCHAR(127)")odb::nullable<std::string> _filename ;odb::nullable<unsigned long> _filesize;
};
ES 文本消息管理:
因为当前聊天室项目中,实现了聊天内容的关键字搜索功能,但是如果在数据库中进行关键字的模糊匹配,则效率会非常低,因此采用 ES 进行消息内容存储与搜索,但是在搜索的时候需要进行会话的过滤,因此这里也要考虑 ES 索引的构造。
🎀 接口实现流程
最近 N 条消息获取:
- 从请求中,获取会话 ID, 要获取的消息数量
- 访问数据库,从数据库中按时间排序,获取指定数量的消息简略信息(消息 ID,会话 ID,消息类型,产生时间,发送者用户 ID,文本消息内容,文件消息元信息)
- 循环构造完整消息(从用户子服务获取消息的发送者用户信息,从文件子服务获取文件/语音/图片数据)
- 组织响应返回给网关服务器。
指定时间段消息搜索:
- 从请求中,获取会话 ID, 要获取的消息的起始时间与结束时间。
- 访问数据库,从数据库中按时间进行范围查询,获取消息简略信息(消息 ID,会话 ID,消息类型,产生时间,发送者用户 ID,文本消息内容,文件消息元信息)
- 循环构造完整消息(从用户子服务获取消息的发送者用户信息,从文件子服务获取文件/语音/图片数据)
- 组织响应返回给网关服务器。
关键字消息搜索:
- 从请求中,获取会话 ID, 搜索关键字。
- 基于封装的 ES 客户端,访问 ES 服务器进行文本消息搜索(以消息内容进行搜索,以会话 ID 进行过滤),从 ES 服务器获取到消息简息(消息 ID,会话 ID, 文本消息内容)。
- 循环从数据库根据消息 ID 获取消息简息(消息 ID,消息类型,会话 ID,发送者ID,产生时间,文本消息内容,文件消息元数据)。
- 循环从用户子服务获取所有消息的发送者用户信息,构造完整消息。
- 组织响应返回给网关服务器。
🦋 好友管理子服务
🎀 功能设计
好友管理子服务,主要用于管理好友相关的数据与操作,因此主要负责以下接口:
- 好友列表的获取:当用户登录成功之后,获取自己好友列表进行展示
- 申请好友:搜索用户之后,点击申请好友,向对方发送好友申请
- 待处理申请的获取:当用户登录成功之后,会获取离线的好友申请请求以待处理
- 好友申请的处理:针对收到的好友申请进行同意/拒绝的处理
- 删除好友:删除当前好友列表中的好友
- 用户搜索:可以进行用户的搜索用于申请好友
- 聊天会话列表的获取:每个单人/多人聊天都有一个聊天会话,在登录成功后可以获取聊天会话,查看历史的消息以及对方的各项信息
- 多人聊天会话的创建:单人聊天会话在对方同意好友时创建,而多人会话需要调用该接口进行手动创建
- 聊天成员列表的获取:多人聊天会话中,可以点击查看群成员按钮,查看群成员信息
🎀 模块划分
- 参数/配置文件解析模块:基于 gflags 框架直接使用进行参数/配置文件解析。
- 日志模块:基于 spdlog 框架封装的模块直接使用进行日志输出。
- 服务注册模块:基于 etcd 框架封装的注册模块直接使用,进行聊天消息存储子服务的注册。
- 数据库数据操作模块:基于 odb-mysql 数据管理封装的模块,实现数据库中数据的操作。
- 申请好友的时候,根据数据库中的数据判断两人是否已经是好友关系
- 申请好友的时候,根据数据库中的数据判断是否已经申请过好友
- 申请好友的时候,针对两位用户 ID 建立好友申请事件信息
- 好友信息处理的时候,找到申请事件,进行删除。
- 获取待处理好友申请事件的时候,从数据库根据用户 ID 查询出所有的申请信息
- 同意好友申请的时候,需要创建单聊会话,向数据库中插入会话信息
- 从数据库根据指定用户 ID 获取所有好友 ID
- 创建群聊的时候,需要创建群聊会话,向数据库中插入会话信息
- 查看群聊成员的时候,从数据库根据会话 ID 获取所有会话成员 ID
- 获取会话列表的时候,从数据库根据用户 ID 获取到所有会话信息
- 删除好友的时候,从数据库中删除两人的好友关系,以及单聊会话,以及会话成员信息
- rpc 服务模块:基于 brpc 框架搭建 rpc 服务器。
- rpc 服务发现与调用模块:基于 etcd 框架与 brpc 框架封装的服务发现与调用模块,
- 连接用户管理子服务:获取好友列表,会话成员,好友申请事件的时候获取用户信息。
- 连接消息管理子服务:在打开聊天会话的时候,需要获取最近的一条消息进行展示。
- ES 客户端模块:基于 elasticsearch 框架实现访问客户端,从 es 服务器进行用户的关键字搜索(用户信息由用户子服务在用户注册的时候添加进去)
功能模块示意图
🎀 数据管理
数据库数据管理:
根据好友相关操作分析,好友操作相关所需要有以下数据表:
用户信息表
该表由用户操作服务进行创建,并在用户注册时添加数据,好友这里只进行查询。
- 通过用户 ID 获取详细用户信息
用户关系表
因为本身用户服务器已经管理了用户个人信息,因此没必要再整一份用户信息出来,也因为当前用户之间只有好友关系(目前未实现:黑名单,陌生人…),因此这里是一个好友关系表,表示谁和谁是好友
包含字段:
- ID:作为主键
- 用户 ID:
- 好友 ID:
需要注意的是两个用户结为好友时,需要添加 (1,2),(2,1) 两条数据
提供的操作:
- 新增用户关系
- 新增好友,通常伴随着新增会话,新增会话伴随着新增会话成员
- 移除用户关系
- 移除好友,通常伴随着移除会话,移除会话伴随着移除会话成员
- 判断两人是否是好友关系
- 以用户 ID 获取用户的所有好友 ID
- 调用用户子服务,以用户 ID 获取所有好友详细信息
ODB 映射结构
#pragma once
#include <odb/core.hxx>
#include <odb/nullable.hxx>#pragma db object table("relation")
class Relation
{
public:Relation() {}private:friend class odb::access;#pragma db id autounsigned long _id;#pragma db type("varchar(64)") index std::string _user_id;#pragma db type("varchar(64)")std::string _peer_id;
};
会话信息
在多人聊天中,舍弃了群的概念,添加了聊天会话的概念,因为会话既可以是两人单聊会话,也可以是多人聊天会话,这样就可以统一管理了。
包含字段:
- ID:作为主键
- 会话 ID:会话标识
- 会话名称: 单聊会话则设置为’单聊会话’或直接为空就行,因为单聊会话名称就是对方名称,头像就是对方头像
- 会话类型: SINGLE-单聊 / GROUP-多人(单聊由服务器在同意好友时创建,多人由用户申请创建)
提供的操作:
- 新增会话
- 向会话成员表中新增会话成员信息
- 向会话表中新增会话信息
- 删除会话
- 删除会话成员表中的所有会话成员信息
- 删除会话表中的会话信息
- 通过会话 ID,获取会话的详细信息
- 通过用户 ID 获取所有的好友单聊会话(连接会话成员表和用会话表)
- 所需字段:
- 会话 ID
- 会话名称:好友的昵称
- 会话类型:单聊类型
- 会话头像 ID:好友的头像 ID
- 好友 ID:
- 所需字段:
- 通过用户 ID 获取所有自己的群聊会话信息(连接会话成员表和会话表)
- 所需字段:
- 会话 ID,
- 会话名称
- 会话类型:群聊类型
- 所需字段:
ODB 映射结构
#pragma once
#include <odb/core.hxx>
#include <odb/nullable.hxx>enum class session_type_t {SINGLE = 1,GROUP = 2
};#pragma db object
class chat_session {
public:chat_session() {}private:friend class odb::access;#pragma db id autolong int _id;#pragma db unique type("VARCHAR(127)")std::string _session_id;#pragma db type("VARCHAR(127)")odb::nullable<std::string> _session_name;#pragma db type("TINYINT")session_type_t _session_type;
};
会话成员
每个会话中都会有两个及以上的成员,只有两个成员的会话是单聊会话,超过两个是多人聊天会话,为了明确哪个用户属于哪个会话,或者说会话中有哪些成员,因此需要有会话成员的数据管理
包含字段:
- ID:作为主键
- 会话 ID:会话标识
- 用户 ID:用户标识
有了这张表就可以轻松的找出哪个用户属于哪个会话了,也可以根据会话 ID 获取所有成员 ID。
提供的操作:
- 向指定会话中添加单个成员
- 向指定会话中添加多个成员。
- 从指定会话中删除单个成员
- 通过会话 ID,获取会话的所有成员 ID
- 删除会话所有成员:在删除会话的时候使用。
ODB 映射结构
#pragma once
#include <odb/core.hxx>
#include <odb/nullable.hxx>
#pragma db object
class chat_session_member
{
public:
chat_session_member (){}
private:
friend class odb::access;
#pragma db id auto
unsigned long _id;
#pragma db index type("VARCHAR(127)")
std::string _session_id;
#pragma db type("VARCHAR(127)")
std::string _user_id;
};
好友申请事件
在好友的操作中有个操作需要额外的管理,那就是申请好友的事件,因为用户 A 申请用户 B 为好友,并非一次性完成,需要用户 B 对本次申请进行处理,同意后才算是一次完整的流程。而在两次操作之间我们就需要为两次操作建立起相匹配的关系映射。
包含字段:
- ID:作为主键
- 事件 ID
- 请求者用户 ID
- 响应者用户 ID
- 状态:用于表示本次请求的处理阶段,其包含三种状态:待处理-todo,同意-accept,拒绝-reject。
提供的操作:
- 新增好友申请事件:申请的时候新增
- 删除好友申请事件:处理完毕(同意/拒绝)的时候删除
- 获取指定用户的所有待处理事件及关联申请者用户信息(连接用户表)
ODB 映射结构
enum class fevent_status{PENDING = 1,ACCEPT = 2,REJECT = 3
};#pragma db object
class friend_event {
public:friend_event() {}private:friend class odb::access;#pragma db id autolong int _id;#pragma db unique type("VARCHAR(127)")std::string _event_id;#pragma db type("VARCHAR(127)")std::string _req_user_id;#pragma db type("VARCHAR(127)")std::string _rsp_user_id;#pragma db type("TINYINT")fevent_status _status;
};
🎀 接口实现流程
获取好友列表
- 获取请求中的用户 ID
- 根据用户 ID,从数据库中取出该用户所有的好友id
- 从用户子服务批量获取用户信息
- 组织响应,将好友列表返回给网关
申请添加好友
- 取出请求中的请求者 ID,和被请求者 ID
- 判断两人是否已经是好友
- 判断该用户是否已经申请过好友关系
- 向好友申请事件表中,新增申请信息
- 组织响应,将事件 ID 信息响应给网关
获取待处理好友申请事件
- 取出请求中的用户 ID2. 根据用户 ID,从申请事件表&用户表中找到该用户所有状态为 PENDING 的待处
理事件关联申请人用户简息 - 根据申请人用户头像 ID,从文件存储子服务器获取所有用户头像信息,组织用户信息结构
- 组织响应,将申请事件列表响应给网关
处理好友申请
- 取出请求中的申请人 ID,和被申请人 ID,以及处理结果
- 根据两人 ID 在申请事件表中查询判断是否存在申请事件
- 判断两人是否已经是好友(互相加好友的情况)
- 不管拒绝还是同意,删除申请事件表中的事件信息(该事件处理完毕)
- 若同意申请,则向用户关系表中添加好友关系数据,向会话表中新增会话信息,向会话成员表中新增成员信息
- 组织响应,将新生成的会话 ID 响应给网关。
删除好友
- 取出请求中的删除者 ID 和被删除者 ID
- 从用户好友关系表中删除相关关系数据,从会话表中删除单聊会话,从会话成员
表中删除会话成员信息 - 组织响应,返回给网关
搜索好友
- 取出请求中的用户 ID,和搜索关键字
- 从好友关系表中取出该用户所有好友 ID
- 根据关键字从 ES 服务器中进行用户搜索,搜索的时候需要将关键字作为用户 ID/ 手机号/ 昵称的搜索关键字进行搜索,且需要根据自己的 ID 和好友 ID 过滤掉自己和自己的好友。
- 根据搜索到的用户简息中的头像 ID,从文件服务器批量获取用户头像数据
- 组织响应,将搜索到的用户列表响应给网关
创建会话
- 从请求中取出用户 ID 与会话名称,以及会话的成员 ID 列表2. 生成会话 ID,并向会话表中新增会话信息数据,会话为群聊会话(单聊会话是同意好友申请的时候创建的)
- 向会话成员表中新增所有的成员信息
- 组织响应,将组织好的会话信息响应给网关。
获取会话列表
- 从请求中取出用户 ID
- 根据用户 ID,从会话表&会话成员表&用户表中取出好友的单聊会话列表(会话 ID,好友用户 ID,好友昵称,好友头像 ID),并组织会话信息结构对象
- 单聊会话中,对方的昵称就是会话名称,对方的头像就是会话头像,会话类型为单聊类型
- 根据单聊会话 ID,从消息存储子服务获取会话的最后一条消息
- 根据好友头像 ID,从文件存储子服务批量获取好友头像数据,
- 组织好单聊会话结构数据
- 根据用户 ID,从会话表&会话成员表中取出群聊会话列表(会话 ID,会话名称)
- 根据群聊会话 ID,从消息存储子服务获取会话的最后一条消息
- 组织好群聊会话结构数据
- 将单聊会话数据和群聊会话数据组织到一起,响应给网关。
获取会话成员
- 取出请求中用户 ID,和会话 ID
- 根据会话 ID,从会话成员表&用户表中取出所有的成员用户信息
- 根据成员信息中的头像 ID,从文件存储子服务批量获取头像数据组织用户信息结构
- 组织响应,将会话的成员用户信息列表响应给网关
🦋 网关子服务
🎀 功能设计
网关服务器在设计中,最重要的两个功能:
- 作为入口服务器接收客户端的所有请求,进行请求的子服务分发,得到响应后进行响应
- 对客户端进行事件通知(好友申请和处理及删除,单聊/群聊会话创建,新消息)
基于以上的两个功能,因此网关服务器包含两项通信:
- HTTP 通信:进行业务处理
- WEBSOCKET 通信:进行事件通知
模块划分:
- 参数/配置文件解析模块:基于 gflags 框架直接使用进行参数/配置文件解析。
- 日志模块:基于 spdlog 框架封装的模块直接使用进行日志输出。
- rpc 服务发现与调用模块:基于 etcd 框架与 brpc 框架封装的服务发现与调用模块
- 因为要分发处理所有请求,因此所有的子服务都需要进行服务发现。
- redis 客户端模块:基于 redis++封装的客户端进行内存数据库数据操作
- 根据用户子服务添加的会话信息进行用户连接身份识别与鉴权
- HTTP 通信服务器模块:基于 cpp-httplib 库搭建 HTTP 服务器,接收 HTTP 请求进行业务处理。
- WEBSOCKET 服务器模块:基于 Websocketpp 库,搭建 websocket 服务器,进行事件通知。
- 客户端长连接管理模块:建议用户 ID 与长连接句柄映射关系,便于后续根据用户 ID 找到连接进行事件通知
模块功能示意图:
🎀 接口实现流程:
用户名注册
- 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
- 查找用户子服务
- 调用子服务对应接口进行业务处理
- 将处理结果响应给客户端。
用户名登录
- 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
- 查找用户子服务
- 调用子服务对应接口进行业务处理
- 将处理结果响应给客户端。
短信验证码获取
- 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
- 查找用户子服务3. 调用子服务对应接口进行业务处理
- 将处理结果响应给客户端。
手机号码注册
- 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
- 查找用户子服务
- 调用子服务对应接口进行业务处理
- 将处理结果响应给客户端。
手机号码登录
- 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
- 查找用户子服务
- 调用子服务对应接口进行业务处理
- 将处理结果响应给客户端。
用户信息获取
- 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
- 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
- 查找用户子服务
- 调用子服务对应接口进行业务处理
- 将处理结果响应给客户端。
修改用户头像
- 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
- 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
- 查找用户子服务
- 调用子服务对应接口进行业务处理
- 将处理结果响应给客户端。
修改用户签名
- 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
- 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
- 查找用户子服务
- 调用子服务对应接口进行业务处理
- 将处理结果响应给客户端。
修改用户昵称
- 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
- 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
- 查找用户子服务
- 调用子服务对应接口进行业务处理
- 将处理结果响应给客户端。
修改用户绑定手机号
- 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
- 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
- 查找用户子服务
- 调用子服务对应接口进行业务处理
- 将处理结果响应给客户端。
获取好友列表
- 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
- 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
- 查找好友子服务
- 调用子服务对应接口进行业务处理
- 将处理结果响应给客户端。
发送好友申请
- 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
- 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID3. 查找用户子服务
- 根据请求中的用户 ID,调用用户子服务,获取用户的详细信息
- 查找好友子服务
- 调用子服务对应接口进行业务处理
- 若处理成功,则通过被申请人 ID,查找对方长连接
- 若长连接存在(对方在线),则组织好友申请通知进行事件通知
- 将处理结果响应给客户端。
获取待处理好友申请
- 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
- 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
- 查找好友子服务
- 调用子服务对应接口进行业务处理
- 将处理结果响应给客户端。
好友申请处理
- 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
- 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
- 查找用户子服务
- 根据请求中的用户 ID,调用用户子服务,获取申请人与被申请人的详细信息
- 查找好友子服务
- 调用子服务对应接口进行业务处理
- 若处理成功,则通过申请人 ID,查找申请人长连接,进行申请处理结果的通知
- 若处理结果是同意,则意味着新聊天会话的创建,则对申请人继续进行聊天会话创建通知
- 从处理结果中取出会话 ID,使用对方的昵称作为会话名称,对方的头像作为会话头像组织会话信息
- 若处理结果是同意,则对当前处理者用户 ID 查找长连接,进行聊天会话创建的通知
- 从处理结果中取出会话 ID,使用对方的昵称作为会话名称,对方的头像作为会话头像组织会话信息
- 清理响应中的会话 ID 信息,
- 若处理结果是同意,则意味着新聊天会话的创建,则对申请人继续进行聊天会话创建通知
- 将处理结果响应给客户端
删除好友
- 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
- 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
- 查找好友子服务
- 调用子服务对应接口进行业务处理
- 若处理成功,则通过被删除者用户 ID,查找对方长连接
- 若长连接存在(对方在线),则组织好友删除通知进行事件通知
- 将处理结果响应给客户端。
搜索用户
- 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
- 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
- 查找好友子服务
- 调用子服务对应接口进行业务处理
- 将处理结果响应给客户端。
获取用户聊天会话列表
- 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
- 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
- 查找好友子服务
- 调用子服务对应接口进行业务处理
- 将处理结果响应给客户端。
创建多人聊天会话
- 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
- 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
- 查找好友子服务4. 调用子服务对应接口进行业务处理
- 若处理成功,循环根据会话成员的 ID 找到他们的长连接,
- 根据响应中的会话信息,逐个进行会话创建的通知
- 清理响应中的会话信息
- 将处理结果响应给客户端。
获取消息会话成员列表
- 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
- 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
- 查找好友子服务
- 调用子服务对应接口进行业务处理
- 将处理结果响应给客户端。
发送新消息
- 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
- 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
- 查找消息转发子服务
- 调用子服务对应接口进行业务处理
- 若处理成功,则根据处理结果中的用户 ID 列表,循环找到目标长连接,根据处理结果中的消息字段组织新消息通知,逐个对目标进行新消息通知。
- 若处理失败,则根据处理结果中的错误提示信息,设置响应内容
- 将处理结果响应给客户端。
获取指定时间段消息列表
- 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
- 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
- 查找消息存储子服务
- 调用子服务对应接口进行业务处理
- 将处理结果响应给客户端。
获取最近 N 条消息列表
- 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
- 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
- 查找消息存储子服务
- 调用子服务对应接口进行业务处理
- 将处理结果响应给客户端。
搜索关键字历史消息
- 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
- 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
- 查找消息存储子服务
- 调用子服务对应接口进行业务处理
- 将处理结果响应给客户端。
单个文件数据获取
- 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
- 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
- 查找文件子服务
- 调用子服务对应接口进行业务处理
- 将处理结果响应给客户端。
多个文件数据获取
- 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
- 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
- 查找文件子服务
- 调用子服务对应接口进行业务处理
- 将处理结果响应给客户端。
单个文件数据上传
- 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
- 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
- 查找文件子服务
- 调用子服务对应接口进行业务处理
- 将处理结果响应给客户端。
多个文件数据上传
- 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
- 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
- 查找文件子服务
- 调用子服务对应接口进行业务处理
- 将处理结果响应给客户端。
语音转文字
- 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
- 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
- 查找语音子服务
- 调用子服务对应接口进行业务处理
- 将处理结果响应给客户端
五:🔥 后台服务器部署
🦋 Docker 简介与安装
docker 是一个用 Go 语言实现的应用容器引擎开源项目,可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化。其广泛应用于开发、测试和生产环境中,帮助开发者和系统管理员简化应用的部署和管理,实现快速的交付,测试和部署。
🎀 docker 安装
- 安装 docker 依赖
sudo apt-get install ca-certificates curl gnupg lsb-release
- 配置加速地址
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"]
}sudo systemctl daemon-reload
- 添加 Docker 官方 GPG 密钥
curl -fsSL http://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg | sudo apt-key add -
- 添加 Docker 软件源
sudo add-apt-repository "deb [arch=amd64]
http://mirrors.aliyun.com/docker-ce/linux/ubuntu $(lsb_release -cs) stable"
- 安装 Docker
sudo apt-get install docker-ce docker-ce-cli containerd.io
- 安装 docker-compose
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
- 配置用户组
sudo groupadd docker
sudo gpasswd -a $USER docker
newgrp docker
- 测试-查看版本
docker version
🎀 docker 常用指令
容器操作
查看
docker ps -a
docker container ls -a
# -a 所有容器
# -q 仅显示容器 ID
删除
docker rm container_id
docker container rm container_id
docker container rm $(docker container ls -a -q)
启动
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
停止
docker container stop container_id
docker container stop $(docker container ls -a -q)
镜像操作
拉取
docker pull image_name:version
查看
docker images
docker image ls -a
创建
docker build [options] path
# options:
# -f, --file string : 指定构建镜像的 dockerfile
# -t, --tag stringArray : 设置镜像名称与版本 -t myImage:version
删除
docker rmi image_id
docker image rm image_id
导入/导出
docker save -o myimage.tar myimage:latest
docker load -i myimage.tar
缓存及镜像清理
docker system df
docker system prune -a
🎀 dockerfile 编写规则简介
样例:
FROM ubuntu:22.04
LABEL MAINTAINER="bitejiuyeke"ENV REDIS_VERSION=5.0.3WORKDIR /imRUN mkdir -p /im/data &&\mkdir -p /im/logs &&\mkdir -p /im/conf &&\mkdir -p /im/binCOPY 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"
注释:
- FROM : 注明所使用的镜像
- LABEL :构建镜像时设置键值对,这里 MAINTAINER 表示维护人
- ENV :用于设置镜像中的环境变量
- WORKDIR :设定镜像中的工作路径(该目录会在镜像系统中自动创建)
- RUN :在镜像中执行的指令
- COPY :将宿主机中的文件拷贝到镜像系统指定路径下
- EXPOSE :对外暴露端口
- CMD : 设置容器启动时默认执行的命令
docker compose 编写规则简介
样例:
version: "3.8"
services:mysql:image: mysql:8.0.39container_name: docker-msyql8-servicevolumes:- ./sql/:/docker-entrypoint-initdb.d/- ./middleware/mysql/data:/var/lib/mysql:rw- ./middleware/mysql/logs:/var/log/mysql:rw- ./conf/mysql:/etc/mysql/environment:MYSQL_ROOT_PASSWORD: 123456ports:- 3306:3306restart: alwaysdepends_on:- etcd
注释:
- version : docker-compose 语法版本
- services : 要启动的服务
- mysql : 第 4 行的 mysql 为对应的服务名称,在这里其实就是个标识
- image:来源镜像及版本
- container_name:用户设定的容器名称
- volumes:挂载卷,其实就是将宿主机上的指定文件或目录与镜像机进行挂载。
- environment:设定镜像机中的环境变量
- ports:宿主机与镜像机的端口映射
- restart:容器重启策略, always 表示无论退出状态如何,容器总是重新启动。
- depends_on:启动依赖,通过依赖关系控制容器启动顺序。
🦋 项目部署
🎀 编写项目配置文件
在项目的各个子服务中,每个子服务可能都会有不同的配置,代码中我们通过 gflags进行了参数解析,但是如果改换了部署的机器,就需要修改代码中的数据,然后重新编译代码,这是一件非常麻烦的事情,会导致项目的自动部署成为空谈,幸好, gflags不仅支持参数的解析,也支持配置文件的解析,因此我们需要将代码中需要的参数通过配置文件来进行配置。
#程序的运行模式, false-调试; true-发布; );
-run_mode=false
#发布模式下,用于指定日志的输出文件);
-log_file=/im/logs/file.log
#发布模式下,用于指定日志输出等级);
-log_level=0
#服务注册中心地址);
-registry_host=http://10.0.0.235:2379#服务监控根目录);
-base_service=/service
#当前实例名称);
-instance_name=/file_service/instance
-instance_name=/friend_service/instance
-instance_name=/message_service/instance
-instance_name=/speech_service/instance
-instance_name=/transmite_service/instance
-instance_name=/user_service/instance
#当前实例的外部访问地址);
-access_host=10.0.0.235:10002
#Rpc 服务器监听端口);
-listen_port=10002
#Rpc 调用超时时间);
-rpc_timeout=-1
#Rpc 的 IO 线程数量);
-rpc_threads=1
#, Mysql 服务器访问地址);
-mysql_host=10.0.0.235
#, Mysql 服务器访问用户名);
-mysql_user=root
#, Mysql 服务器访问密码);
-mysql_pswd=123456
#, Mysql 默认库名称);
-mysql_db=bite_im
#, Mysql 客户端字符集);
-mysql_cset=utf8
#, Mysql 服务器访问端口);
-mysql_port=3306
#, Mysql 连接池最大连接数量);
-mysql_pool_count=4
-es_host=http://10.0.0.235:9200/
#, Redis 服务器访问地址);
-redis_host=10.0.0.235
#, Redis 服务器访问端口);
-redis_port=6379
#, Redis 默认库号);
-redis_db=0
#, Redis 长连接保活选项);-redis_keep_alive=true
#, 消息队列服务器访问用户名);
-mq_user=root
#, 消息队列服务器访问密码);
-mq_pswd=123456
#, 消息队列服务器访问地址);
-mq_host=10.0.0.235:5672
#, 持久化消息的发布交换机名称);
-mq_msg_exchange=msg_exchange
#, 持久化消息的发布队列名称);
-mq_msg_queue=msg_queue
#, 交换机与队列的绑定 key);
-mq_msg_binding_key=msg_queue
#, 短信平台密钥 ID);
-dms_key_id=xxx
#, 短信平台密钥);
-dms_key_secret=xxx
#, 语音平台应用 ID);
-app_id=xxx
#, 语音平台 API 密钥);
-api_key=xxx
#, 语音平台加密密钥);
-secret_key=xxx
-file_service=/service/file_service
#, 好友管理子服务名称);
-friend_service=/service/friend_service
#, 消息存储子服务名称);
-message_service=/service/message_service
#, 用户管理子服务名称);
-user_service=/service/user_service
#, 语音识别子服务名称);
-speech_service=/service/speech_service
#, 转发管理子服务名称);
-transmite_service=/service/transmite_service
🎀 查询程序依赖
在我们的子服务中,所采用的 docker 镜像与我们的开发环境保持一致,使用ubuntu:22.04,但是这个镜像是一个空白的镜像,因此我们需要针对这个空白的 ubuntu 镜像进行改造,搭建我们服务的运行环境。
注意,我们要搭建的是运行环境,而不是开发环境,也就是说只需要镜像中包含有我们程序运行所需的动态库即可,因此我们需要查找到我们子服务程序的依赖库,并将其拷贝到镜像中。
#!/bin/bash
deplist=$( ldd $1 | awk '{if (match($3,"/")){ print $3}}' )
cp -Lr $deplist $2
注释:
- ldd :该 shell 指令的作用是查看指定程序的库依赖信息
- awk:这是一个功能复杂的指令,当前用于进行字符串分割,并获取指定列
- cp:用于文件拷贝
- -L : 跟踪软连接文件,即若依赖库是软连接文件则跟踪到实际文件进行拷贝
- -r : 递归处理
🎀 编写 dockerfile
编写每个子服务的 dockerfile,用于构造 docker 镜像。
# 声明基础镜像来源
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
🎀 编写 entrypoint.sh
包含中间件在内,我们共有 13 个服务需要启动,这些服务之间会存在一些依赖关系,比如 user_service 启动之前,必须保证 mysql, redis, etcd 这些中间件已经启动才可以,因此我们需要做一些启动的顺序控制。
但是单纯 yaml 配置文件中的 depends 无法满足需求,因为它只能控制容器的启动顺序,无法控制容器内程序的启动顺序,因此,我们需要通过端口探测的方式进行程序运行的控制。
#!/bin/bash#./entrypoint.sh -h 127.0.0.1 -p 3306,2379,6379 -c '/im/bin/file_server -flagfile=./xx.conf'# 1. 编写一个端口探测函数,端口连接不上则循环等待
# wait_for 127.0.0.1 3306
wait_for() {while ! nc -z $1 $2 doecho "$2 端口连接失败,休眠等待!";sleep 1;doneecho "$1:$2 检测成功!";
}
# 2. 对脚本运行参数进行解析,获取到ip,port,command
declare ip
declare ports
declare command
while getopts "h:p:c:" arg
docase $arg inh)ip=$OPTARG;;p)ports=$OPTARG;;c)command=$OPTARG;;esac
done
# 3. 通过执行脚本进行端口检测
# ${port //,/ } 针对port中的内容,以空格替换字符串中的, shell中数组--一种以空格间隔的字符串
for port in ${ports//,/ }
dowait_for $ip $port
done
# 4. 执行command
# eval 对一个字符串进行二次检测,将其当作命令进行执行
eval $command
注释:
- nc 指令: nc(Netcat)是一个功能强大的网络工具, -z 选项用于扫描监听中的守护进程,也就是扫描远程主机上的开放端口。当与 nc 命令一起使用时, -z 选项会让 nc 尝试连接到指定的端口,但不会发送任何数据或接收任何响应。这通常用于端口扫描,以检查远程主机上的端口是否开放。
- declare :声明变量
- getopts :捕获运行参数
- eval :用于将字符串作为命令来执行。
🎀 编写 docker-compose
包含中间件在内,我们共有 13 个服务需要启动,若一一都需要手动启动,会比较麻烦,因此我们使用 docker-compose 进行统一管理启动。
version: "3.8"services:etcd:image: quay.io/coreos/etcd:v3.3.25container_name: etcd-serviceenvironment:- 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:2379volumes:# 1. 希望容器内的程序能够访问宿主机上的文件# 2. 希望容器内程序运行所产生的数据文件能落在宿主机上- ./middle/data/etcd:/var/lib/etcd:rwports:- 2379:2379restart: alwaysmysql:image: mysql:8.0.42container_name: mysql-serviceenvironment:MYSQL_ROOT_PASSWORD: 502502volumes:# 1. 希望容器内的程序能够访问宿主机上的文件# 2. 希望容器内程序运行所产生的数据文件能落在宿主机上- ./sql:/docker-entrypoint-initdb.d/:rw- ./middle/data/mysql:/var/lib/mysql:rwports:- 3306:3306restart: alwaysredis:image: redis:6.0.16container_name: redis-servicevolumes:# 1. 希望容器内的程序能够访问宿主机上的文件# 2. 希望容器内程序运行所产生的数据文件能落在宿主机上- ./middle/data/redis:/var/lib/redis:rwports:- 6379:6379restart: alwayselasticsearch:image: elasticsearch:7.17.21container_name: elasticsearch-serviceenvironment:- "discovery.type=single-node"volumes:# 1. 希望容器内的程序能够访问宿主机上的文件# 2. 希望容器内程序运行所产生的数据文件能落在宿主机上- ./middle/data/elasticsearch:/data:rwports:- 9200:9200- 9300:9300restart: alwaysrabbitmq:image: rabbitmq:3.9.13container_name: rabbitmq-serviceenvironment:RABBITMQ_DEFAULT_USER: rootRABBITMQ_DEFAULT_PASS: 502502volumes:# 1. 希望容器内的程序能够访问宿主机上的文件# 2. 希望容器内程序运行所产生的数据文件能落在宿主机上- ./middle/data/rabbitmq:/var/lib/rabbitmq:rwports:- 5672:5672restart: alwaysfile_server:build: ./file#image: server-user_servercontainer_name: file_server-servicevolumes:# 1. 希望容器内的程序能够访问宿主机上的文件# 2. 希望容器内程序运行所产生的数据文件能落在宿主机上# 挂载的信息: entrypoint.sh文件 数据目录(im/logs, im/data), 配置文件- ./conf/file_server.conf:/im/conf/file_server.conf- ./middle/data/logs:/im/logs:rw- ./middle/data/data:/im/data:rw- ./entrypoint.sh:/im/bin/entrypoint.shports:- 10002:10002restart: alwaysentrypoint:# 跟dockerfile中的cmd比较类似,都是容器启动后的默认操作--替代dockerfile中的cmd/im/bin/entrypoint.sh -h 10.1.12.8 -p 2379 -c "/im/bin/file_server -flagfile=/im/conf/file_server.conf"depends_on:- etcdfriend_server:build: ./friend#image: file-server:v1container_name: friend_server-servicevolumes:# 1. 希望容器内的程序能够访问宿主机上的文件# 2. 希望容器内程序运行所产生的数据文件能落在宿主机上# 挂载的信息: entrypoint.sh文件 数据目录(im/logs, im/data), 配置文件- ./conf/friend_server.conf:/im/conf/friend_server.conf- ./middle/data/logs:/im/logs:rw- ./middle/data/data:/im/data:rw- ./entrypoint.sh:/im/bin/entrypoint.shports:- 10006:10006restart: alwaysdepends_on:- etcd- mysql- elasticsearchentrypoint:# 跟dockerfile中的cmd比较类似,都是容器启动后的默认操作--替代dockerfile中的cmd/im/bin/entrypoint.sh -h 10.1.12.8 -p 2379,3306,9200 -c "/im/bin/friend_server -flagfile=/im/conf/friend_server.conf"gateway_server:build: ./gateway#image: file-server:v1container_name: gateway_server-servicevolumes:# 1. 希望容器内的程序能够访问宿主机上的文件# 2. 希望容器内程序运行所产生的数据文件能落在宿主机上# 挂载的信息: entrypoint.sh文件 数据目录(im/logs, im/data), 配置文件- ./conf/gateway_server.conf:/im/conf/gateway_server.conf- ./middle/data/logs:/im/logs:rw- ./middle/data/data:/im/data:rw- ./entrypoint.sh:/im/bin/entrypoint.shports:- 9000:9000- 9001:9001restart: alwaysdepends_on:- etcd- redisentrypoint:# 跟dockerfile中的cmd比较类似,都是容器启动后的默认操作--替代dockerfile中的cmd/im/bin/entrypoint.sh -h 10.1.12.8 -p 2379,6379 -c "/im/bin/gateway_server -flagfile=/im/conf/gateway_server.conf"message_server:build: ./message#image: file-server:v1container_name: message_server-servicevolumes:# 1. 希望容器内的程序能够访问宿主机上的文件# 2. 希望容器内程序运行所产生的数据文件能落在宿主机上# 挂载的信息: entrypoint.sh文件 数据目录(im/logs, im/data), 配置文件- ./conf/message_server.conf:/im/conf/message_server.conf- ./middle/data/logs:/im/logs:rw- ./middle/data/data:/im/data:rw- ./entrypoint.sh:/im/bin/entrypoint.shports:- 10005:10005restart: alwaysdepends_on:- etcd- mysql- elasticsearch- rabbitmqentrypoint:# 跟dockerfile中的cmd比较类似,都是容器启动后的默认操作--替代dockerfile中的cmd/im/bin/entrypoint.sh -h 10.1.12.8 -p 2379,3306,9200,5672 -c "/im/bin/message_server -flagfile=/im/conf/message_server.conf"speech_server:build: ./speech#image: file-server:v1container_name: speech_server-servicevolumes:# 1. 希望容器内的程序能够访问宿主机上的文件# 2. 希望容器内程序运行所产生的数据文件能落在宿主机上# 挂载的信息: entrypoint.sh文件 数据目录(im/logs, im/data), 配置文件- ./conf/speech_server.conf:/im/conf/speech_server.conf- ./middle/data/logs:/im/logs:rw- ./middle/data/data:/im/data:rw- ./entrypoint.sh:/im/bin/entrypoint.shports:- 10001:10001restart: alwaysdepends_on:- etcdentrypoint:# 跟dockerfile中的cmd比较类似,都是容器启动后的默认操作--替代dockerfile中的cmd/im/bin/entrypoint.sh -h 10.1.12.8 -p 2379 -c "/im/bin/speech_server -flagfile=/im/conf/speech_server.conf"transmite_server:build: ./transmite#image: file-server:v1container_name: transmite_server-servicevolumes:# 1. 希望容器内的程序能够访问宿主机上的文件# 2. 希望容器内程序运行所产生的数据文件能落在宿主机上# 挂载的信息: entrypoint.sh文件 数据目录(im/logs, im/data), 配置文件- ./conf/transmite_server.conf:/im/conf/transmite_server.conf- ./middle/data/logs:/im/logs:rw- ./middle/data/data:/im/data:rw- ./entrypoint.sh:/im/bin/entrypoint.shports:- 10004:10004restart: alwaysdepends_on:- etcd- mysql- rabbitmqentrypoint:# 跟dockerfile中的cmd比较类似,都是容器启动后的默认操作--替代dockerfile中的cmd/im/bin/entrypoint.sh -h 10.1.12.8 -p 2379,3306,5672 -c "/im/bin/transmite_server -flagfile=/im/conf/transmite_server.conf"user_server:build: ./user#image: file-server:v1container_name: user_server-servicevolumes:# 1. 希望容器内的程序能够访问宿主机上的文件# 2. 希望容器内程序运行所产生的数据文件能落在宿主机上# 挂载的信息: entrypoint.sh文件 数据目录(im/logs, im/data), 配置文件- ./conf/user_server.conf:/im/conf/user_server.conf- ./middle/data/logs:/im/logs:rw- ./middle/data/data:/im/data:rw- ./entrypoint.sh:/im/bin/entrypoint.shports:- 10003:10003restart: alwaysdepends_on:- etcd- mysql- redis- elasticsearchentrypoint:# 跟dockerfile中的cmd比较类似,都是容器启动后的默认操作--替代dockerfile中的cmd/im/bin/entrypoint.sh -h 10.1.12.8 -p 2379,3306,5672,9200 -c "/im/bin/user_server -flagfile=/im/conf/user_server.conf"
六:🔥 共勉
😋 以上就是我对 微服务即时通讯系统:服务端
的理解, 觉得这篇博客对你有帮助的,可以点赞收藏关注支持一波~ 😉