RPC 调用方的核心使命是:让开发者像调用本地方法一样调用远程方法,底层自动完成以下工作:
封装符合协议的 RPC 请求数据(解决 TCP 粘包 / 拆包);
从 Zookeeper 动态获取目标服务的 IP:Port(服务发现);
建立 TCP 连接,发送请求数据;
接收服务端响应,反序列化得到结果;
处理所有异常(网络错误、序列化失败、服务不存在等)。
核心流程总览
| 步骤 | 核心操作 | 关键目的 |
|---|---|---|
| 1 | 开发者调用 stub 方法 | 上层无感知,像调用本地函数一样发起远程调用 |
| 2 | 触发MprpcChannel::CallMethod |
进入 RPC 框架底层处理逻辑,替代 Protobuf 默认的本地调用 |
| 3 | 反射解析 + 参数序列化 | 1. 从方法描述符中提取服务名、方法名2. 将请求参数序列化为字符串,获取参数长度 |
| 4 | 封装 RPC 请求头 | 构造RpcHeader对象并序列化,存储服务名、方法名、参数长度,解决 TCP 粘包的核心基础 |
| 5 | 拼接请求数据 | 按固定格式拼接:[4字节headerSize] + [RpcHeader序列化串] + [参数序列化串],确保服务端能精准解析 |
| 6 | ZK 服务发现 | 1. 拼接 ZK 节点路径/服务名/方法名2. 获取节点数据(服务端 IP:Port)3. 解析 IP 和 Port,实现地址动态获取 |
| 7 | 建立 TCP 连接 | 1. 创建套接字2. 配置服务端地址3. 支持重试机制,提升连接可靠性 |
| 8 | 发送请求数据 | 通过 TCP 连接发送完整的请求数据,完成调用请求投递 |
| 9 | 接收响应数据 | 阻塞读取服务端返回的响应字节流,避免固定缓冲区截断 |
| 10 | 反序列化响应 | 用ParseFromArray按字节长度解析响应,填充到response对象,解决\0截断问题 |
| 11 | 结果返回 | 开发者通过response获取调用结果,通过controller获取错误信息 |
代码整体功能总结
MprpcChannel是 Protobuf RpcChannel的自定义实现(Protobuf 要求 RPC 客户端必须实现RpcChannel接口),CallMethod是核心方法,替代了 Protobuf 默认的本地调用逻辑,实现:
- 解析要调用的服务名、方法名,序列化请求参数;
- 封装 RPC 请求头(RpcHeader),拼接完整的请求数据(解决粘包);
- 从 Zookeeper 获取目标服务的 IP:Port(服务发现);
- 建立 TCP 连接,发送 RPC 请求;
- 接收服务端响应,反序列化得到结果;
- 处理所有异常场景,通过
RpcController返回错误信息。
代码逐模块详细解析
void MprpcChannel::CallMethod(const google::protobuf::MethodDescriptor *method,
google::protobuf::RpcController *controller,
const google::protobuf::Message *request,
google::protobuf::Message *response,
google::protobuf::Closure *done)
{
// ===================== 第一步:提取服务名和方法名(Protobuf反射) =====================
const google::protobuf::ServiceDescriptor *sd = method->service();
std::string service_name = sd->name(); // 如:UserServiceRpc
std::string method_name = method->name(); // 如:Login
// ===================== 第二步:序列化请求参数 =====================
uint32_t args_size = 0;
std::string args_str;
if (request->SerializeToString(&args_str)) // 序列化请求参数(如LoginRequest)
{
args_size = args_str.size(); // 记录参数长度
}
else
{
// 序列化失败:通过controller设置错误信息,返回
controller->SetFailed("serialize request error!");
return;
}
// ===================== 第三步:封装RPC请求头(RpcHeader) =====================
mprpc::RpcHeader rpcHeader;
rpcHeader.set_service_name(service_name); // 设置服务名
rpcHeader.set_method_name(method_name); // 设置方法名
rpcHeader.set_args_size(args_size); // 设置参数长度
uint32_t header_size = 0;
std::string rpc_header_str;
if (rpcHeader.SerializeToString(&rpc_header_str)) // 序列化请求头
{
header_size = rpc_header_str.size(); // 记录请求头长度
}
else
{
controller->SetFailed("serialize rpc header error!");
return;
}
// ===================== 第四步:拼接完整的RPC请求数据(解决粘包核心) =====================
std::string send_rpc_str;
// 1. 前4字节:请求头长度(header_size)→ 解决TCP粘包的关键
send_rpc_str.insert(0, std::string((char *)&header_size, 4));
// 2. 拼接序列化后的请求头
send_rpc_str += rpc_header_str;
// 3. 拼接序列化后的请求参数
send_rpc_str += args_str;
// 调试输出
std::cout << "============================================" << std::endl;
std::cout << "header_size: " << header_size << std::endl;
std::cout << "rpc_header_str: " << rpc_header_str << 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;
// ===================== 第五步:创建TCP客户端套接字 =====================
int clientfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == clientfd)
{
char errtxt[512] = {0};
sprintf(errtxt, "create socket error! errno:%d", errno);
controller->SetFailed(errtxt);
return;
}
// ===================== 第六步:从ZK获取服务地址(服务发现核心) =====================
ZkClient zkCli;
zkCli.Start(); // 连接ZK服务器
// 构建ZK节点路径:/服务名/方法名(与服务提供者注册的路径一致)
std::string method_path = "/" + service_name + "/" + method_name;
// 获取节点数据(即服务提供者的IP:Port,如127.0.0.1:8080)
std::string host_data = zkCli.GetData(method_path.c_str());
if (host_data == "")
{
controller->SetFailed(method_path + " is not exist!");
close(clientfd); // 必须关闭套接字,避免资源泄漏
return;
}
// 解析IP和Port(分割":")
int idx = host_data.find(":");
if (idx == -1)
{
controller->SetFailed(method_path + " address is invalid!");
close(clientfd);
return;
}
std::string ip = host_data.substr(0, idx);
uint16_t port = atoi(host_data.substr(idx + 1, host_data.size() - idx).c_str());
// ===================== 第七步:连接RPC服务端 =====================
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(port); // 网络字节序转换
server_addr.sin_addr.s_addr = inet_addr(ip.c_str());
// 建立TCP连接
if (-1 == connect(clientfd, (struct sockaddr *)&server_addr, sizeof(server_addr)))
{
close(clientfd);
char errtxt[512] = {0};
sprintf(errtxt, "connect error! errno:%d", errno);
controller->SetFailed(errtxt);
return;
}
// ===================== 第八步:发送RPC请求数据 =====================
if (-1 == send(clientfd, send_rpc_str.c_str(), send_rpc_str.size(), 0))
{
close(clientfd);
char errtxt[512] = {0};
sprintf(errtxt, "send error! errno:%d", errno);
controller->SetFailed(errtxt);
return;
}
// ===================== 第九步:接收服务端响应 =====================
char recv_buf[1024] = {0};
int recv_size = 0;
// 阻塞接收响应(短连接模式,服务端发送后会断开连接)
if (-1 == (recv_size = recv(clientfd, recv_buf, 1024, 0)))
{
close(clientfd);
char errtxt[512] = {0};
sprintf(errtxt, "recv error! errno:%d", errno);
controller->SetFailed(errtxt);
return;
}
// ===================== 第十步:反序列化响应数据 =====================
// 关键修复:原代码用ParseFromString会因\0截断,改用ParseFromArray(按长度解析)
// if (!response->ParseFromString(response_str)) // 有bug的写法
if (!response->ParseFromArray(recv_buf, recv_size))
{
close(clientfd);
char errtxt[512] = {0};
sprintf(errtxt, "parse error! response_str:%s", recv_buf);
controller->SetFailed(errtxt);
return;
}
// ===================== 第十一步:关闭套接字 =====================
close(clientfd);
}
核心原理与关键细节
1. 为什么要封装 4 字节的请求头长度?
和服务端的onMessage对应,TCP 是流式协议,粘包 / 拆包问题会导致数据边界模糊:
- 客户端先发送 4 字节的
header_size,服务端读取这 4 字节后,就能确定后续要读多少字节的RpcHeader; - 从
RpcHeader中读取args_size,再读固定长度的参数,确保服务端能精准解析一个完整的请求。
2. 服务发现的核心逻辑
- 客户端通过 ZK 的节点路径
/服务名/方法名,获取服务提供者注册的 IP:Port; - 无需硬编码服务地址,实现「服务地址动态发现」,服务提供者扩容 / 缩容时,客户端能自动获取最新地址(需结合 ZK Watcher,当前代码未实现)。
3. 关键 bug 修复:ParseFromArray vs ParseFromString
- 原代码注释中提到的 bug:
recv_buf是字符数组,若响应数据中包含\0(如字符串中的空字符),std::string(response_str, recv_buf)会截断数据,导致反序列化失败; - 解决方案:
ParseFromArray(recv_buf, recv_size)直接按接收的字节长度解析,不依赖\0结尾,彻底解决截断问题。
总结
-
请求封装:按「4 字节 headerSize + RpcHeader + 参数」封装数据,解决 TCP 粘包 / 拆包;
-
服务发现:从 ZK 动态获取服务地址,替代硬编码,实现服务解耦;
-
网络通信:RAII 封装套接字避免资源泄漏,连接重试提升可靠性;
-
响应解析:ParseFromArray 按字节长度解析,避免数据截断;
-
易用性:开发者只需调用 stub. 方法名 (),底层自动完成所有远程调用逻辑,符合「本地调用」的体验。