微服务即时通讯系统(服务端)——网关服务设计与实现(7)

文章目录

微服务即时通讯系统(服务端)------网关服务设计与实现(7)

MicrCommSys: 微通信系统 - 消息存储微服务等项目

一、整体架构设计思路

1.1 为什么网关要这么设计?

想象一下咱们这个即时通讯系统,客户端要和服务端交互,有几十种不同的功能:登录注册、发消息、加好友、传文件等等。如果让客户端直接去连每个服务,会特别混乱,就像去一个办公楼找不同部门,每个部门都要单独敲门。

所以我们设计了网关服务,它就相当于大楼的前台接待处

  • HTTP部分:像接待窗口,处理客户的业务办理(填表、交材料)
  • WebSocket部分:像广播喇叭,有紧急通知时就广播给相关的人

1.2 核心设计决策

双协议设计的原因:

  • 普通业务请求(登录、发消息)用HTTP:简单、成熟,有现成的库支持
  • 实时通知(新消息、好友申请)用WebSocket:需要长连接,能主动推送给客户端

举个例子:你给朋友发消息

  1. 你先通过HTTP把消息内容发给网关
  2. 网关转发给消息服务处理
  3. 处理完后,网关通过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(), ...);
        }
    }
}

这里的逻辑

  1. 先处理完业务(数据库操作)
  2. 再检查对方是否在线
  3. 在线才发通知,不在线就等下次上线再提醒
  4. 业务处理和通知是分离的,互不影响

四、关键技术设计解析

4.1 为什么用Redis存会话?

考虑这样一个场景:用户用手机和电脑同时登录。

cpp 复制代码
// Redis里大概是这样存的
Key: session:abc123xyz
Value: user_1001
Expire: 7天

Key: session:def456uvw  
Value: user_1001  // 同一个用户,不同设备
Expire: 7天

用Redis的好处

  1. 共享:所有网关实例都能访问,支持水平扩展
  2. 快速:内存操作,比查数据库快得多
  3. 自动清理:设置过期时间,不用写代码清理
  4. 简单:就相当于一个大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();

构建器的优点

  1. 清晰:一眼就知道配了什么
  2. 灵活:想配什么就配什么,不强制顺序
  3. 安全:build()时会检查配置是否完整
  4. 可读:像读说明书一样

六、实际工作流程举例

场景:小明给小红发消息

  1. HTTP请求阶段

    text 复制代码
    小明手机 → 网关HTTP(8080):
    "我要发消息,session_id=xyz123,内容=hello"
    
    网关检查 → Redis:"xyz123对应user_1001"
    
    网关转发 → 消息服务:"user_1001要发消息,内容=hello"
    
    消息服务处理 → 存数据库,返回成功
    
    网关返回 → 小明手机:"发送成功"
  2. WebSocket通知阶段

    text 复制代码
    消息服务告诉网关:"消息存好了,要通知小红(user_1002)"
    
    网关检查 → connetion_manager_:"user_1002在线吗?"
    
    找到连接 → 小红手机的WebSocket连接
    
    网关推送 → 小红手机:"你有新消息,来自小明"
    
    小红手机 → 显示新消息提醒

场景:用户断线重连

  1. 网络断开

    text 复制代码
    网关发现 → ping不通了
    调用_onClose清理:
    - 从connetion_manager_删除映射
    - 从Redis删除在线状态
  2. 重新连接

    text 复制代码
    用户重连 → 建立新WebSocket
    发送认证 → 还是用原来的session_id
    网关验证 → Redis里session还在(没过期)
    重新绑定 → 建立新映射关系

七、设计总结

网关设计的几个核心思想:

  1. 单一入口:所有请求都走网关,好管理、好监控
  2. 职责分离:HTTP管请求,WebSocket管推送,各干各的
  3. 无状态:网关自己不存用户数据,全放Redis
  4. 只转发不处理:业务逻辑交给专门的服务
  5. 实时感知:谁在线、谁断线,心里有数

这样做的好处:

对客户端:只要连网关一个地方,简单

对后端服务:网关帮忙做了鉴权、限流、负载均衡

对运维:监控一个点就知道整体状态

对开发:加新功能只要加接口,不用改网关核心

这个设计让整个系统像邮局一样工作:

  • 你来寄信(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;
}
相关推荐
麦聪聊数据1 小时前
Web架构如何打通从SQL 脚本到API 服务的全链路追踪?
数据库·sql·架构
枫叶丹41 小时前
【Qt开发】Qt窗口(六) -> QMessageBox 消息对话框
c语言·开发语言·数据库·c++·qt·microsoft
ShiLiu_mtx1 小时前
Keepalived,Haproxy负载均衡集群
linux·运维·负载均衡
gugugu.1 小时前
从单机到微服务:分布式架构演进全景解析
分布式·微服务·架构
幸福右手牵1 小时前
服务器 IP 地址配置方案
linux·服务器·tcp/ip·智能路由器
七夜zippoe1 小时前
MateChat多模态交互实践:图文理解与语音对话系统集成
microsoft·架构·多模态·matechat
vortex51 小时前
Ubuntu 虚拟机配置静态 IP
linux·tcp/ip·ubuntu
橘颂TA1 小时前
【Linux】进程池
linux·运维·服务器·c++
草莓熊Lotso1 小时前
Git 多人协作全流程实战:分支协同 + 冲突解决 + 跨分支协助
linux·运维·服务器·人工智能·经验分享·git·python