一、核心概念回顾
在 RPC 分布式通信中:
服务发布:RPC 服务提供者将本地实现的服务(如
UserService)注册到框架中,并通过 Zookeeper 暴露服务地址,供消费者发现;客户端调用请求处理:服务提供者接收消费者的 RPC 请求,解析请求中的「服务名、方法名、参数」,通过 Protobuf 反射调用本地方法,将结果返回给消费者。
二、完整实现流程(端到端)
我们以「用户登录服务(UserService::Login)」为例,完整实现:
- 定义 Protobuf 服务接口;
- 实现本地服务逻辑;
- 发布 RPC 服务(基于
RpcProvider); - 解析并处理客户端的登录请求。
步骤 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.h和user_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)
通过RpcProvider将UserService发布为 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/Login→IP:Port)。
步骤 4:处理客户端调用请求(RpcProvider 核心逻辑)
RpcProvider的onMessage方法是处理客户端 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);
总结
-
服务发布核心 :通过 Protobuf 定义接口→实现本地业务逻辑→
NotifyService注册到框架→Run启动服务 + ZK 注册; -
请求处理核心:解析「4 字节头长度 + RpcHeader + 参数」→Protobuf 反射调用本地方法→回调发送响应;
-
关键保障:
-
数据格式(4 字节头长度)解决 TCP 粘包 / 拆包;
-
ZK 临时节点实现服务自动上下线;
-
Protobuf 反射实现通用的方法调用,无需为每个服务编写解析逻辑;
-
加锁 + 异常捕获保证框架稳定性。
-