文章目录
- 微服务即时通讯系统(服务端)------网关服务设计与实现(7)
-
- 一、整体架构设计思路
-
- [1.1 为什么网关要这么设计?](#1.1 为什么网关要这么设计?)
- [1.2 核心设计决策](#1.2 核心设计决策)
- 二、代码框架详解
-
- [2.1 主类结构:GateWayServer](#2.1 主类结构:GateWayServer)
- [2.2 初始化过程](#2.2 初始化过程)
- 三、核心功能实现逻辑
-
- [3.1 用户连接怎么管理?](#3.1 用户连接怎么管理?)
- [3.2 HTTP请求怎么处理?](#3.2 HTTP请求怎么处理?)
- [3.3 事件通知怎么发送?](#3.3 事件通知怎么发送?)
- 四、关键技术设计解析
-
- [4.1 为什么用Redis存会话?](#4.1 为什么用Redis存会话?)
- [4.2 连接保活机制](#4.2 连接保活机制)
- [4.3 服务发现怎么工作?](#4.3 服务发现怎么工作?)
- 五、构建器模式的作用
- 六、实际工作流程举例
- 七、设计总结
- 八、具体使用与部署设计
-
- [8.1 配置文件解析](#8.1 配置文件解析)
- [8.1.1 实现](#8.1.1 实现)
微服务即时通讯系统(服务端)------网关服务设计与实现(7)
MicrCommSys: 微通信系统 - 消息存储微服务等项目
一、整体架构设计思路
1.1 为什么网关要这么设计?
想象一下咱们这个即时通讯系统,客户端要和服务端交互,有几十种不同的功能:登录注册、发消息、加好友、传文件等等。如果让客户端直接去连每个服务,会特别混乱,就像去一个办公楼找不同部门,每个部门都要单独敲门。
所以我们设计了网关服务,它就相当于大楼的前台接待处:
- HTTP部分:像接待窗口,处理客户的业务办理(填表、交材料)
- WebSocket部分:像广播喇叭,有紧急通知时就广播给相关的人
1.2 核心设计决策
双协议设计的原因:
- 普通业务请求(登录、发消息)用HTTP:简单、成熟,有现成的库支持
- 实时通知(新消息、好友申请)用WebSocket:需要长连接,能主动推送给客户端
举个例子:你给朋友发消息
- 你先通过HTTP把消息内容发给网关
- 网关转发给消息服务处理
- 处理完后,网关通过WebSocket告诉你朋友:"有新消息了"
这样设计的好处是各司其职,HTTP负责请求响应,WebSocket负责实时推送。
二、代码框架详解
2.1 主类结构:GateWayServer
看看咱们的GateWayServer类,它把整个网关的功能都组织在一起:
cpp
class GateWayServer {
// 两个服务器
std::shared_ptr<WSocketppTool::WS_Server> webs_server_; // WebSocket服务器
std::shared_ptr<httplib::Server> http_server_; // HTTP服务器
// 三个管理器
std::shared_ptr<Connection> connetion_manager_; // 管谁在线、谁连上了
std::shared_ptr<bite_im::Session> redis_session_; // 管登录会话
std::shared_ptr<bite_im::Status> redis_status_; // 管用户状态
// 服务发现相关
std::shared_ptr<brpcChannelTool::ServiceManager> service_manager_; // 找服务的
std::shared_ptr<Etcd_Tool::MonitorEtcd> discovery_; // 监控服务状态
};
2.2 初始化过程
看看构造函数里做了什么:
cpp
GateWayServer(int32_t http_port, int32_t ws_port, ...) {
// 1. 创建连接管理器
connetion_manager_ = std::make_shared<Connection>();
// 2. 初始化WebSocket服务器
webs_server_ = std::make_shared<WSocketppTool::WS_Server>(ws_port);
webs_server_->ws_server_.set_open_handler(std::bind(&GateWayServer::_onOpen, this, ...));
webs_server_->ws_server_.set_close_handler(std::bind(&GateWayServer::_onClose, this, ...));
// 设置了4个关键回调函数
// 3. 初始化HTTP服务器
http_server_ = std::make_shared<httplib::Server>();
http_server_->Post(USER_GET_VERIFY_CODE, std::bind(&GateWayServer::GetPhoneVerifyCode, this, ...));
http_server_->Post(USER_USERNAME_REGISTER, std::bind(&GateWayServer::UserRegister, this, ...));
// ... 注册了20多个接口处理函数
// 4. 启动HTTP服务线程
http_thread_ = std::thread([this, http_port](){
http_server_->listen("0.0.0.0", http_port);
});
http_thread_.detach(); // 后台运行
}
这里有个关键点:HTTP和WebSocket是分开运行的。HTTP在独立线程里跑,WebSocket在主线程里跑。这样不会互相影响。
三、核心功能实现逻辑
3.1 用户连接怎么管理?
当用户打开App时,会先建立WebSocket连接。看看这个过程:
cpp
void _onMsg(websocketpp::connection_hdl con_hdl, message_ptr msg_ptr) {
// 1. 客户端发来认证数据
std::string data = msg_ptr->get_payload();
// 2. 解析出session_id(登录时拿到的)
ClientAuthenticationReq req;
req.ParseFromString(data);
std::string user_ssid = req.session_id();
// 3. 用session_id去Redis查用户ID
sw::redis::OptionalString uid = redis_session_->uid(user_ssid);
// 4. 建立映射关系:用户ID ↔ 连接
bool ret = connetion_manager_->insert(ptr, *uid, user_ssid);
// 5. 启动保活定时器
keepAlive(ptr);
}
这里的设计逻辑:
- 用户必须先登录(拿到session_id)才能连WebSocket
- 通过session_id就能知道是谁,不需要重复登录
- 连接和用户绑定,以后就能通过用户ID找到连接
3.2 HTTP请求怎么处理?
以"获取用户信息"这个接口为例:
cpp
void GetUserInfo(const httplib::Request& request, httplib::Response& response) {
// 1. 解析请求
GetUserInfoReq req;
req.ParseFromString(request.body);
// 2. 鉴权:检查session_id是否有效
std::string ssid = req.session_id();
sw::redis::OptionalString uid = redis_session_->uid(ssid);
if(!uid) { // 查不到就是没登录或session过期
// 返回错误
return;
}
// 3. 把用户ID设置到请求里
req.set_user_id(*uid);
// 4. 找到用户服务(可能有很多个实例)
auto channel = service_manager_->Choose(user_service_name_);
// 5. 调用用户服务的接口
bite_im::UserService_Stub stub(channel.get());
stub.GetUserInfo(&cntl, &req, &rsp, nullptr);
// 6. 返回结果
response.set_content(rsp.SerializeAsString(), "application/x-protobuf");
}
关键设计点:
- 所有需要登录的接口都先鉴权再处理
- 鉴权失败就直接返回,不往后端转发
- 网关不处理业务逻辑,只做转发
3.3 事件通知怎么发送?
以"发送好友申请"为例,这里比较有意思:
cpp
void FriendAdd(...) {
// 1. 先处理业务:保存好友申请到数据库
stub.FriendAdd(&cntl, &req, &rsp, nullptr);
// 2. 如果成功了,要给对方发通知
if(rsp.success()) {
// 找到对方的连接
auto con_ptr = connetion_manager_->getConptr(req.respondent_id());
if(con_ptr) { // 对方在线才发
// 组织通知消息
NotifyMessage notifymsg;
notifymsg.set_notify_type(FRIEND_ADD_APPLY_NOTIFY);
// 通过WebSocket发过去
websocketpp::lib::error_code ec =
con_ptr->send(notifymsg.SerializeAsString(), ...);
}
}
}
这里的逻辑:
- 先处理完业务(数据库操作)
- 再检查对方是否在线
- 在线才发通知,不在线就等下次上线再提醒
- 业务处理和通知是分离的,互不影响
四、关键技术设计解析
4.1 为什么用Redis存会话?
考虑这样一个场景:用户用手机和电脑同时登录。
cpp
// Redis里大概是这样存的
Key: session:abc123xyz
Value: user_1001
Expire: 7天
Key: session:def456uvw
Value: user_1001 // 同一个用户,不同设备
Expire: 7天
用Redis的好处:
- 共享:所有网关实例都能访问,支持水平扩展
- 快速:内存操作,比查数据库快得多
- 自动清理:设置过期时间,不用写代码清理
- 简单:就相当于一个大Map,用起来方便
4.2 连接保活机制
WebSocket连接可能因为网络问题断掉,所以要有保活:
cpp
void keepAlive(connection_ptr con_ptr) {
// 每60秒发一次ping
webs_server_->ws_server_.set_timer(60000, [this, weak_con](){
auto con_ptr = weak_con.lock(); // 弱指针,防止内存泄漏
if(con_ptr && con_ptr->get_state() == open) {
con_ptr->ping(""); // 发心跳
keepAlive(con_ptr); // 继续下一轮
} else {
// 连接已经断了,不继续了
}
});
}
设计考虑:
- 用弱指针,连接断了能自动释放
- 60秒比较合适,太频繁浪费流量,太久检测不到断线
- 递归调用,一直保活到连接断开
4.3 服务发现怎么工作?
咱们系统有很多微服务:用户服务、消息服务、好友服务等等。这些服务可能有多台机器,网关怎么知道找谁?
cpp
// 服务发现的工作流程
1. 用户服务启动 → 向etcd注册:"我在192.168.1.100:8000"
2. 网关监控etcd → 发现新服务
3. 网关建立连接 → 保存到service_manager_
4. 有请求时 → 从多个实例里选一个
// 具体代码
auto channel = service_manager_->Choose(user_service_name_);
// service_manager_里可能有:
// - 192.168.1.100:8000
// - 192.168.1.101:8000
// - 192.168.1.102:8000
// 它会选一个压力小的
这样做的好处:
- 动态扩展:加机器不用改网关配置
- 故障转移:一台挂了自动换另一台
- 负载均衡:请求分散到多台机器
五、构建器模式的作用
看咱们的GateWayServerBuilder,为什么要这么设计?
cpp
// 不用构建器的话,初始化要这样:
auto gateway = new GateWayServer(8080, 8081, redis_client,
service_manager, discovery,
speech_service_name, ...);
// 参数太多,容易弄错顺序
// 用构建器后:
GateWayServerBuilder builder;
builder.make_redis_object("127.0.0.1", 6379, ...);
builder.make_channel_manager_object(...);
builder.make_discovery_object("127.0.0.1:2379", ...);
builder.make_http_port(8080);
builder.make_websocket_port(8081);
auto gateway = builder.build();
构建器的优点:
- 清晰:一眼就知道配了什么
- 灵活:想配什么就配什么,不强制顺序
- 安全:build()时会检查配置是否完整
- 可读:像读说明书一样
六、实际工作流程举例
场景:小明给小红发消息
-
HTTP请求阶段:
text小明手机 → 网关HTTP(8080): "我要发消息,session_id=xyz123,内容=hello" 网关检查 → Redis:"xyz123对应user_1001" 网关转发 → 消息服务:"user_1001要发消息,内容=hello" 消息服务处理 → 存数据库,返回成功 网关返回 → 小明手机:"发送成功" -
WebSocket通知阶段:
text消息服务告诉网关:"消息存好了,要通知小红(user_1002)" 网关检查 → connetion_manager_:"user_1002在线吗?" 找到连接 → 小红手机的WebSocket连接 网关推送 → 小红手机:"你有新消息,来自小明" 小红手机 → 显示新消息提醒
场景:用户断线重连
-
网络断开:
text网关发现 → ping不通了 调用_onClose清理: - 从connetion_manager_删除映射 - 从Redis删除在线状态 -
重新连接:
text用户重连 → 建立新WebSocket 发送认证 → 还是用原来的session_id 网关验证 → Redis里session还在(没过期) 重新绑定 → 建立新映射关系
七、设计总结
网关设计的几个核心思想:
- 单一入口:所有请求都走网关,好管理、好监控
- 职责分离:HTTP管请求,WebSocket管推送,各干各的
- 无状态:网关自己不存用户数据,全放Redis
- 只转发不处理:业务逻辑交给专门的服务
- 实时感知:谁在线、谁断线,心里有数
这样做的好处:
对客户端:只要连网关一个地方,简单
对后端服务:网关帮忙做了鉴权、限流、负载均衡
对运维:监控一个点就知道整体状态
对开发:加新功能只要加接口,不用改网关核心
这个设计让整个系统像邮局一样工作:
- 你来寄信(HTTP请求),我帮你送到(转发到对应服务)
- 你有新信(事件通知),我打电话告诉你(WebSocket推送)
- 我不关心信里写什么(业务逻辑),我只管送(转发)
八、具体使用与部署设计
8.1 配置文件解析
咱们这个网关服务提供了灵活的配置方式,主要通过命令行参数来配置。看看 main.cpp 里的配置设计:
cpp
//日志模式
DEFINE_bool(log_mode, false, "日志模式: false(调试模式,默认), true(发布模式)");
DEFINE_bool(log_output_mode, false, "控制台模式: false, 文件模式:true");
DEFINE_string(log_file, "app.log", "发布模式下日志名称");
DEFINE_int32(log_level, 2, "发布模式下日志等级设置");
//etcd服务发现
DEFINE_string(registry_host, "http://127.0.0.1:2379", "etcd服务注册中心地址");
//子服务
DEFINE_string(file_service_name, "/service/file_service", "文件子服务在etcd的文件位置");
DEFINE_string(user_service_name , "/service/user_service", "用户子服务在etcd的文件位置");
DEFINE_string(message_service_name , "/service/message_service", "消息子服务在etcd的文件位置");
DEFINE_string(transmite_service_name, "/service/transmite_service", "消息转发子服务在etcd的文件位置");
DEFINE_string(speech_service_name , "/service/speech_service", "语音子服务在etcd的文件位置");
DEFINE_string(friend_service_name , "/service/friend_service", "好友子服务在etcd的文件位置");
//redis初始化数据
DEFINE_string(redis_host, "127.0.0.1", "redis主机地址");
DEFINE_int32(redis_port, 6379, "redis主机端口");
DEFINE_int32(redis_db, 0, "redis数据库编号");
DEFINE_int64(redis_timeout, 10000, "redis链接超时时间");
DEFINE_bool(redis_keepalive, true, "redis是否保活");
//端口
DEFINE_int32(http_port, 9000, "http端口");
DEFINE_int32(ws_port, 9001, "websocket端口");
8.1.1 实现
cpp
#include "gateway_server.hpp"
#include <chrono>
#include <gflags/gflags.h>
#include <butil/logging.h>
#include <memory>
#include <spdlog/common.h>
//日志模式
DEFINE_bool(log_mode, false, "日志模式: false(调试模式,默认), true(发布模式)");
DEFINE_bool(log_output_mode, false, "控制台模式: false, 文件模式:true");
DEFINE_string(log_file, "app.log", "发布模式下日志名称");
DEFINE_int32(log_level, 2, "发布模式下日志等级设置");
//etcd服务发现
DEFINE_string(registry_host, "http://127.0.0.1:2379", "etcd服务注册中心地址");
//子服务
DEFINE_string(file_service_name, "/service/file_service", "文件子服务在etcd的文件位置");
DEFINE_string(user_service_name , "/service/user_service", "用户子服务在etcd的文件位置");
DEFINE_string(message_service_name , "/service/message_service", "消息子服务在etcd的文件位置");
DEFINE_string(transmite_service_name, "/service/transmite_service", "消息转发子服务在etcd的文件位置");
DEFINE_string(speech_service_name , "/service/speech_service", "语音子服务在etcd的文件位置");
DEFINE_string(friend_service_name , "/service/friend_service", "好友子服务在etcd的文件位置");
//redis初始化数据
DEFINE_string(redis_host, "127.0.0.1", "redis主机地址");
DEFINE_int32(redis_port, 6379, "redis主机端口");
DEFINE_int32(redis_db, 0, "redis数据库编号");
DEFINE_int64(redis_timeout, 10000, "redis链接超时时间");
DEFINE_bool(redis_keepalive, true, "redis是否保活");
//端口
DEFINE_int32(http_port, 9000, "http端口");
DEFINE_int32(ws_port, 9001, "websocket端口");
int main(int argc, char* argv[])
{
gflags::ParseCommandLineFlags(&argc, &argv, true);
logging::SetMinLogLevel(4);
LogModule::Log::Init(FLAGS_log_mode
, FLAGS_log_file
, FLAGS_log_level
, FLAGS_log_output_mode);
bite_im::GateWayServerBuilder gwsb;
//服务管理
gwsb.make_channel_manager_object(FLAGS_speech_service_name,
FLAGS_user_service_name,
FLAGS_file_service_name,
FLAGS_message_service_name,
FLAGS_transmite_service_name,
FLAGS_friend_service_name);
//监视文件子服务上线
gwsb.make_discovery_object(FLAGS_registry_host, FLAGS_file_service_name);
gwsb.make_discovery_object(FLAGS_registry_host, FLAGS_user_service_name);
gwsb.make_discovery_object(FLAGS_registry_host, FLAGS_message_service_name);
gwsb.make_discovery_object(FLAGS_registry_host, FLAGS_transmite_service_name);
gwsb.make_discovery_object(FLAGS_registry_host, FLAGS_speech_service_name);
gwsb.make_discovery_object(FLAGS_registry_host, FLAGS_friend_service_name);
//绑定端口
gwsb.make_http_port(FLAGS_http_port);
gwsb.make_websocket_port(FLAGS_ws_port);
//redis服务,会话,状态,验证码
gwsb.make_redis_object(FLAGS_redis_host
, FLAGS_redis_port
, FLAGS_redis_db
, std::chrono::milliseconds(FLAGS_redis_timeout)
, FLAGS_redis_keepalive);
std::shared_ptr<bite_im::GateWayServer> server = gwsb.build();
server->start();
return 0;
}