RPC分布式通信(5)--发布 RPC 服务、处理客户端调用请求

一、核心概念回顾

在 RPC 分布式通信中:

  • 服务发布:RPC 服务提供者将本地实现的服务(如UserService)注册到框架中,并通过 Zookeeper 暴露服务地址,供消费者发现;

  • 客户端调用请求处理:服务提供者接收消费者的 RPC 请求,解析请求中的「服务名、方法名、参数」,通过 Protobuf 反射调用本地方法,将结果返回给消费者。

二、完整实现流程(端到端)

我们以「用户登录服务(UserService::Login)」为例,完整实现:

  1. 定义 Protobuf 服务接口;
  2. 实现本地服务逻辑;
  3. 发布 RPC 服务(基于RpcProvider);
  4. 解析并处理客户端的登录请求。
步骤 1:定义 Protobuf 服务接口(核心)

首先通过 Protobuf 定义 RPC 服务的接口(.proto文件),这是 RPC 框架的「接口契约」,确保客户端和服务端数据格式一致。

复制代码
// user_service.proto
syntax = "proto3";

package mprpc;

// 登录请求参数
message LoginRequest {
    string name = 1;
    string pwd = 2;
}

// 登录响应结果
message LoginResponse {
    int32 errcode = 1; // 错误码:0=成功,非0=失败
    string errmsg = 2; // 错误信息
    bool success = 3;  // 是否登录成功
}

// RPC服务定义(必须继承google::protobuf::Service)
service UserServiceRpc {
    rpc Login(LoginRequest) returns (LoginResponse); // 登录方法
}

通过 Protobuf 编译工具生成 C++ 代码(命令示例):

复制代码
protoc --cpp_out=. user_service.proto

编译后会生成user_service.pb.huser_service.pb.cc,包含:

  • LoginRequest/LoginResponse消息类;
  • UserServiceRpc抽象服务类(继承google::protobuf::Service)。
步骤 2:实现本地服务逻辑

服务提供者需要继承 Protobuf 生成的抽象服务类,实现具体的业务逻辑(如登录校验)。

复制代码
// user_service_impl.h
#pragma once
#include "user_service.pb.h"
#include <string>

// 实现UserServiceRpc的具体业务逻辑
class UserService : public mprpc::UserServiceRpc {
public:
    // 本地业务方法(真实的登录逻辑)
    bool Login(const std::string& name, const std::string& pwd) {
        // 模拟数据库校验逻辑
        if (name == "wangt" && pwd == "123456") {
            return true;
        }
        return false;
    }

    // 重写Protobuf生成的虚函数(RPC框架会调用这个方法)
    void Login(::google::protobuf::RpcController* controller,
               const ::mprpc::LoginRequest* request,
               ::mprpc::LoginResponse* response,
               ::google::protobuf::Closure* done) override {
        // 1. 解析请求参数
        std::string name = request->name();
        std::string pwd = request->pwd();

        // 2. 调用本地业务方法
        bool login_result = Login(name, pwd);

        // 3. 构造响应结果
        response->set_errcode(0);
        response->set_errmsg("");
        response->set_success(login_result);

        // 4. 执行回调(通知框架发送响应)
        done->Run();
    }
};
步骤 3:发布 RPC 服务(基于 RpcProvider)

通过RpcProviderUserService发布为 RPC 服务,并注册到 Zookeeper。

复制代码
// main.cpp(服务提供者主函数)
#include "rpcprovide.hpp"
#include "user_service_impl.h"
#include "mprpcapplication.hpp"

int main(int argc, char* argv[]) {
    // 1. 初始化RPC框架(读取配置文件)
    MprpcApplication::Init(argc, argv);

    // 2. 创建RPC服务提供者对象
    RpcProvider provider;

    // 3. 发布RPC服务(核心:将UserService注册到框架)
    provider.NotifyService(new UserService());

    // 4. 启动RPC服务(包含ZK注册、muduo网络服务启动)
    provider.Run();

    return 0;
}

关键细节

  • NotifyService(new UserService()):框架通过 Protobuf 反射解析UserService的服务名(UserServiceRpc)和方法名(Login),并缓存到m_serviceMap

  • provider.Run():启动 muduo TCP 服务(监听配置的 IP:Port),同时将服务信息注册到 ZK(/UserServiceRpc/LoginIP:Port)。

步骤 4:处理客户端调用请求(RpcProvider 核心逻辑)

RpcProvideronMessage方法是处理客户端 RPC 请求的核心,它承接 muduo 网络库的消息回调,完成「请求解析→反射调用→响应返回」的全流程。

复制代码
// 处理客户端RPC请求(muduo的消息回调函数)
void RpcProvider::onMessage(const muduo::net::TcpConnectionPtr& conn, 
                            muduo::net::Buffer* buffer, 
                            muduo::Timestamp timestamp) {
    // ===================== 第一步:读取完整的请求数据 =====================
    // 从muduo的Buffer中读取所有数据(解决TCP粘包的基础:先读全数据再解析)
    std::string recv_data = buffer->retrieveAllAsString();

    // ===================== 第二步:解析RpcHeader长度(解决粘包核心) =====================
    // RPC请求数据格式:[4字节Header长度][RpcHeader序列化数据][参数序列化数据]
    // 前4字节是无符号int,存储RpcHeader的字节长度
    uint32_t header_size = 0;
    // 从recv_data的第0位开始,拷贝4字节到header_size(注意类型转换)
    recv_data.copy((char*)&header_size, 4, 0);

    // ===================== 第三步:解析RpcHeader(获取服务/方法元信息) =====================
    // 提取RpcHeader的序列化字符串(从第4字节开始,长度为header_size)
    std::string rpc_header_str = recv_data.substr(4, header_size);
    mprpc::RpcHeader rpc_header; // 预定义的Protobuf结构体,存储服务名/方法名/参数长度
    if (!rpc_header.ParseFromString(rpc_header_str)) {
        std::cerr << "[ERROR] 解析RpcHeader失败:" << rpc_header_str << std::endl;
        return;
    }

    // 从RpcHeader中提取核心信息
    std::string service_name = rpc_header.service_name();  // 如:UserServiceRpc
    std::string method_name = rpc_header.method_name();    // 如:Login
    uint32_t args_size = rpc_header.args_size();           // 参数的字节长度

    // ===================== 第四步:解析方法参数(业务请求数据) =====================
    // 参数数据起始位置:4字节(Header长度) + header_size(Header数据)
    std::string args_str = recv_data.substr(4 + header_size, args_size);

    // 调试输出(方便排查问题)
    std::cout << "=====================================" << std::endl;
    std::cout << "header_size: " << header_size << std::endl;
    std::cout << "service_name: " << service_name << std::endl;
    std::cout << "method_name: " << method_name << std::endl;
    std::cout << "args_str: " << args_str << std::endl;
    std::cout << "=====================================" << std::endl;

    // ===================== 第五步:查找本地注册的服务和方法 =====================
    // 加锁保证m_serviceMap线程安全(多IO线程可能同时访问)
    std::lock_guard<std::mutex> lock(m_serviceMapMutex);
    auto service_it = m_serviceMap.find(service_name);
    if (service_it == m_serviceMap.end()) {
        std::cerr << "[ERROR] 服务不存在:" << service_name << std::endl;
        return;
    }

    auto method_it = service_it->second.m_methodMap.find(method_name);
    if (method_it == service_it->second.m_methodMap.end()) {
        std::cerr << "[ERROR] 方法不存在:" << service_name << ":" << method_name << std::endl;
        return;
    }

    // 获取核心对象:服务实例(如UserService) + 方法描述符(如Login方法)
    google::protobuf::Service* service = service_it->second.m_service; // 本地业务对象
    const google::protobuf::MethodDescriptor* method = method_it->second; // 方法元信息

    // ===================== 第六步:创建请求/响应对象(Protobuf反射) =====================
    // 1. 创建请求对象(如LoginRequest):通过服务+方法反射获取请求原型并实例化
    google::protobuf::Message* request = service->GetRequestPrototype(method).New();
    if (!request->ParseFromString(args_str)) {
        std::cerr << "[ERROR] 解析请求参数失败:" << args_str << std::endl;
        delete request; // 释放内存,避免泄漏
        return;
    }

    // 2. 创建响应对象(如LoginResponse):同理反射创建
    google::protobuf::Message* response = service->GetResponsePrototype(method).New();

    // ===================== 第七步:绑定回调函数(执行完业务后发送响应) =====================
    // NewCallback:Protobuf提供的回调封装,参数说明:
    // - this:当前RpcProvider实例
    // - &RpcProvider::SendRpcResponse:回调函数(发送响应)
    // - conn:TCP连接对象(用于发送数据)
    // - response:响应对象(要返回给客户端的数据)
    google::protobuf::Closure* done = google::protobuf::NewCallback<
        RpcProvider, 
        const muduo::net::TcpConnectionPtr&, 
        google::protobuf::Message*
    >(this, &RpcProvider::SendRpcResponse, conn, response);

    // ===================== 第八步:反射调用本地业务方法(核心) =====================
    try {
        // CallMethod:Protobuf Service的核心反射方法,等价于:
        // UserService->Login(nullptr, request, response, done)
        // 参数说明:
        // - method:方法描述符
        // - nullptr:RpcController(暂未使用)
        // - request:请求参数对象
        // - response:响应结果对象
        // - done:回调函数(执行完业务后触发)
        service->CallMethod(method, nullptr, request, response, done);
    } catch (const std::exception& e) {
        // 捕获业务方法异常,避免框架崩溃,构造错误响应
        std::cerr << "[ERROR] 调用业务方法异常:" << e.what() << std::endl;
        // 强制转换为具体的响应类型(如LoginResponse),设置错误信息
        mprpc::LoginResponse* err_resp = dynamic_cast<mprpc::LoginResponse*>(response);
        if (err_resp) {
            err_resp->set_errcode(-1);
            err_resp->set_errmsg(e.what());
            err_resp->set_success(false);
        }
        done->Run(); // 即使异常,也要触发回调发送响应
    }

    // ===================== 第九步:释放请求对象内存 =====================
    delete request; // 响应对象在SendRpcResponse中释放
}

// 辅助函数:发送RPC响应给客户端
void RpcProvider::SendRpcResponse(const muduo::net::TcpConnectionPtr& conn, 
                                  google::protobuf::Message* response) {
    std::string response_str;
    // 序列化响应对象(如LoginResponse)
    if (response->SerializeToString(&response_str)) {
        // 通过muduo的TCP连接发送响应数据
        conn->send(response_str);
        std::cout << "[INFO] 发送响应成功,长度:" << response_str.size() << std::endl;
    } else {
        std::cerr << "[ERROR] 序列化响应失败" << std::endl;
    }

    // 短连接设计:主动断开连接(模拟HTTP短连接,减少资源占用)
    conn->shutdown();
    // 释放响应对象内存
    delete response;
}

核心原理

  • Protobuf 的Service::CallMethod是反射的核心入口,它会调用我们实现的UserService::Login方法;
  • done->Run()会触发SendRpcResponse,将LoginResponse(success=true)序列化后发送给客户端。

四、关键优化与避坑点

1. 数据粘包 / 拆包问题
  • 问题:TCP 是流式协议,客户端发送的请求可能被拆分成多个包,或多个请求粘成一个包;
  • 解决方案:我们的请求格式「4 字节头长度 + RpcHeader + 参数」天然解决粘包 ------ 先读 4 字节确定头长度,再读固定长度的头,最后读固定长度的参数,确保完整解析一个请求。
2. 线程安全问题
  • 问题m_serviceMap可能被多个 muduo IO 线程同时访问(如多个客户端请求);

  • 解决方案 :对m_serviceMap的读写加互斥锁:

    复制代码
    // 在RpcProvider中新增互斥锁
    std::mutex m_serviceMapMutex;
    
    // 访问m_serviceMap时加锁
    std::lock_guard<std::mutex> lock(m_serviceMapMutex);
    auto it = m_serviceMap.find(service_name);
3. 业务异常处理
  • 问题:本地业务方法(如 Login)抛出异常会导致框架崩溃;

  • 解决方案 :在CallMethod外层加 try-catch,捕获异常并构造错误响应:

    复制代码
    try {
        service->CallMethod(method, nullptr, req, resp, done);
    } catch (const std::exception& e) {
        mprpc::LoginResponse *err_resp = dynamic_cast<mprpc::LoginResponse*>(resp);
        err_resp->set_errcode(-1);
        err_resp->set_errmsg(e.what());
        err_resp->set_success(false);
        done->Run(); // 仍触发回调,返回错误响应
    }
4. 服务优雅退出
  • 问题:直接 kill 进程会导致 ZK 临时节点未及时删除(ZK 检测连接断开有延迟);

  • 解决方案 :注册信号处理函数,优雅关闭服务:

    复制代码
    void StopRpcProvider(int sig) {
        // 停止muduo事件循环
        loop.quit();
        // 关闭ZK连接(可选,进程退出会自动关闭)
        m_zkCli.close();
        LOG_INFO << "RpcProvider stopped gracefully";
        exit(0);
    }
    
    // 在Run方法中注册信号处理
    signal(SIGINT, StopRpcProvider);
    signal(SIGTERM, StopRpcProvider);

总结

  1. 服务发布核心 :通过 Protobuf 定义接口→实现本地业务逻辑→NotifyService注册到框架→Run启动服务 + ZK 注册;

  2. 请求处理核心:解析「4 字节头长度 + RpcHeader + 参数」→Protobuf 反射调用本地方法→回调发送响应;

  3. 关键保障

    • 数据格式(4 字节头长度)解决 TCP 粘包 / 拆包;

    • ZK 临时节点实现服务自动上下线;

    • Protobuf 反射实现通用的方法调用,无需为每个服务编写解析逻辑;

    • 加锁 + 异常捕获保证框架稳定性。

相关推荐
少控科技2 小时前
QT进阶日记004
开发语言·qt
a努力。2 小时前
宇树Java面试被问:数据库死锁检测和自动回滚机制
java·数据库·elasticsearch·面试·职场和发展·rpc·jenkins
LDG_AGI2 小时前
【机器学习】深度学习推荐系统(三十):X 推荐算法Phoenix rerank机制
人工智能·分布式·深度学习·算法·机器学习·推荐算法
秋雨雁南飞3 小时前
C# 分布式消息框架
分布式
ZePingPingZe3 小时前
TCC—最终一致性分布式事务方案及【案例】
分布式·微服务
alonewolf_993 小时前
RabbitMQ高级功能全面解析:队列选型、死信队列与消息分片实战指南
分布式·消息队列·rabbitmq·ruby
抠脚学代码3 小时前
Qt与Linux
linux·数据库·qt
机器视觉知识推荐、就业指导4 小时前
Qt 6 所有 C++ 类(官方完整清单 · 原始索引版)
开发语言·c++·qt
hellojackjiang20114 小时前
如何保障分布式IM聊天系统的消息有序性(即消息不乱)
分布式·架构·即时通讯·im开发