即时通讯项目---网关服务

文章目录

  • 前言
    • [1. 前言](#1. 前言)
    • [2. 往期内容](#2. 往期内容)
    • [3. 本篇简介](#3. 本篇简介)
  • 功能设计
    • [1. 模块划分:](#1. 模块划分:)
    • [2. 模块功能示意图:](#2. 模块功能示意图:)
  • 接口实现流程:
    • [1. 用户名注册](#1. 用户名注册)
    • [2. 用户名登录](#2. 用户名登录)
    • [3. 短信验证码获取](#3. 短信验证码获取)
    • [4. 手机号码注册](#4. 手机号码注册)
    • [5. 手机号码登录](#5. 手机号码登录)
    • [6. 用户信息获取](#6. 用户信息获取)
    • [7. 修改用户头像](#7. 修改用户头像)
    • [8. 修改用户签名](#8. 修改用户签名)
    • [9. 修改用户昵称](#9. 修改用户昵称)
    • [10. 修改用户绑定手机号](#10. 修改用户绑定手机号)
    • [11. 获取好友列表](#11. 获取好友列表)
    • [12. 发送好友申请](#12. 发送好友申请)
    • [13. 获取待处理好友申请](#13. 获取待处理好友申请)
    • [14. 好友申请处理](#14. 好友申请处理)
    • [15. 删除好友](#15. 删除好友)
    • [16. 搜索用户](#16. 搜索用户)
    • [17. 获取用户聊天会话列表](#17. 获取用户聊天会话列表)
    • [18. 创建多人聊天会话](#18. 创建多人聊天会话)
    • [19. 获取消息会话成员列表](#19. 获取消息会话成员列表)
    • [20. 发送新消息](#20. 发送新消息)
    • [21. 获取指定时间段消息列表](#21. 获取指定时间段消息列表)
    • [22. 获取最近 N 条消息列表](#22. 获取最近 N 条消息列表)
    • [23. 搜索关键字历史消息](#23. 搜索关键字历史消息)
    • [24. 单个文件数据获取](#24. 单个文件数据获取)
    • [25. 多个文件数据获取](#25. 多个文件数据获取)
    • [26. 单个文件数据上传](#26. 单个文件数据上传)
    • [27. 多个文件数据上传](#27. 多个文件数据上传)
    • [28. 语音转文字](#28. 语音转文字)
  • 其余函数

前言

1. 前言

本篇是我介绍Chat-Im(仿微信的即时通讯项目)中的一篇,大家可以有兴趣的可以参照我的gitee代码和博客自己动手试一试。

gitee:https://gitee.com/qi-haozhe/chat-im
点我直接转到gitee

大家可以点击右上角的红框,这里会显示我提交的记录,在这里面你可以看到我做这个项目的顺序,以及每次完成了什么模块,每一次提交了什么文件,再结合我博客对于该项目的一些解析,相信有点基础的同学都可以独立完成这个项目的。

2. 往期内容

都已设置超链接,点击跳转,且按照学习顺序排序,不建议跳着来看。博客还未跟新完,持续更新中!

ChatIM项目语音识别安装与使用

brpc的安装与使用介绍以及channel的封装

即时通讯项目---etcd、spdlog、odb二次封装

ChatIm项目文件上传与获取

即时通讯项目---用户管理

即时通讯项目---消息转发子服务

即时通讯项目---消息持久化子服务

即时通讯项目---好友管理子服务


3. 本篇简介

本模块主要介绍即时通讯项目的网关部分,其它微服务完成以后,网关部分主要处理各种服务的请求,网关层根据请求服务的不同,去分别调用对应的微服务进行处理,然后给客户端返回结果就行。

功能设计

网关服务器在设计中,最重要的两个功能:

  • 作为入口服务器接收客户端的所有请求,进行请求的子服务分发,得到响应后进行响应
  • 对客户端进行事件通知(好友申请和处理及删除,单聊/群聊会话创建,新消息)
    基于以上的两个功能,因此网关服务器包含两项通信:
  • HTTP 通信:进行业务处理
  • WEBSOCKET 通信:进行事件通知

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 找到连接进行事件通知

2. 模块功能示意图:

接口实现流程:

1. 用户名注册

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 查找用户子服务
  3. 调用子服务对应接口进行业务处理
  4. 将处理结果响应给客户端。
cpp 复制代码
void UserRegister(const httplib::Request &request, httplib::Response &response) {
    //1. 取出http请求正文,将正文进行反序列化
    ymm_im::UserRegisterReq req;
    ymm_im::UserRegisterRsp rsp;
    auto err_response = [&req, &rsp, &response](const std::string &errmsg) -> void {
        rsp.set_success(false);
        rsp.set_errmsg(errmsg);
        response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
    };
    bool ret = req.ParseFromString(request.body);
    if (ret == false) {
        LOG_ERROR("用户名注册请求正文反序列化失败!");
        return err_response("用户名注册请求正文反序列化失败!");
    }
    //2. 将请求转发给用户子服务进行业务处理
    auto channel = _mm_channels->choose(_user_service_name);
    if (!channel) {
        LOG_ERROR("{} 未找到可提供业务处理的用户子服务节点!", req.request_id());
        return err_response("未找到可提供业务处理的用户子服务节点!");
    }
    ymm_im::UserService_Stub stub(channel.get());
    brpc::Controller cntl;
    stub.UserRegister(&cntl, &req, &rsp, nullptr);
    if (cntl.Failed()) {
        LOG_ERROR("{} 用户子服务调用失败!", req.request_id());
        return err_response("用户子服务调用失败!");
    }
    //3. 得到用户子服务的响应后,将响应内容进行序列化作为http响应正文
    response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
}

2. 用户名登录

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 查找用户子服务
  3. 调用子服务对应接口进行业务处理
  4. 将处理结果响应给客户端。
cpp 复制代码
void UserLogin(const httplib::Request &request, httplib::Response &response) {
    //1. 取出http请求正文,将正文进行反序列化
    ymm_im::UserLoginReq req;
    ymm_im::UserLoginRsp rsp;
    auto err_response = [&req, &rsp, &response](const std::string &errmsg) -> void {
        rsp.set_success(false);
        rsp.set_errmsg(errmsg);
        response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
    };
    bool ret = req.ParseFromString(request.body);
    if (ret == false) {
        LOG_ERROR("用户登录请求正文反序列化失败!");
        return err_response("用户登录请求正文反序列化失败!");
    }
    //2. 将请求转发给用户子服务进行业务处理
    auto channel = _mm_channels->choose(_user_service_name);
    if (!channel) {
        LOG_ERROR("{} 未找到可提供业务处理的用户子服务节点!", req.request_id());
        return err_response("未找到可提供业务处理的用户子服务节点!");
    }
    ymm_im::UserService_Stub stub(channel.get());
    brpc::Controller cntl;
    stub.UserLogin(&cntl, &req, &rsp, nullptr);
    if (cntl.Failed()) {
        LOG_ERROR("{} 用户子服务调用失败!", req.request_id());
        return err_response("用户子服务调用失败!");
    }
    //3. 得到用户子服务的响应后,将响应内容进行序列化作为http响应正文
    response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
}

3. 短信验证码获取

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 查找用户子服务
  3. 调用子服务对应接口进行业务处理
  4. 将处理结果响应给客户端。
cpp 复制代码
void GetPhoneVerifyCode(const httplib::Request &request, httplib::Response &response) {
    //1. 取出http请求正文,将正文进行反序列化
    ymm_im::PhoneVerifyCodeReq req;
    ymm_im::PhoneVerifyCodeRsp rsp;
    auto err_response = [&req, &rsp, &response](const std::string &errmsg) -> void {
        rsp.set_success(false);
        rsp.set_errmsg(errmsg);
        response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
    };
    bool ret = req.ParseFromString(request.body);
    if (ret == false) {
        LOG_ERROR("获取短信验证码请求正文反序列化失败!");
        return err_response("获取短信验证码请求正文反序列化失败!");
    }
    //2. 将请求转发给用户子服务进行业务处理
    auto channel = _mm_channels->choose(_user_service_name);
    if (!channel) {
        LOG_ERROR("{} 未找到可提供业务处理的用户子服务节点!", req.request_id());
        return err_response("未找到可提供业务处理的用户子服务节点!");
    }
    ymm_im::UserService_Stub stub(channel.get());
    brpc::Controller cntl;
    stub.GetPhoneVerifyCode(&cntl, &req, &rsp, nullptr);
    if (cntl.Failed()) {
        LOG_ERROR("{} 用户子服务调用失败!", req.request_id());
        return err_response("用户子服务调用失败!");
    }
    //3. 得到用户子服务的响应后,将响应内容进行序列化作为http响应正文
    response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
}

4. 手机号码注册

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 查找用户子服务
  3. 调用子服务对应接口进行业务处理
  4. 将处理结果响应给客户端。
cpp 复制代码
void PhoneRegister(const httplib::Request &request, httplib::Response &response) {
    //1. 取出http请求正文,将正文进行反序列化
    ymm_im::PhoneRegisterReq req;
    ymm_im::PhoneRegisterRsp rsp;
    auto err_response = [&req, &rsp, &response](const std::string &errmsg) -> void {
        rsp.set_success(false);
        rsp.set_errmsg(errmsg);
        response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
    };
    bool ret = req.ParseFromString(request.body);
    if (ret == false) {
        LOG_ERROR("手机号注册请求正文反序列化失败!");
        return err_response("手机号注册请求正文反序列化失败!");
    }
    //2. 将请求转发给用户子服务进行业务处理
    auto channel = _mm_channels->choose(_user_service_name);
    if (!channel) {
        LOG_ERROR("{} 未找到可提供业务处理的用户子服务节点!", req.request_id());
        return err_response("未找到可提供业务处理的用户子服务节点!");
    }
    ymm_im::UserService_Stub stub(channel.get());
    brpc::Controller cntl;
    stub.PhoneRegister(&cntl, &req, &rsp, nullptr);
    if (cntl.Failed()) {
        LOG_ERROR("{} 用户子服务调用失败!", req.request_id());
        return err_response("用户子服务调用失败!");
    }
    //3. 得到用户子服务的响应后,将响应内容进行序列化作为http响应正文
    response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
}

5. 手机号码登录

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 查找用户子服务
  3. 调用子服务对应接口进行业务处理
  4. 将处理结果响应给客户端。
cpp 复制代码
void PhoneLogin(const httplib::Request &request, httplib::Response &response) {
    //1. 取出http请求正文,将正文进行反序列化
    ymm_im::PhoneLoginReq req;
    ymm_im::PhoneLoginRsp rsp;
    auto err_response = [&req, &rsp, &response](const std::string &errmsg) -> void {
        rsp.set_success(false);
        rsp.set_errmsg(errmsg);
        response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
    };
    bool ret = req.ParseFromString(request.body);
    if (ret == false) {
        LOG_ERROR("手机号登录请求正文反序列化失败!");
        return err_response("手机号登录请求正文反序列化失败!");
    }
    //2. 将请求转发给用户子服务进行业务处理
    auto channel = _mm_channels->choose(_user_service_name);
    if (!channel) {
        LOG_ERROR("{} 未找到可提供业务处理的用户子服务节点!", req.request_id());
        return err_response("未找到可提供业务处理的用户子服务节点!");
    }
    ymm_im::UserService_Stub stub(channel.get());
    brpc::Controller cntl;
    stub.PhoneLogin(&cntl, &req, &rsp, nullptr);
    if (cntl.Failed()) {
        LOG_ERROR("{} 用户子服务调用失败!", req.request_id());
        return err_response("用户子服务调用失败!");
    }
    //3. 得到用户子服务的响应后,将响应内容进行序列化作为http响应正文
    response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
}

6. 用户信息获取

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
  3. 查找用户子服务
  4. 调用子服务对应接口进行业务处理
  5. 将处理结果响应给客户端。
cpp 复制代码
void GetUserInfo(const httplib::Request &request, httplib::Response &response) {
    //1. 取出http请求正文,将正文进行反序列化
    ymm_im::GetUserInfoReq req;
    ymm_im::GetUserInfoRsp rsp;
    auto err_response = [&req, &rsp, &response](const std::string &errmsg) -> void {
        rsp.set_success(false);
        rsp.set_errmsg(errmsg);
        response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
    };
    bool ret = req.ParseFromString(request.body);
    if (ret == false) {
        LOG_ERROR("获取用户信息请求正文反序列化失败!");
        return err_response("获取用户信息请求正文反序列化失败!");
    }
    //2. 客户端身份识别与鉴权
    std::string ssid = req.session_id();
    auto uid = _redis_session->uid(ssid);
    if (!uid) {
        LOG_ERROR("{} 获取登录会话关联用户信息失败!", ssid);
        return err_response("获取登录会话关联用户信息失败!");
    }
    req.set_user_id(*uid);
    //2. 将请求转发给用户子服务进行业务处理
    auto channel = _mm_channels->choose(_user_service_name);
    if (!channel) {
        LOG_ERROR("{} 未找到可提供业务处理的用户子服务节点!", req.request_id());
        return err_response("未找到可提供业务处理的用户子服务节点!");
    }
    ymm_im::UserService_Stub stub(channel.get());
    brpc::Controller cntl;
    stub.GetUserInfo(&cntl, &req, &rsp, nullptr);
    if (cntl.Failed()) {
        LOG_ERROR("{} 用户子服务调用失败!", req.request_id());
        return err_response("用户子服务调用失败!");
    }
    //3. 得到用户子服务的响应后,将响应内容进行序列化作为http响应正文
    response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
}

7. 修改用户头像

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
  3. 查找用户子服务
  4. 调用子服务对应接口进行业务处理
  5. 将处理结果响应给客户端。
cpp 复制代码
void SetUserAvatar(const httplib::Request &request, httplib::Response &response) {
    //1. 取出http请求正文,将正文进行反序列化
    ymm_im::SetUserAvatarReq req;
    ymm_im::SetUserAvatarRsp rsp;
    auto err_response = [&req, &rsp, &response](const std::string &errmsg) -> void {
        rsp.set_success(false);
        rsp.set_errmsg(errmsg);
        response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
    };
    bool ret = req.ParseFromString(request.body);
    if (ret == false) {
        LOG_ERROR("用户头像设置请求正文反序列化失败!");
        return err_response("用户头像设置请求正文反序列化失败!");
    }
    //2. 客户端身份识别与鉴权
    std::string ssid = req.session_id();
    auto uid = _redis_session->uid(ssid);
    if (!uid) {
        LOG_ERROR("{} 获取登录会话关联用户信息失败!", ssid);
        return err_response("获取登录会话关联用户信息失败!");
    }
    req.set_user_id(*uid);
    //2. 将请求转发给用户子服务进行业务处理
    auto channel = _mm_channels->choose(_user_service_name);
    if (!channel) {
        LOG_ERROR("{} 未找到可提供业务处理的用户子服务节点!", req.request_id());
        return err_response("未找到可提供业务处理的用户子服务节点!");
    }
    ymm_im::UserService_Stub stub(channel.get());
    brpc::Controller cntl;
    stub.SetUserAvatar(&cntl, &req, &rsp, nullptr);
    if (cntl.Failed()) {
        LOG_ERROR("{} 用户子服务调用失败!", req.request_id());
        return err_response("用户子服务调用失败!");
    }
    //3. 得到用户子服务的响应后,将响应内容进行序列化作为http响应正文
    response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
}

8. 修改用户签名

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
  3. 查找用户子服务
  4. 调用子服务对应接口进行业务处理
  5. 将处理结果响应给客户端。
cpp 复制代码
void SetUserDescription(const httplib::Request &request, httplib::Response &response) {
    //1. 取出http请求正文,将正文进行反序列化
    ymm_im::SetUserDescriptionReq req;
    ymm_im::SetUserDescriptionRsp rsp;
    auto err_response = [&req, &rsp, &response](const std::string &errmsg) -> void {
        rsp.set_success(false);
        rsp.set_errmsg(errmsg);
        response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
    };
    bool ret = req.ParseFromString(request.body);
    if (ret == false) {
        LOG_ERROR("用户签名设置请求正文反序列化失败!");
        return err_response("用户签名设置请求正文反序列化失败!");
    }
    //2. 客户端身份识别与鉴权
    std::string ssid = req.session_id();
    auto uid = _redis_session->uid(ssid);
    if (!uid) {
        LOG_ERROR("{} 获取登录会话关联用户信息失败!", ssid);
        return err_response("获取登录会话关联用户信息失败!");
    }
    req.set_user_id(*uid);
    //2. 将请求转发给用户子服务进行业务处理
    auto channel = _mm_channels->choose(_user_service_name);
    if (!channel) {
        LOG_ERROR("{} 未找到可提供业务处理的用户子服务节点!", req.request_id());
        return err_response("未找到可提供业务处理的用户子服务节点!");
    }
    ymm_im::UserService_Stub stub(channel.get());
    brpc::Controller cntl;
    stub.SetUserDescription(&cntl, &req, &rsp, nullptr);
    if (cntl.Failed()) {
        LOG_ERROR("{} 用户子服务调用失败!", req.request_id());
        return err_response("用户子服务调用失败!");
    }
    //3. 得到用户子服务的响应后,将响应内容进行序列化作为http响应正文
    response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
}

9. 修改用户昵称

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
  3. 查找用户子服务
  4. 调用子服务对应接口进行业务处理
  5. 将处理结果响应给客户端。
cpp 复制代码
void SetUserNickname(const httplib::Request &request, httplib::Response &response) {
    //1. 取出http请求正文,将正文进行反序列化
    ymm_im::SetUserNicknameReq req;
    ymm_im::SetUserNicknameRsp rsp;
    auto err_response = [&req, &rsp, &response](const std::string &errmsg) -> void {
        rsp.set_success(false);
        rsp.set_errmsg(errmsg);
        response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
    };
    bool ret = req.ParseFromString(request.body);
    if (ret == false) {
        LOG_ERROR("用户昵称设置请求正文反序列化失败!");
        return err_response("用户昵称设置请求正文反序列化失败!");
    }
    //2. 客户端身份识别与鉴权
    std::string ssid = req.session_id();
    auto uid = _redis_session->uid(ssid);
    if (!uid) {
        LOG_ERROR("{} 获取登录会话关联用户信息失败!", ssid);
        return err_response("获取登录会话关联用户信息失败!");
    }
    req.set_user_id(*uid);
    //2. 将请求转发给用户子服务进行业务处理
    auto channel = _mm_channels->choose(_user_service_name);
    if (!channel) {
        LOG_ERROR("{} 未找到可提供业务处理的用户子服务节点!", req.request_id());
        return err_response("未找到可提供业务处理的用户子服务节点!");
    }
    ymm_im::UserService_Stub stub(channel.get());
    brpc::Controller cntl;
    stub.SetUserNickname(&cntl, &req, &rsp, nullptr);
    if (cntl.Failed()) {
        LOG_ERROR("{} 用户子服务调用失败!", req.request_id());
        return err_response("用户子服务调用失败!");
    }
    //3. 得到用户子服务的响应后,将响应内容进行序列化作为http响应正文
    response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
}

10. 修改用户绑定手机号

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
  3. 查找用户子服务
  4. 调用子服务对应接口进行业务处理
  5. 将处理结果响应给客户端。
cpp 复制代码
void SetUserPhoneNumber(const httplib::Request &request, httplib::Response &response) {
    //1. 取出http请求正文,将正文进行反序列化
    ymm_im::SetUserPhoneNumberReq req;
    ymm_im::SetUserPhoneNumberRsp rsp;
    auto err_response = [&req, &rsp, &response](const std::string &errmsg) -> void {
        rsp.set_success(false);
        rsp.set_errmsg(errmsg);
        response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
    };
    bool ret = req.ParseFromString(request.body);
    if (ret == false) {
        LOG_ERROR("用户手机号设置请求正文反序列化失败!");
        return err_response("用户手机号设置请求正文反序列化失败!");
    }
    //2. 客户端身份识别与鉴权
    std::string ssid = req.session_id();
    auto uid = _redis_session->uid(ssid);
    if (!uid) {
        LOG_ERROR("{} 获取登录会话关联用户信息失败!", ssid);
        return err_response("获取登录会话关联用户信息失败!");
    }
    req.set_user_id(*uid);
    //2. 将请求转发给用户子服务进行业务处理
    auto channel = _mm_channels->choose(_user_service_name);
    if (!channel) {
        LOG_ERROR("{} 未找到可提供业务处理的用户子服务节点!", req.request_id());
        return err_response("未找到可提供业务处理的用户子服务节点!");
    }
    ymm_im::UserService_Stub stub(channel.get());
    brpc::Controller cntl;
    stub.SetUserPhoneNumber(&cntl, &req, &rsp, nullptr);
    if (cntl.Failed()) {
        LOG_ERROR("{} 用户子服务调用失败!", req.request_id());
        return err_response("用户子服务调用失败!");
    }
    //3. 得到用户子服务的响应后,将响应内容进行序列化作为http响应正文
    response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
}

11. 获取好友列表

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
  3. 查找好友子服务
  4. 调用子服务对应接口进行业务处理
  5. 将处理结果响应给客户端。
cpp 复制代码
void GetFriendList(const httplib::Request &request, httplib::Response &response) {
    //1. 取出http请求正文,将正文进行反序列化
    ymm_im::GetFriendListReq req;
    ymm_im::GetFriendListRsp rsp;
    auto err_response = [&req, &rsp, &response](const std::string &errmsg) -> void {
        rsp.set_success(false);
        rsp.set_errmsg(errmsg);
        response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
    };
    bool ret = req.ParseFromString(request.body);
    if (ret == false) {
        LOG_ERROR("获取好友列表请求正文反序列化失败!");
        return err_response("获取好友列表请求正文反序列化失败!");
    }
    //2. 客户端身份识别与鉴权
    std::string ssid = req.session_id();
    auto uid = _redis_session->uid(ssid);
    if (!uid) {
        LOG_ERROR("{} 获取登录会话关联用户信息失败!", ssid);
        return err_response("获取登录会话关联用户信息失败!");
    }
    req.set_user_id(*uid);
    //2. 将请求转发给好友子服务进行业务处理
    auto channel = _mm_channels->choose(_friend_service_name);
    if (!channel) {
        LOG_ERROR("{} 未找到可提供业务处理的用户子服务节点!", req.request_id());
        return err_response("未找到可提供业务处理的用户子服务节点!");
    }
    ymm_im::FriendService_Stub stub(channel.get());
    brpc::Controller cntl;
    stub.GetFriendList(&cntl, &req, &rsp, nullptr);
    if (cntl.Failed()) {
        LOG_ERROR("{} 好友子服务调用失败!", req.request_id());
        return err_response("好友子服务调用失败!");
    }
    //3. 得到用户子服务的响应后,将响应内容进行序列化作为http响应正文
    response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
}

12. 发送好友申请

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
  3. 查找用户子服务
  4. 根据请求中的用户 ID,调用用户子服务,获取用户的详细信息
  5. 查找好友子服务
  6. 调用子服务对应接口进行业务处理
  7. 若处理成功,则通过被申请人 ID,查找对方长连接
    a. 若长连接存在(对方在线),则组织好友申请通知进行事件通知
  8. 将处理结果响应给客户端。
cpp 复制代码
void FriendAdd(const httplib::Request &request, httplib::Response &response) {
    // 好友申请的业务处理中,好友子服务其实只是在数据库创建了申请事件
    // 网关需要做的事情:当好友子服务将业务处理完毕后,如果处理是成功的--需要通知被申请方
    // 1. 正文的反序列化,提取关键要素:登录会话ID
    ymm_im::FriendAddReq req;
    ymm_im::FriendAddRsp rsp;
    auto err_response = [&req, &rsp, &response](const std::string &errmsg) -> void {
        rsp.set_success(false);
        rsp.set_errmsg(errmsg);
        response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
    };
    bool ret = req.ParseFromString(request.body);
    if (ret == false) {
        LOG_ERROR("申请好友请求正文反序列化失败!");
        return err_response("申请好友请求正文反序列化失败!");
    }
    // 2. 客户端身份识别与鉴权
    std::string ssid = req.session_id();
    auto uid = _redis_session->uid(ssid);
    if (!uid) {
        LOG_ERROR("{} 获取登录会话关联用户信息失败!", ssid);
        return err_response("获取登录会话关联用户信息失败!");
    }
    req.set_user_id(*uid);
    // 3. 将请求转发给好友子服务进行业务处理
    auto channel = _mm_channels->choose(_friend_service_name);
    if (!channel) {
        LOG_ERROR("{} 未找到可提供业务处理的用户子服务节点!", req.request_id());
        return err_response("未找到可提供业务处理的用户子服务节点!");
    }
    ymm_im::FriendService_Stub stub(channel.get());
    brpc::Controller cntl;
    stub.FriendAdd(&cntl, &req, &rsp, nullptr);
    if (cntl.Failed()) {
        LOG_ERROR("{} 好友子服务调用失败!", req.request_id());
        return err_response("好友子服务调用失败!");
    }
    // 4. 若业务处理成功 --- 且获取被申请方长连接成功,则向被申请放进行好友申请事件通知
    auto conn = _connections->connection(req.respondent_id());
    if (rsp.success() && conn) {
        LOG_DEBUG("找到被申请人 {} 长连接,对其进行好友申请通知", req.respondent_id());
        auto user_rsp = _GetUserInfo(req.request_id(), *uid);
        if (!user_rsp) {
            LOG_ERROR("{} 获取当前客户端用户信息失败!", req.request_id());
            return err_response("获取当前客户端用户信息失败!");
        }
        ymm_im::NotifyMessage notify;
        notify.set_notify_type(ymm_im::NotifyType::FRIEND_ADD_APPLY_NOTIFY);
        notify.mutable_friend_add_apply()->mutable_user_info()->CopyFrom(user_rsp->user_info());
        conn->send(notify.SerializeAsString(), websocketpp::frame::opcode::value::binary);
    }
    // 5. 向客户端进行响应
    response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
}

13. 获取待处理好友申请

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
  3. 查找好友子服务
  4. 调用子服务对应接口进行业务处理
  5. 将处理结果响应给客户端。
cpp 复制代码
void GetPendingFriendEventList(const httplib::Request &request, httplib::Response &response) {
    ymm_im::GetPendingFriendEventListReq req;
    ymm_im::GetPendingFriendEventListRsp rsp;
    auto err_response = [&req, &rsp, &response](const std::string &errmsg) -> void {
        rsp.set_success(false);
        rsp.set_errmsg(errmsg);
        response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
    };
    bool ret = req.ParseFromString(request.body);
    if (ret == false) {
        LOG_ERROR("获取待处理好友申请请求正文反序列化失败!");
        return err_response("获取待处理好友申请请求正文反序列化失败!");
    }
    // 2. 客户端身份识别与鉴权
    std::string ssid = req.session_id();
    auto uid = _redis_session->uid(ssid);
    if (!uid) {
        LOG_ERROR("{} 获取登录会话关联用户信息失败!", ssid);
        return err_response("获取登录会话关联用户信息失败!");
    }
    req.set_user_id(*uid);
    // 3. 将请求转发给好友子服务进行业务处理
    auto channel = _mm_channels->choose(_friend_service_name);
    if (!channel) {
        LOG_ERROR("{} 未找到可提供业务处理的用户子服务节点!", req.request_id());
        return err_response("未找到可提供业务处理的用户子服务节点!");
    }
    ymm_im::FriendService_Stub stub(channel.get());
    brpc::Controller cntl;
    stub.GetPendingFriendEventList(&cntl, &req, &rsp, nullptr);
    if (cntl.Failed()) {
        LOG_ERROR("{} 好友子服务调用失败!", req.request_id());
        return err_response("好友子服务调用失败!");
    }
    // 5. 向客户端进行响应
    response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
}

14. 好友申请处理

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
  3. 查找用户子服务
  4. 根据请求中的用户 ID,调用用户子服务,获取申请人与被申请人的详细信息
  5. 查找好友子服务
  6. 调用子服务对应接口进行业务处理
  7. 若处理成功,则通过申请人 ID,查找申请人长连接,进行申请处理结果的通知
    • 若处理结果是同意,则意味着新聊天会话的创建,则对申请人继续进行聊天会话创建通知
      • 从处理结果中取出会话 ID,使用对方的昵称作为会话名称,对方的头像作为会话头像组织会话信息
    • 若处理结果是同意,则对当前处理者用户 ID 查找长连接,进行聊天会话创建的通知
      • 从处理结果中取出会话 ID,使用对方的昵称作为会话名称,对方的头像作为会话头像组织会话信息
    • 清理响应中的会话 ID 信息,
  8. 将处理结果响应给客户端
cpp 复制代码
void FriendAddProcess(const httplib::Request &request, httplib::Response &response) {
    //好友申请的处理-----
    ymm_im::FriendAddProcessReq req;
    ymm_im::FriendAddProcessRsp rsp;
    auto err_response = [&req, &rsp, &response](const std::string &errmsg) -> void {
        rsp.set_success(false);
        rsp.set_errmsg(errmsg);
        response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
    };
    bool ret = req.ParseFromString(request.body);
    if (ret == false) {
        LOG_ERROR("好友申请处理请求正文反序列化失败!");
        return err_response("好友申请处理请求正文反序列化失败!");
    }
    // 2. 客户端身份识别与鉴权
    std::string ssid = req.session_id();
    auto uid = _redis_session->uid(ssid);
    if (!uid) {
        LOG_ERROR("{} 获取登录会话关联用户信息失败!", ssid);
        return err_response("获取登录会话关联用户信息失败!");
    }
    req.set_user_id(*uid);
    // 3. 将请求转发给好友子服务进行业务处理
    auto channel = _mm_channels->choose(_friend_service_name);
    if (!channel) {
        LOG_ERROR("{} 未找到可提供业务处理的用户子服务节点!", req.request_id());
        return err_response("未找到可提供业务处理的用户子服务节点!");
    }
    ymm_im::FriendService_Stub stub(channel.get());
    brpc::Controller cntl;
    stub.FriendAddProcess(&cntl, &req, &rsp, nullptr);
    if (cntl.Failed()) {
        LOG_ERROR("{} 好友子服务调用失败!", req.request_id());
        return err_response("好友子服务调用失败!");
    }
    
    if (rsp.success()) {
        auto process_user_rsp = _GetUserInfo(req.request_id(), *uid);
        if (!process_user_rsp) {
            LOG_ERROR("{} 获取用户信息失败!", req.request_id());
            return err_response("获取用户信息失败!");
        }
        auto apply_user_rsp = _GetUserInfo(req.request_id(), req.apply_user_id());
        if (!process_user_rsp) {
            LOG_ERROR("{} 获取用户信息失败!", req.request_id());
            return err_response("获取用户信息失败!");
        }
        auto process_conn = _connections->connection(*uid);
        if (process_conn) LOG_DEBUG("找到处理人的长连接!");
        else LOG_DEBUG("未找到处理人的长连接!");
        auto apply_conn = _connections->connection(req.apply_user_id());
        if (apply_conn) LOG_DEBUG("找到申请人的长连接!");
        else LOG_DEBUG("未找到申请人的长连接!");
        //4. 将处理结果给申请人进行通知
        if (apply_conn) {
            ymm_im::NotifyMessage notify;
            notify.set_notify_type(ymm_im::NotifyType::FRIEND_ADD_PROCESS_NOTIFY);
            auto process_result = notify.mutable_friend_process_result();
            process_result->mutable_user_info()->CopyFrom(process_user_rsp->user_info());
            process_result->set_agree(req.agree());
            apply_conn->send(notify.SerializeAsString(), 
                websocketpp::frame::opcode::value::binary);
            LOG_DEBUG("对申请人进行申请处理结果通知!");
        }
        //5. 若处理结果是同意 --- 会伴随着单聊会话的创建 -- 因此需要对双方进行会话创建的通知
        if (req.agree() && apply_conn) { //对申请人的通知---会话信息就是处理人信息
            ymm_im::NotifyMessage notify;
            notify.set_notify_type(ymm_im::NotifyType::CHAT_SESSION_CREATE_NOTIFY);
            auto chat_session = notify.mutable_new_chat_session_info();
            chat_session->mutable_chat_session_info()->set_single_chat_friend_id(*uid);
            chat_session->mutable_chat_session_info()->set_chat_session_id(rsp.new_session_id());
            chat_session->mutable_chat_session_info()->set_chat_session_name(process_user_rsp->user_info().nickname());
            chat_session->mutable_chat_session_info()->set_avatar(process_user_rsp->user_info().avatar());
            apply_conn->send(notify.SerializeAsString(), websocketpp::frame::opcode::value::binary);
            LOG_DEBUG("对申请人进行会话创建通知!");
        }
        if (req.agree() && process_conn) { //对处理人的通知 --- 会话信息就是申请人信息
            ymm_im::NotifyMessage notify;
            notify.set_notify_type(ymm_im::NotifyType::CHAT_SESSION_CREATE_NOTIFY);
            auto chat_session = notify.mutable_new_chat_session_info();
            chat_session->mutable_chat_session_info()->set_single_chat_friend_id(req.apply_user_id());
            chat_session->mutable_chat_session_info()->set_chat_session_id(rsp.new_session_id());
            chat_session->mutable_chat_session_info()->set_chat_session_name(apply_user_rsp->user_info().nickname());
            chat_session->mutable_chat_session_info()->set_avatar(apply_user_rsp->user_info().avatar());
            process_conn->send(notify.SerializeAsString(), websocketpp::frame::opcode::value::binary);
            LOG_DEBUG("对处理人进行会话创建通知!");
        }
    }
    //6. 对客户端进行响应
    response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
}

15. 删除好友

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
  3. 查找好友子服务
  4. 调用子服务对应接口进行业务处理
  5. 若处理成功,则通过被删除者用户 ID,查找对方长连接
    a. 若长连接存在(对方在线),则组织好友删除通知进行事件通知
  6. 将处理结果响应给客户端。
cpp 复制代码
void FriendRemove(const httplib::Request &request, httplib::Response &response) {
    // 1. 正文的反序列化,提取关键要素:登录会话ID
    ymm_im::FriendRemoveReq req;
    ymm_im::FriendRemoveRsp rsp;
    auto err_response = [&req, &rsp, &response](const std::string &errmsg) -> void {
        rsp.set_success(false);
        rsp.set_errmsg(errmsg);
        response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
    };
    bool ret = req.ParseFromString(request.body);
    if (ret == false) {
        LOG_ERROR("删除好友请求正文反序列化失败!");
        return err_response("删除好友请求正文反序列化失败!");
    }
    // 2. 客户端身份识别与鉴权
    std::string ssid = req.session_id();
    auto uid = _redis_session->uid(ssid);
    if (!uid) {
        LOG_ERROR("{} 获取登录会话关联用户信息失败!", ssid);
        return err_response("获取登录会话关联用户信息失败!");
    }
    req.set_user_id(*uid);
    // 3. 将请求转发给好友子服务进行业务处理
    auto channel = _mm_channels->choose(_friend_service_name);
    if (!channel) {
        LOG_ERROR("{} 未找到可提供业务处理的用户子服务节点!", req.request_id());
        return err_response("未找到可提供业务处理的用户子服务节点!");
    }
    ymm_im::FriendService_Stub stub(channel.get());
    brpc::Controller cntl;
    stub.FriendRemove(&cntl, &req, &rsp, nullptr);
    if (cntl.Failed()) {
        LOG_ERROR("{} 好友子服务调用失败!", req.request_id());
        return err_response("好友子服务调用失败!");
    }
    // 4. 若业务处理成功 --- 且获取被申请方长连接成功,则向被申请放进行好友申请事件通知
    auto conn = _connections->connection(req.peer_id());
    if (rsp.success() && conn) {
        LOG_ERROR("对被删除人 {} 进行好友删除通知!", req.peer_id());
        ymm_im::NotifyMessage notify;
        notify.set_notify_type(ymm_im::NotifyType::FRIEND_REMOVE_NOTIFY);
        notify.mutable_friend_remove()->set_user_id(*uid);
        conn->send(notify.SerializeAsString(), websocketpp::frame::opcode::value::binary);
    }
    // 5. 向客户端进行响应
    response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
}

16. 搜索用户

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
  3. 查找好友子服务
  4. 调用子服务对应接口进行业务处理
  5. 将处理结果响应给客户端。
cpp 复制代码
void FriendSearch(const httplib::Request &request, httplib::Response &response) {
    ymm_im::FriendSearchReq req;
    ymm_im::FriendSearchRsp rsp;
    auto err_response = [&req, &rsp, &response](const std::string &errmsg) -> void {
        rsp.set_success(false);
        rsp.set_errmsg(errmsg);
        response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
    };
    bool ret = req.ParseFromString(request.body);
    if (ret == false) {
        LOG_ERROR("用户搜索请求正文反序列化失败!");
        return err_response("用户搜索请求正文反序列化失败!");
    }
    // 2. 客户端身份识别与鉴权
    std::string ssid = req.session_id();
    auto uid = _redis_session->uid(ssid);
    if (!uid) {
        LOG_ERROR("{} 获取登录会话关联用户信息失败!", ssid);
        return err_response("获取登录会话关联用户信息失败!");
    }
    req.set_user_id(*uid);
    // 3. 将请求转发给好友子服务进行业务处理
    auto channel = _mm_channels->choose(_friend_service_name);
    if (!channel) {
        LOG_ERROR("{} 未找到可提供业务处理的用户子服务节点!", req.request_id());
        return err_response("未找到可提供业务处理的用户子服务节点!");
    }
    ymm_im::FriendService_Stub stub(channel.get());
    brpc::Controller cntl;
    stub.FriendSearch(&cntl, &req, &rsp, nullptr);
    if (cntl.Failed()) {
        LOG_ERROR("{} 好友子服务调用失败!", req.request_id());
        return err_response("好友子服务调用失败!");
    }
    // 5. 向客户端进行响应
    response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
}

17. 获取用户聊天会话列表

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
  3. 查找好友子服务
  4. 调用子服务对应接口进行业务处理
  5. 将处理结果响应给客户端。
cpp 复制代码
void GetChatSessionList(const httplib::Request &request, httplib::Response &response) {
    ymm_im::GetChatSessionListReq req;
    ymm_im::GetChatSessionListRsp rsp;
    auto err_response = [&req, &rsp, &response](const std::string &errmsg) -> void {
        rsp.set_success(false);
        rsp.set_errmsg(errmsg);
        response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
    };
    bool ret = req.ParseFromString(request.body);
    if (ret == false) {
        LOG_ERROR("获取聊天会话列表请求正文反序列化失败!");
        return err_response("获取聊天会话列表请求正文反序列化失败!");
    }
    // 2. 客户端身份识别与鉴权
    std::string ssid = req.session_id();
    auto uid = _redis_session->uid(ssid);
    if (!uid) {
        LOG_ERROR("{} 获取登录会话关联用户信息失败!", ssid);
        return err_response("获取登录会话关联用户信息失败!");
    }
    req.set_user_id(*uid);
    // 3. 将请求转发给好友子服务进行业务处理
    auto channel = _mm_channels->choose(_friend_service_name);
    if (!channel) {
        LOG_ERROR("{} 未找到可提供业务处理的用户子服务节点!", req.request_id());
        return err_response("未找到可提供业务处理的用户子服务节点!");
    }
    ymm_im::FriendService_Stub stub(channel.get());
    brpc::Controller cntl;
    stub.GetChatSessionList(&cntl, &req, &rsp, nullptr);
    if (cntl.Failed()) {
        LOG_ERROR("{} 好友子服务调用失败!", req.request_id());
        return err_response("好友子服务调用失败!");
    }
    // 5. 向客户端进行响应
    response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
}

18. 创建多人聊天会话

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
  3. 查找好友子服务
  4. 调用子服务对应接口进行业务处理
  5. 若处理成功,循环根据会话成员的 ID 找到他们的长连接,
    a. 根据响应中的会话信息,逐个进行会话创建的通知
    b. 清理响应中的会话信息
  6. 将处理结果响应给客户端。
cpp 复制代码
void ChatSessionCreate(const httplib::Request &request, httplib::Response &response) {
    ymm_im::ChatSessionCreateReq req;
    ymm_im::ChatSessionCreateRsp rsp;
    auto err_response = [&req, &rsp, &response](const std::string &errmsg) -> void {
        rsp.set_success(false);
        rsp.set_errmsg(errmsg);
        response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
    };
    bool ret = req.ParseFromString(request.body);
    if (ret == false) {
        LOG_ERROR("创建聊天会话请求正文反序列化失败!");
        return err_response("创建聊天会话请求正文反序列化失败!");
    }
    // 2. 客户端身份识别与鉴权
    std::string ssid = req.session_id();
    auto uid = _redis_session->uid(ssid);
    if (!uid) {
        LOG_ERROR("{} 获取登录会话关联用户信息失败!", ssid);
        return err_response("获取登录会话关联用户信息失败!");
    }
    req.set_user_id(*uid);
    // 3. 将请求转发给好友子服务进行业务处理
    auto channel = _mm_channels->choose(_friend_service_name);
    if (!channel) {
        LOG_ERROR("{} 未找到可提供业务处理的用户子服务节点!", req.request_id());
        return err_response("未找到可提供业务处理的用户子服务节点!");
    }
    ymm_im::FriendService_Stub stub(channel.get());
    brpc::Controller cntl;
    stub.ChatSessionCreate(&cntl, &req, &rsp, nullptr);
    if (cntl.Failed()) {
        LOG_ERROR("{} 好友子服务调用失败!", req.request_id());
        return err_response("好友子服务调用失败!");
    }
    // 4. 若业务处理成功 --- 且获取被申请方长连接成功,则向被申请放进行好友申请事件通知
    if (rsp.success()){
        for (int i = 0; i < req.member_id_list_size(); i++) {
            auto conn = _connections->connection(req.member_id_list(i));
            if (!conn) { 
                LOG_DEBUG("未找到群聊成员 {} 长连接", req.member_id_list(i));
                continue;
            }
            ymm_im::NotifyMessage notify;
            notify.set_notify_type(ymm_im::NotifyType::CHAT_SESSION_CREATE_NOTIFY);
            auto chat_session = notify.mutable_new_chat_session_info();
            chat_session->mutable_chat_session_info()->CopyFrom(rsp.chat_session_info());
            conn->send(notify.SerializeAsString(), websocketpp::frame::opcode::value::binary);
            LOG_DEBUG("对群聊成员 {} 进行会话创建通知", req.member_id_list(i));
        }
    }
    // 5. 向客户端进行响应
    rsp.clear_chat_session_info();
    response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
}

19. 获取消息会话成员列表

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
  3. 查找好友子服务
  4. 调用子服务对应接口进行业务处理
  5. 将处理结果响应给客户端。
cpp 复制代码
void GetChatSessionMember(const httplib::Request &request, httplib::Response &response) {
    ymm_im::GetChatSessionMemberReq req;
    ymm_im::GetChatSessionMemberRsp rsp;
    auto err_response = [&req, &rsp, &response](const std::string &errmsg) -> void {
        rsp.set_success(false);
        rsp.set_errmsg(errmsg);
        response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
    };
    bool ret = req.ParseFromString(request.body);
    if (ret == false) {
        LOG_ERROR("获取聊天会话成员请求正文反序列化失败!");
        return err_response("获取聊天会话成员请求正文反序列化失败!");
    }
    // 2. 客户端身份识别与鉴权
    std::string ssid = req.session_id();
    auto uid = _redis_session->uid(ssid);
    if (!uid) {
        LOG_ERROR("{} 获取登录会话关联用户信息失败!", ssid);
        return err_response("获取登录会话关联用户信息失败!");
    }
    req.set_user_id(*uid);
    // 3. 将请求转发给好友子服务进行业务处理
    auto channel = _mm_channels->choose(_friend_service_name);
    if (!channel) {
        LOG_ERROR("{} 未找到可提供业务处理的用户子服务节点!", req.request_id());
        return err_response("未找到可提供业务处理的用户子服务节点!");
    }
    ymm_im::FriendService_Stub stub(channel.get());
    brpc::Controller cntl;
    stub.GetChatSessionMember(&cntl, &req, &rsp, nullptr);
    if (cntl.Failed()) {
        LOG_ERROR("{} 好友子服务调用失败!", req.request_id());
        return err_response("好友子服务调用失败!");
    }
    // 5. 向客户端进行响应
    response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
}

20. 发送新消息

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
  3. 查找消息转发子服务
  4. 调用子服务对应接口进行业务处理
  5. 若处理成功,则根据处理结果中的用户 ID 列表,循环找到目标长连接,根据处理结果中的消息字段组织新消息通知,逐个对目标进行新消息通知。
  6. 若处理失败,则根据处理结果中的错误提示信息,设置响应内容
  7. 将处理结果响应给客户端。
cpp 复制代码
void NewMessage(const httplib::Request &request, httplib::Response &response) {
    ymm_im::NewMessageReq req;
    ymm_im::NewMessageRsp rsp;//这是给客户端的响应
    ymm_im::GetTransmitTargetRsp target_rsp;//这是请求子服务的响应
    auto err_response = [&req, &rsp, &response](const std::string &errmsg) -> void {
        rsp.set_success(false);
        rsp.set_errmsg(errmsg);
        response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
    };
    bool ret = req.ParseFromString(request.body);
    if (ret == false) {
        LOG_ERROR("新消息请求正文反序列化失败!");
        return err_response("新消息请求正文反序列化失败!");
    }
    // 2. 客户端身份识别与鉴权
    std::string ssid = req.session_id();
    auto uid = _redis_session->uid(ssid);
    if (!uid) {
        LOG_ERROR("{} 获取登录会话关联用户信息失败!", ssid);
        return err_response("获取登录会话关联用户信息失败!");
    }
    req.set_user_id(*uid);
    // 3. 将请求转发给好友子服务进行业务处理
    auto channel = _mm_channels->choose(_transmite_service_name);
    if (!channel) {
        LOG_ERROR("{} 未找到可提供业务处理的用户子服务节点!", req.request_id());
        return err_response("未找到可提供业务处理的用户子服务节点!");
    }
    ymm_im::MsgTransmitService_Stub stub(channel.get());
    brpc::Controller cntl;
    stub.GetTransmitTarget(&cntl, &req, &target_rsp, nullptr);
    if (cntl.Failed()) {
        LOG_ERROR("{} 消息转发子服务调用失败!", req.request_id());
        return err_response("消息转发子服务调用失败!");
    }
    // 4. 若业务处理成功 --- 且获取被申请方长连接成功,则向被申请放进行好友申请事件通知
    if (target_rsp.success()){
        for (int i = 0; i < target_rsp.target_id_list_size(); i++) {
            std::string notify_uid = target_rsp.target_id_list(i);
            if (notify_uid == *uid) continue; //不通知自己
            auto conn = _connections->connection(notify_uid);
            if (!conn) { continue;}
            ymm_im::NotifyMessage notify;
            notify.set_notify_type(ymm_im::NotifyType::CHAT_MESSAGE_NOTIFY);
            auto msg_info = notify.mutable_new_message_info();
            msg_info->mutable_message_info()->CopyFrom(target_rsp.message());
            conn->send(notify.SerializeAsString(), websocketpp::frame::opcode::value::binary);
        }
    }
    // 5. 向客户端进行响应
    rsp.set_request_id(req.request_id());
    rsp.set_success(target_rsp.success());
    rsp.set_errmsg(target_rsp.errmsg());
    response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
}

21. 获取指定时间段消息列表

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
  3. 查找消息存储子服务
  4. 调用子服务对应接口进行业务处理
  5. 将处理结果响应给客户端。
cpp 复制代码
void GetHistoryMsg(const httplib::Request &request, httplib::Response &response) {
    ymm_im::GetHistoryMsgReq req;
    ymm_im::GetHistoryMsgRsp rsp;
    auto err_response = [&req, &rsp, &response](const std::string &errmsg) -> void {
        rsp.set_success(false);
        rsp.set_errmsg(errmsg);
        response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
    };
    bool ret = req.ParseFromString(request.body);
    if (ret == false) {
        LOG_ERROR("获取区间消息请求正文反序列化失败!");
        return err_response("获取区间消息请求正文反序列化失败!");
    }
    // 2. 客户端身份识别与鉴权
    std::string ssid = req.session_id();
    auto uid = _redis_session->uid(ssid);
    if (!uid) {
        LOG_ERROR("{} 获取登录会话关联用户信息失败!", ssid);
        return err_response("获取登录会话关联用户信息失败!");
    }
    req.set_user_id(*uid);
    // 3. 将请求转发给好友子服务进行业务处理
    auto channel = _mm_channels->choose(_message_service_name);
    if (!channel) {
        LOG_ERROR("{} 未找到可提供业务处理的用户子服务节点!", req.request_id());
        return err_response("未找到可提供业务处理的用户子服务节点!");
    }
    ymm_im::MsgStorageService_Stub stub(channel.get());
    brpc::Controller cntl;
    stub.GetHistoryMsg(&cntl, &req, &rsp, nullptr);
    if (cntl.Failed()) {
        LOG_ERROR("{} 消息存储子服务调用失败!", req.request_id());
        return err_response("消息存储子服务调用失败!");
    }
    // 5. 向客户端进行响应
    response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
}

22. 获取最近 N 条消息列表

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
  3. 查找消息存储子服务
  4. 调用子服务对应接口进行业务处理
  5. 将处理结果响应给客户端。
cpp 复制代码
void GetRecentMsg(const httplib::Request &request, httplib::Response &response) {
    ymm_im::GetRecentMsgReq req;
    ymm_im::GetRecentMsgRsp rsp;
    auto err_response = [&req, &rsp, &response](const std::string &errmsg) -> void {
        rsp.set_success(false);
        rsp.set_errmsg(errmsg);
        response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
    };
    bool ret = req.ParseFromString(request.body);
    if (ret == false) {
        LOG_ERROR("获取最近消息请求正文反序列化失败!");
        return err_response("获取最近消息请求正文反序列化失败!");
    }
    // 2. 客户端身份识别与鉴权
    std::string ssid = req.session_id();
    auto uid = _redis_session->uid(ssid);
    if (!uid) {
        LOG_ERROR("{} 获取登录会话关联用户信息失败!", ssid);
        return err_response("获取登录会话关联用户信息失败!");
    }
    req.set_user_id(*uid);
    // 3. 将请求转发给好友子服务进行业务处理
    auto channel = _mm_channels->choose(_message_service_name);
    if (!channel) {
        LOG_ERROR("{} 未找到可提供业务处理的用户子服务节点!", req.request_id());
        return err_response("未找到可提供业务处理的用户子服务节点!");
    }
    ymm_im::MsgStorageService_Stub stub(channel.get());
    brpc::Controller cntl;
    stub.GetRecentMsg(&cntl, &req, &rsp, nullptr);
    if (cntl.Failed()) {
        LOG_ERROR("{} 消息存储子服务调用失败!", req.request_id());
        return err_response("消息存储子服务调用失败!");
    }
    // 5. 向客户端进行响应
    response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
}

23. 搜索关键字历史消息

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
  3. 查找消息存储子服务
  4. 调用子服务对应接口进行业务处理
  5. 将处理结果响应给客户端。
cpp 复制代码
void MsgSearch(const httplib::Request &request, httplib::Response &response) {
    ymm_im::MsgSearchReq req;
    ymm_im::MsgSearchRsp rsp;
    auto err_response = [&req, &rsp, &response](const std::string &errmsg) -> void {
        rsp.set_success(false);
        rsp.set_errmsg(errmsg);
        response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
    };
    bool ret = req.ParseFromString(request.body);
    if (ret == false) {
        LOG_ERROR("消息搜索请求正文反序列化失败!");
        return err_response("消息搜索请求正文反序列化失败!");
    }
    // 2. 客户端身份识别与鉴权
    std::string ssid = req.session_id();
    auto uid = _redis_session->uid(ssid);
    if (!uid) {
        LOG_ERROR("{} 获取登录会话关联用户信息失败!", ssid);
        return err_response("获取登录会话关联用户信息失败!");
    }
    req.set_user_id(*uid);
    // 3. 将请求转发给好友子服务进行业务处理
    auto channel = _mm_channels->choose(_message_service_name);
    if (!channel) {
        LOG_ERROR("{} 未找到可提供业务处理的用户子服务节点!", req.request_id());
        return err_response("未找到可提供业务处理的用户子服务节点!");
    }
    ymm_im::MsgStorageService_Stub stub(channel.get());
    brpc::Controller cntl;
    stub.MsgSearch(&cntl, &req, &rsp, nullptr);
    if (cntl.Failed()) {
        LOG_ERROR("{} 消息存储子服务调用失败!", req.request_id());
        return err_response("消息存储子服务调用失败!");
    }
    // 5. 向客户端进行响应
    response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
    }
            

24. 单个文件数据获取

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
  3. 查找文件子服务
  4. 调用子服务对应接口进行业务处理
  5. 将处理结果响应给客户端。
cpp 复制代码
void GetSingleFile(const httplib::Request &request, httplib::Response &response) {
    ymm_im::GetSingleFileReq req;
    ymm_im::GetSingleFileRsp rsp;
    auto err_response = [&req, &rsp, &response](const std::string &errmsg) -> void {
        rsp.set_success(false);
        rsp.set_errmsg(errmsg);
        response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
    };
    bool ret = req.ParseFromString(request.body);
    if (ret == false) {
        LOG_ERROR("单文件下载请求正文反序列化失败!");
        return err_response("单文件下载请求正文反序列化失败!");
    }
    // 2. 客户端身份识别与鉴权
    std::string ssid = req.session_id();
    auto uid = _redis_session->uid(ssid);
    if (!uid) {
        LOG_ERROR("{} 获取登录会话关联用户信息失败!", ssid);
        return err_response("获取登录会话关联用户信息失败!");
    }
    req.set_user_id(*uid);
    // 3. 将请求转发给好友子服务进行业务处理
    auto channel = _mm_channels->choose(_file_service_name);
    if (!channel) {
        LOG_ERROR("{} 未找到可提供业务处理的用户子服务节点!", req.request_id());
        return err_response("未找到可提供业务处理的用户子服务节点!");
    }
    ymm_im::FileService_Stub stub(channel.get());
    brpc::Controller cntl;
    stub.GetSingleFile(&cntl, &req, &rsp, nullptr);
    if (cntl.Failed()) {
        LOG_ERROR("{} 文件存储子服务调用失败!", req.request_id());
        return err_response("文件存储子服务调用失败!");
    }
    // 5. 向客户端进行响应
    response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
}

25. 多个文件数据获取

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
  3. 查找文件子服务
  4. 调用子服务对应接口进行业务处理
  5. 将处理结果响应给客户端。
cpp 复制代码
void GetMultiFile(const httplib::Request &request, httplib::Response &response) {
    ymm_im::GetMultiFileReq req;
    ymm_im::GetMultiFileRsp rsp;
    auto err_response = [&req, &rsp, &response](const std::string &errmsg) -> void {
        rsp.set_success(false);
        rsp.set_errmsg(errmsg);
        response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
    };
    bool ret = req.ParseFromString(request.body);
    if (ret == false) {
        LOG_ERROR("单文件下载请求正文反序列化失败!");
        return err_response("单文件下载请求正文反序列化失败!");
    }
    // 2. 客户端身份识别与鉴权
    std::string ssid = req.session_id();
    auto uid = _redis_session->uid(ssid);
    if (!uid) {
        LOG_ERROR("{} 获取登录会话关联用户信息失败!", ssid);
        return err_response("获取登录会话关联用户信息失败!");
    }
    req.set_user_id(*uid);
    // 3. 将请求转发给好友子服务进行业务处理
    auto channel = _mm_channels->choose(_file_service_name);
    if (!channel) {
        LOG_ERROR("{} 未找到可提供业务处理的用户子服务节点!", req.request_id());
        return err_response("未找到可提供业务处理的用户子服务节点!");
    }
    ymm_im::FileService_Stub stub(channel.get());
    brpc::Controller cntl;
    stub.GetMultiFile(&cntl, &req, &rsp, nullptr);
    if (cntl.Failed()) {
        LOG_ERROR("{} 文件存储子服务调用失败!", req.request_id());
        return err_response("文件存储子服务调用失败!");
    }
    // 5. 向客户端进行响应
    response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
}

26. 单个文件数据上传

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
  3. 查找文件子服务
  4. 调用子服务对应接口进行业务处理
  5. 将处理结果响应给客户端。
cpp 复制代码
void PutSingleFile(const httplib::Request &request, httplib::Response &response) {
    ymm_im::PutSingleFileReq req;
    ymm_im::PutSingleFileRsp rsp;
    auto err_response = [&req, &rsp, &response](const std::string &errmsg) -> void {
        rsp.set_success(false);
        rsp.set_errmsg(errmsg);
        response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
    };
    bool ret = req.ParseFromString(request.body);
    if (ret == false) {
        LOG_ERROR("单文件上传请求正文反序列化失败!");
        return err_response("单文件上传请求正文反序列化失败!");
    }
    // 2. 客户端身份识别与鉴权
    std::string ssid = req.session_id();
    auto uid = _redis_session->uid(ssid);
    if (!uid) {
        LOG_ERROR("{} 获取登录会话关联用户信息失败!", ssid);
        return err_response("获取登录会话关联用户信息失败!");
    }
    req.set_user_id(*uid);
    // 3. 将请求转发给好友子服务进行业务处理
    auto channel = _mm_channels->choose(_file_service_name);
    if (!channel) {
        LOG_ERROR("{} 未找到可提供业务处理的用户子服务节点!", req.request_id());
        return err_response("未找到可提供业务处理的用户子服务节点!");
    }
    ymm_im::FileService_Stub stub(channel.get());
    brpc::Controller cntl;
    stub.PutSingleFile(&cntl, &req, &rsp, nullptr);
    if (cntl.Failed()) {
        LOG_ERROR("{} 文件存储子服务调用失败!", req.request_id());
        return err_response("文件存储子服务调用失败!");
    }
    // 5. 向客户端进行响应
    response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
}

27. 多个文件数据上传

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
  3. 查找文件子服务
  4. 调用子服务对应接口进行业务处理
  5. 将处理结果响应给客户端。
cpp 复制代码
void PutMultiFile(const httplib::Request &request, httplib::Response &response) {
    ymm_im::PutMultiFileReq req;
    ymm_im::PutMultiFileRsp rsp;
    auto err_response = [&req, &rsp, &response](const std::string &errmsg) -> void {
        rsp.set_success(false);
        rsp.set_errmsg(errmsg);
        response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
    };
    bool ret = req.ParseFromString(request.body);
    if (ret == false) {
        LOG_ERROR("批量文件上传请求正文反序列化失败!");
        return err_response("批量文件上传请求正文反序列化失败!");
    }
    // 2. 客户端身份识别与鉴权
    std::string ssid = req.session_id();
    auto uid = _redis_session->uid(ssid);
    if (!uid) {
        LOG_ERROR("{} 获取登录会话关联用户信息失败!", ssid);
        return err_response("获取登录会话关联用户信息失败!");
    }
    req.set_user_id(*uid);
    // 3. 将请求转发给好友子服务进行业务处理
    auto channel = _mm_channels->choose(_file_service_name);
    if (!channel) {
        LOG_ERROR("{} 未找到可提供业务处理的用户子服务节点!", req.request_id());
        return err_response("未找到可提供业务处理的用户子服务节点!");
    }
    ymm_im::FileService_Stub stub(channel.get());
    brpc::Controller cntl;
    stub.PutMultiFile(&cntl, &req, &rsp, nullptr);
    if (cntl.Failed()) {
        LOG_ERROR("{} 文件存储子服务调用失败!", req.request_id());
        return err_response("文件存储子服务调用失败!");
    }
    // 5. 向客户端进行响应
    response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
}

28. 语音转文字

  1. 取出 HTTP 请求正文,进行 ProtoBuf 反序列化
  2. 根据请求中的会话 ID 进行鉴权,并获取用户 ID,向请求中设置用户 ID
  3. 查找语音子服务
  4. 调用子服务对应接口进行业务处理
  5. 将处理结果响应给客户端。
cpp 复制代码
void SpeechRecognition(const httplib::Request &request, httplib::Response &response) {
    LOG_DEBUG("收到语音转文字请求!");
    ymm_im::SpeechRecognitionReq req;
    ymm_im::SpeechRecognitionRsp rsp;
    auto err_response = [&req, &rsp, &response](const std::string &errmsg) -> void {
        rsp.set_success(false);
        rsp.set_errmsg(errmsg);
        response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
    };
    bool ret = req.ParseFromString(request.body);
    if (ret == false) {
        LOG_ERROR("语音识别请求正文反序列化失败!");
        return err_response("语音识别请求正文反序列化失败!");
    }
    // 2. 客户端身份识别与鉴权
    std::string ssid = req.session_id();
    auto uid = _redis_session->uid(ssid);
    if (!uid) {
        LOG_ERROR("{} 获取登录会话关联用户信息失败!", ssid);
        return err_response("获取登录会话关联用户信息失败!");
    }
    req.set_user_id(*uid);
    // 3. 将请求转发给好友子服务进行业务处理
    auto channel = _mm_channels->choose(_speech_service_name);
    if (!channel) {
        LOG_ERROR("{} 未找到可提供业务处理的用户子服务节点!", req.request_id());
        return err_response("未找到可提供业务处理的用户子服务节点!");
    }
    ymm_im::SpeechService_Stub stub(channel.get());
    brpc::Controller cntl;
    stub.SpeechRecognition(&cntl, &req, &rsp, nullptr);
    if (cntl.Failed()) {
        LOG_ERROR("{} 语音识别子服务调用失败!", req.request_id());
        return err_response("语音识别子服务调用失败!");
    }
    // 5. 向客户端进行响应
    response.set_content(rsp.SerializeAsString(), "application/x-protbuf");
}

其余函数

onopen、onclose、onmessage都是要设置到websocket里的回调函数

  • onopen表示连接建立成功时的回调
  • onclose表示连接断开之后要执行的回调函数,执行一些清理工作
  • 收到第一条消息后,根据消息中的会话ID进行身份识别,将客户端长连接添加管理
cpp 复制代码
void onOpen(websocketpp::connection_hdl hdl) {
    LOG_DEBUG("websocket长连接建立成功 {}", (size_t)_ws_server.get_con_from_hdl(hdl).get());
}
void onClose(websocketpp::connection_hdl hdl) {
    //长连接断开时做的清理工作
    //0. 通过连接对象,获取对应的用户ID与登录会话ID
    auto conn = _ws_server.get_con_from_hdl(hdl);
    std::string uid, ssid;
    bool ret = _connections->client(conn, uid, ssid);
    if (ret == false) {
        LOG_WARN("长连接断开,未找到长连接对应的客户端信息!");
        return ;
    }
    //1. 移除登录会话信息
    _redis_session->remove(ssid);
    //2. 移除登录状态信息
    _redis_status->remove(uid);
    //3. 移除长连接管理数据
    _connections->remove(conn);
    LOG_DEBUG("{} {} {} 长连接断开,清理缓存数据!", ssid, uid, (size_t)conn.get());
}

void onMessage(websocketpp::connection_hdl hdl, server_t::message_ptr msg) {
    //收到第一条消息后,根据消息中的会话ID进行身份识别,将客户端长连接添加管理
    //1. 取出长连接对应的连接对象
    auto conn = _ws_server.get_con_from_hdl(hdl);
    //2. 针对消息内容进行反序列化 -- ClientAuthenticationReq -- 提取登录会话ID
    ymm_im::ClientAuthenticationReq request;
    bool ret = request.ParseFromString(msg->get_payload());
    if (ret == false) {
        LOG_ERROR("长连接身份识别失败:正文反序列化失败!");
        _ws_server.close(hdl, websocketpp::close::status::unsupported_data, "正文反序列化失败!");
        return;
    }
    //3. 在会话信息缓存中,查找会话信息 
    std::string ssid = request.session_id();
    auto uid = _redis_session->uid(ssid);
    //4. 会话信息不存在则关闭连接
    if (!uid) {
        LOG_ERROR("长连接身份识别失败:未找到会话信息 {}!", ssid);
        _ws_server.close(hdl, websocketpp::close::status::unsupported_data, "未找到会话信息!");
        return;
    }
    //5. 会话信息存在,则添加长连接管理
    _connections->insert(conn, *uid, ssid);
    LOG_DEBUG("新增长连接管理:{}-{}-{}", ssid, *uid, (size_t)conn.get());
    keepAlive(conn);
}
cpp 复制代码
void keepAlive(server_t::connection_ptr conn) {
    if (!conn || conn->get_state() != websocketpp::session::state::value::open) {
        LOG_DEBUG("非正常连接状态,结束连接保活");
        return;
    }
    conn->ping("");
    _ws_server.set_timer(60000, std::bind(&GatewayServer::keepAlive, this, conn));
}
相关推荐
感哥5 小时前
C++ 多态
c++
沐怡旸12 小时前
【底层机制】std::string 解决的痛点?是什么?怎么实现的?怎么正确用?
c++·面试
River41615 小时前
Javer 学 c++(十三):引用篇
c++·后端
轻松Ai享生活18 小时前
5 节课深入学习Linux Cgroups
linux
感哥18 小时前
C++ std::set
c++
侃侃_天下18 小时前
最终的信号类
开发语言·c++·算法
christine-rr18 小时前
linux常用命令(4)——压缩命令
linux·服务器·redis
三坛海会大神55519 小时前
LVS与Keepalived详解(二)LVS负载均衡实现实操
linux·负载均衡·lvs
東雪蓮☆19 小时前
深入理解 LVS-DR 模式与 Keepalived 高可用集群
linux·运维·服务器·lvs
博笙困了19 小时前
AcWing学习——差分
c++·算法