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 反射实现通用的方法调用,无需为每个服务编写解析逻辑;

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

相关推荐
giaz14n9X9 小时前
Redis 分布式锁进阶第六十三篇
分布式
ha_lydms11 小时前
AnalyticDB分区、分布键性能优化
android·大数据·分布式·性能优化·分布式计算·分区·analyticdb
pqk6V6Vep11 小时前
Redis 分布式锁进阶第一篇讲解
数据库·redis·分布式
giaz14n9X11 小时前
Redis 分布式锁进阶第六十一篇
数据库·redis·分布式
ytttr87313 小时前
Qt 数字键盘实现
开发语言·qt
洛水水13 小时前
消息队列与Kafka详解
分布式·kafka
hoiii18713 小时前
Qt 实现屏幕截图功能
开发语言·qt·命令模式
满天星830357714 小时前
【Qt】信号和槽(三) (断开连接和lambda函数)
qt
鸿乃江边鸟14 小时前
Spark中怎么做Spark canonicalize归一化
大数据·分布式·spark
fpcc14 小时前
C++编程实践—C++实现类似Qt的信号槽机制
c++·qt