当前位置: 代码网 > it编程>编程语言>C/C++ > C++微服务UserServer设计与实现方法详解

C++微服务UserServer设计与实现方法详解

2025年11月21日 C/C++ 我要评论
前言做 im 项目时,用户服务(userserver)是整个系统的基石 —— 所有业务(好友、消息、朋友圈)都依赖用户认证和基础信息。这篇文章就从实战角度,聊聊我是怎么设计、实

前言

做 im 项目时,用户服务(userserver)是整个系统的基石 —— 所有业务(好友、消息、朋友圈)都依赖用户认证和基础信息。这篇文章就从实战角度,聊聊我是怎么设计、实现 userserver 的,包括核心功能落地、依赖替换(比如用模拟短信服务替代真实平台)、以及那些踩过的坑,希望能给做 c++ 后端的朋友一些参考。

一、先搞懂:userserver 在 im 系统里的角色

在之前的 im 微服务架构里,userserver 承担 3 个核心职责:

  1. 用户认证:注册(用户名 / 手机号)、登录(用户名密码 / 手机验证码)、会话管理;

  2. 用户信息管理:头像、昵称、签名、手机号的修改与查询;

  3. 基础支撑:给其他服务提供用户信息(比如好友服务查好友资料、消息服务查发送者信息)。

所以设计时,必须考虑可扩展性(比如后续加第三方登录)、可测试性(比如不用真实短信也能测手机号登录)、性能(登录会话用 redis 缓存,避免查库)。

二、核心设计:从依赖到架构,拒绝 “硬编码”

1. 依赖注入:让服务更灵活(踩过坑才懂的重要性)

最开始写 userserviceimpl 的时候,我直接在类里 new 了 dmsclient(真实短信服务),后来发现个人开发者没法申请企业短信资质,想换成模拟服务时,改了大半天代码。后来重构时,把所有外部依赖都通过构造函数注入,这才清爽了。

看核心构造函数:

userserviceimpl(
    const mocksmsclient::ptr& mock_sms_client,  // 短信服务(真实/模拟可替换)
    const std::shared_ptr<elasticlient::client>& es_client,  // es(用户搜索用)
    const std::shared_ptr<odb::core::database>& mysql_client,  // mysql(用户数据存储)
    const std::shared_ptr<sw::redis::redis>& redis_client,  // redis(会话/验证码)
    const servicemanager::ptr& channel_manager,  // 服务管理(调用文件服务等)
    const std::string& file_service_name  // 文件服务名称(定位服务用)
) :
    _es_user(std::make_shared<esuser>(es_client)),
    _mysql_user(std::make_shared<usertable>(mysql_client)),
    _redis_session(std::make_shared<session>(redis_client)),
    _redis_status(std::make_shared<status>(redis_client)),
    _redis_codes(std::make_shared<codes>(redis_client)),
    _file_service_name(file_service_name),
    _mm_channels(channel_manager),
    _dms_client(mock_sms_client)  // 注入短信服务,而非内部new
{
    _es_user->createindex();  // 初始化es用户索引
}

这样设计的好处:

  • 替换依赖不碰业务代码:把mocksmsclient换成真实dmsclient,只需要改构建器(userserverbuilder)的初始化逻辑,userserviceimpl里的getphoneverifycode完全不用动;

  • 测试方便:写单元测试时,能注入 “假的 redis 客户端”“假的 es 客户端”,不用依赖真实中间件。

2. 核心依赖拆解:每个组件各司其职

依赖组件作用实战细节
mocksmsclient模拟短信发送内部不调用外部平台,只打印日志 + 存 redis 验证码
odb(mysql orm)用户数据 crud需用odb工具生成 orm 代码,避免手写 sql
redis(sw::redis++)会话存储、验证码、登录状态会话过期设 2 小时,验证码 5 分钟过期
elasticlient用户搜索(比如好友搜索)初始化时创建user索引,支持昵称 / 手机号模糊查
servicemanager调用其他微服务(如文件服务上传头像)基于 etcd 发现服务节点,rr 轮询负载均衡

三、核心功能落地:从代码到业务,讲透细节

1. 用户注册:不只是存数据,还要做校验

注册逻辑看起来简单,但细节容易出问题,比如密码强度、昵称重复。看关键代码:

bool password_check(const std::string &password) {
    // 密码规则:6-15位,只含字母、数字、_、-
    if (password.size() < 6 || password.size() > 15) {
        log_error("密码长度不合法:{}-{}", password, password.size());
        return false;
    }
    for (int i = 0; i < password.size(); i++) {
        if (!((password[i] > 'a' && password[i] < 'z') ||
            (password[i] > 'a' && password[i] < 'z') ||
            (password[i] > '0' && password[i] < '9') ||
            password[i] == '_' || password[i] == '-')) {
            log_error("密码字符不合法:{}", password);
            return false;
        }
    }
    return true;
}

void userregister(...) {
    // 1. 取请求参数
    std::string nickname = request->nickname();
    std::string password = request->password();
    
    // 2. 校验昵称、密码
    if (!nickname_check(nickname)) {
        return err_response("用户名长度不合法!");
    }
    if (!password_check(password)) {
        return err_response("密码格式不合法!");
    }
    
    // 3. 查昵称是否已存在(odb orm调用)
    auto user = _mysql_user->select_by_nickname(nickname);
    if (user) {
        return err_response("用户名被占用!");
    }
    
    // 4. 生成用户id,存mysql+es
    std::string uid = uuid();  // 自定义工具函数,生成唯一id
    user = std::make_shared<user>(uid, nickname, password);
    if (!_mysql_user->insert(user)) {
        return err_response("mysql数据库新增数据失败!");
    }
    if (!_es_user->appenddata(uid, "", nickname, "", "")) {
        return err_response("es搜索引擎新增数据失败!");
    }
    
    // 5. 返回成功
    response->set_success(true);
}

实战踩坑:最开始没做密码校验,测试时输入特殊字符导致数据库存储异常,后来加了严格的字符校验,还在日志里打印不合法的密码,方便排查问题。

2. 手机号验证码:用模拟服务突破平台限制

真实短信服务(如阿里云 dms)需要企业资质,个人开发没法用,所以做了mocksmsclient来模拟。核心逻辑是:生成验证码→存 redis→返回验证码 id,校验时从 redis 查。

第一步:设计 mocksmsclient

// mock_sms.hpp
class mocksmsclient {
public:
    using ptr = std::shared_ptr<mocksmsclient>;
    mocksmsclient(const codes::ptr& codes_client) : _codes_client(codes_client) {}
    
    // 与真实dmsclient接口完全一致,方便替换
    bool send(const std::string& phone, const std::string& code) {
        // 不调用外部平台,只打印日志(测试时能直接看到验证码)
        log_info("【模拟短信】向{}发送验证码:{}", phone, code);
        return true;
    }
private:
    codes::ptr _codes_client;  // 用于后续扩展,比如存验证码
};

第二步:集成到 getphoneverifycode

void getphoneverifycode(...) {
    // 1. 校验手机号格式(11位,以1开头,第二位3-9)
    std::string phone = request->phone_number();
    if (!phone_check(phone)) {
        return err_response("手机号码格式错误!");
    }
    
    // 2. 生成4位验证码(自定义工具函数vcode())
    std::string code_id = uuid();
    std::string code = vcode();  // 返回如"1234"
    
    // 3. 调用模拟短信服务(实际只打日志)
    if (!_dms_client->send(phone, code)) {
        return err_response("短信验证码发送失败!");
    }
    
    // 4. 存redis(5分钟过期)
    _redis_codes->append(code_id, code, std::chrono::minutes(5));
    
    // 5. 返回验证码id
    response->set_verify_code_id(code_id);
    response->set_success(true);
}

关键优势:后来要对接真实短信服务时,只需要实现一个realsmsclient,保持send接口一致,在userserverbuilder里换个注入对象就行,业务代码一行不用改。

3. 登录会话管理:redis 防多端登录

登录成功后,要生成会话 id(ssid),存 redis,还要标记用户登录状态,防止同一账号多端登录:

void userlogin(...) {
    // 1. 校验用户名密码
    auto user = _mysql_user->select_by_nickname(nickname);
    if (!user || password != user->password()) {
        return err_response("用户名或密码错误!");
    }
    
    // 2. 查是否已登录(redis查登录状态)
    if (_redis_status->exists(user->user_id())) {
        return err_response("用户已在其他地方登录!");
    }
    
    // 3. 生成ssid,存redis(2小时过期)
    std::string ssid = uuid();
    _redis_session->append(ssid, user->user_id(), std::chrono::hours(2));
    
    // 4. 标记登录状态(2小时过期,与会话同步)
    _redis_status->append(user->user_id(), std::chrono::hours(2));
    
    // 5. 返回ssid
    response->set_login_session_id(ssid);
    response->set_success(true);
}

细节_redis_session_redis_status是封装的 redis 操作类,内部调用sw::redis::redis::set并设置过期时间,避免手动写 redis 命令,减少出错概率。

4. 用户信息修改:联动多存储(mysql+es + 文件服务)

以 “设置头像” 为例,需要上传头像到文件服务→更新 mysql 的 avatar_id→同步 es 信息:

void setuseravatar(...) {
    // 1. 取用户id和头像数据
    std::string uid = request->user_id();
    std::string avatar_data = request->avatar();
    
    // 2. 查用户是否存在
    auto user = _mysql_user->select_by_id(uid);
    if (!user) {
        return err_response("未找到用户信息!");
    }
    
    // 3. 调用文件服务上传头像(通过servicemanager找文件服务节点)
    auto channel = _mm_channels->choose(_file_service_name);
    fileservice_stub stub(channel.get());
    putsinglefilereq file_req;
    putsinglefilersp file_rsp;
    file_req.mutable_file_data()->set_file_content(avatar_data);
    stub.putsinglefile(&cntl, &file_req, &file_rsp, nullptr);
    if (cntl.failed() || !file_rsp.success()) {
        return err_response("文件子服务调用失败!");
    }
    
    // 4. 更新mysql的avatar_id
    std::string avatar_id = file_rsp.file_info().file_id();
    user->avatar_id(avatar_id);
    if (!_mysql_user->update(user)) {
        return err_response("更新数据库用户头像id失败!");
    }
    
    // 5. 同步es信息
    if (!_es_user->appenddata(user->user_id(), user->phone(), 
        user->nickname(), user->description(), avatar_id)) {
        return err_response("更新搜索引擎用户头像id失败!");
    }
    
    response->set_success(true);
}

经验:文件服务调用可能失败,后来加了重试机制(失败后重试 2 次),还在日志里打印文件服务的地址和错误信息,方便定位是网络问题还是服务本身的问题。

四、实战踩坑记录:这些问题比代码更重要

1. odb 代码生成遗漏导致链接错误

最开始用 odb 的 orm,只写了user.hxx,没生成 orm 实现代码,编译时出现一堆undefined reference to odb::access::object_traits_impl<zrt::user>错误。

解决方法

  1. 安装 odb 工具:sudo apt install odb

  2. 生成 orm 代码:odb -d mysql --std c++11 user.hxx -o source/

  3. cmake 里添加生成的user-odb.cxx到源文件列表:

add_executable(user_server
    source/user_server.cc
    source/user-odb.cxx  # 必须加,否则链接不到orm实现
)

2. redis 客户端初始化顺序错误

最开始在make_redis_object里创建了codes实例,但没赋值给_codes_client,导致make_mock_sms_object_codes_client是空的,运行崩溃。

解决方法:调整初始化顺序,确保 redis 客户端先初始化,再创建codesmocksmsclient

// userserverbuilder
void make_redis_object(...) {
    _redis_client = redisclientfactory::create(...);
    // 初始化codes,赋值给成员变量
    _codes_client = std::make_shared<codes>(_redis_client);
}

void make_mock_sms_object() {
    // 此时_codes_client已初始化,不会空指针
    _mock_sms_client = std::make_shared<mocksmsclient>(_codes_client);
}

3. 依赖库链接不全导致未定义错误

编译时出现undefined reference to sw::redis::redis::set,是因为 cmake 没链接swredis++hiredis库。

解决方法:在 cmake 里添加链接:

# 查找依赖库
find_package(swredis++ required)
find_package(hiredis required)

# 链接到目标
target_link_libraries(user_server
    private
    sw::redis++::swredis++
    hiredis::hiredis
    odb::mysql  # odb mysql库
    elasticlient::elasticlient  # es客户端库
    brpc  # rpc库
    pthread  # 线程库
)

五、总结:userserver 设计的 3 个核心要点

  1. 无状态设计:用户服务不存本地数据(会话、状态都放 redis),方便横向扩展,加节点就能扛更高并发;

  2. 依赖注入优先:所有外部依赖(短信、数据库、缓存)都通过构造函数注入,方便替换和测试,比如用模拟短信突破平台限制;

  3. 分层清晰:业务逻辑(注册登录)、数据访问(odb/redis/es)、服务调用(servicemanager)分层,修改某一层不影响其他层。

做用户服务时,最容易忽略的是 “可测试性” 和 “容错性”—— 比如一开始没做模拟短信,导致没法本地测试手机号登录;没加重试机制,文件服务偶尔超时就失败。这些问题都是实战中踩出来的,比单纯的代码实现更有价值。

到此这篇关于c++微服务userserver设计与实现方法的文章就介绍到这了,更多相关c++微服务userserver实现内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com