RPC分布式通信(6)---调用方自动封装请求数据、从 ZK 获取服务地址、建立 TCP 连接发送请求、接收并解析响应

RPC 调用方的核心使命是:让开发者像调用本地方法一样调用远程方法,底层自动完成以下工作:

  1. 封装符合协议的 RPC 请求数据(解决 TCP 粘包 / 拆包);

  2. 从 Zookeeper 动态获取目标服务的 IP:Port(服务发现);

  3. 建立 TCP 连接,发送请求数据;

  4. 接收服务端响应,反序列化得到结果;

  5. 处理所有异常(网络错误、序列化失败、服务不存在等)。

核心流程总览

步骤 核心操作 关键目的
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 默认的本地调用逻辑,实现:

  1. 解析要调用的服务名、方法名,序列化请求参数;
  2. 封装 RPC 请求头(RpcHeader),拼接完整的请求数据(解决粘包);
  3. 从 Zookeeper 获取目标服务的 IP:Port(服务发现);
  4. 建立 TCP 连接,发送 RPC 请求;
  5. 接收服务端响应,反序列化得到结果;
  6. 处理所有异常场景,通过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结尾,彻底解决截断问题。

总结

  1. 请求封装:按「4 字节 headerSize + RpcHeader + 参数」封装数据,解决 TCP 粘包 / 拆包;

  2. 服务发现:从 ZK 动态获取服务地址,替代硬编码,实现服务解耦;

  3. 网络通信:RAII 封装套接字避免资源泄漏,连接重试提升可靠性;

  4. 响应解析:ParseFromArray 按字节长度解析,避免数据截断;

  5. 易用性:开发者只需调用 stub. 方法名 (),底层自动完成所有远程调用逻辑,符合「本地调用」的体验。

相关推荐
alonewolf_992 小时前
RabbitMQ快速上手与核心概念详解
分布式·消息队列·rabbitmq
头发还没掉光光2 小时前
Linux网络之TCP协议
linux·运维·开发语言·网络·网络协议·tcp/ip
陌路202 小时前
RPC分布式通信(4)--Zookeeper
分布式·zookeeper·rpc
海棠AI实验室2 小时前
第 4 篇:为什么选择 Cloudflare Tunnel——在“无公网 IP + 零端口暴露”前提下,把家里 Mac 变成可交付的线上服务
tcp/ip·macos·智能路由器
廋到被风吹走3 小时前
【分布式缓存】分布式缓存架构全解析:从 Redis Cluster 到多级缓存策略
分布式·缓存·架构
wheeldown3 小时前
【Linux网络基础】Linux 网络基础与 TCP 协议
linux·网络·tcp/ip
上海云盾安全满满15 小时前
高防IP线路质量重要吗
网络·网络协议·tcp/ip
敏叔V58720 小时前
联邦学习与大模型:隐私保护下的分布式模型训练与微调方案
分布式
tobias.b20 小时前
408真题解析-2009-39-网络-TCP拥塞控制
网络·网络协议·tcp/ip·计算机考研·408考研·408真题解析