Mprpc服务框架的实现

MPRPC

该项目是在 Linux 环境下基于 muduo、Protobuf 和 Zookeeper 实现的一个轻量级 RPC 框架。可以把单体架构系统的本地方法调用,重构成基于 TCP 网络通信的 RPC 远程方法调用,实现统一台机器不同进程或者不同机器之间的服务调用。

每个类都是先附带说明,最后给出这个类的完整代码.
整体代码参考:mprpc

思考

集群和分布式

单机server的缺陷:

  • 受限于硬件资源,server能承受的并发量有限
  • 任意模块的修改都会导致整个项目代码重新编译部署
  • 系统中,有些模块是属于CPU密集型或者IO密集型的,各模块对于各硬件资源的需求是不一样的

集群:

每一台服务器独立运行一个工程的所有模块。(多台服务器运行多个server)

  • 优点:用户的并发量提升,一台服务器出问题不影响其余的服务器

  • 缺点:任意模块的修改都会导致整个项目代码重新编译部署,而且需要多次部署

分布式:

一个工程拆分了很多模块,每一个模块独立部署运行在一个服务器主机上,所有服务器协同工作共同提供服务,每一台服务器称作分布式的一个节点,根据节点的并发要求,对一个节点可以再做节点模块集群部署。

有的模块需要高并发,在分布式节点中继续进行集群部署;有的模块不需要只需要一台及其即可。

这样就解决了单机server的缺点。

分布式所带来的问题:

  • 大系统的软件模块如何划分?各模块可能会实现大量重复的代码
  • 各模块运行在不同的进程中,各模块之间如何访问?机器1上的模块进程1如何调用机器2上的模块进程2的方法呢?-- 涉及网络IO? -- 这就是我们这个项目的作用。

RPC通信原理

RPC ( Remote Procedure Call Protocol )远程过程调用协议,下图就是RPC主要做的事情:(callee是服务提供者,caller为服务消费者)

蓝色框中的就是我们的分别不是网络通信框架负责处理的范围。
黄色部分:设计 rpc 方法参数的打包和解析,也就是数据的序列化和反序列化,使用 Protobuf。
Protobuf使用二进制存储,xml和json是文本存储,Protobuf更省空间
Protobuf不需要存储额外的信息,json是kv存储
绿色部分:网络部分,包括寻找 rpc 服务主机,发起 rpc 调用请求和响应 rpc 调用结果,使用 muduo网络库和 zookeeper 服务配置中心(专门做服务发现)。

代码框架

下图是我们项目的代码框架。

lib存放动态库,example存放一些简单示例,out存放可执行文件,src存放生成项目动态库的主要源码,test存放一些测试代码。

auto_build.sh是用来一键辅助安装的脚本
auto_clean.sh一键卸载
gen-proto.sh用来辅助protobuf的生成

CMakeLists.text是项目的总构建文件,同时在src,example,test下均存有子构建文件

peRzzad.png

  • 根目录构建文件

主要是确定项目的输入输出路径,确定语言标准,寻找需要的依赖库

cmake_minimum_required(VERSION 3.10.0)
project(mprpcs VERSION 0.1.0 LANGUAGES C CXX)


# 配置
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED on)
set(CMAKE_BUILD_TYPE Release)

set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/lib)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/out)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_SOURCE_DIR}/out)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_SOURCE_DIR}/out)

set(CMAKE_POSITION_INDEPENDENT_CODE ON)  # -fPIC,动态库必需
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

add_compile_options($<$<CONFIG:Debug>:-g> $<$<CONFIG:Release>:-O2>)


# 依赖
find_package(muduo REQUIRED)
find_package(PkgConfig REQUIRED)
pkg_check_modules(PROTOBUF REQUIRED IMPORTED_TARGET protobuf)


add_subdirectory(src)
add_subdirectory(example)
add_subdirectory(test)
  • src构建文件

目的很简单,就是将src下的所有文件打包成一个动态库libmprpc,同时PUBLIC连接所需要的所有依赖库。

后面的一些操作是为了项目完毕的时候进行库的安装(也就是头文件移动到/usr/local/include/mprpc,库文件移动到/usr/local/lib/mprpc)。

同时为了我们的库安装之后,使用CMake的时候可以直接find_package导入本库,导出了目标到/usr/locallib/cmake/mprpc。

# 动态库
file(GLOB_RECURSE SRC_LIST "*.cpp" "*.cc")
add_definitions(-DTHREADED)
add_library(mprpc SHARED ${SRC_LIST} )
target_include_directories(mprpc PRIVATE /usr/local/include/zookeeper)

target_link_libraries(
    mprpc PRIVATE
    -Wl,--whole-archive
    /usr/local/lib/libzookeeper_mt.a
    -Wl,--no-whole-archive
    sasl2
)

target_link_libraries(mprpc PUBLIC 
    pthread 
    PkgConfig::PROTOBUF
    muduo
    grpc
    grpc++
    gpr
)
target_include_directories(mprpc PUBLIC
        $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
        $<INSTALL_INTERFACE:include/${PROJECT_NAME}>
)

install(DIRECTORY include/
    DESTINATION include/${PROJECT_NAME}
    FILES_MATCHING PATTERN "*.h *.hpp"
)

install(TARGETS mprpc 
    EXPORT mprpcTargets
    LIBRARY DESTINATION lib
)

install(EXPORT mprpcTargets
    FILE mprpcTargets.cmake
    DESTINATION lib/cmake/mprpc
)

  • example构建文件

这里十分简单,就是将本目录下的文件能进行编译编译生成一个可执行文件放在。

同时注意到我们连接了mprpc库,而我们的mprpc也链接了其他所有的依赖库,这样我们就只需要导入mprpc,自动传递运行时依赖。

include_directories(${CMAKE_CURRENT_SOURCE_DIR})
link_libraries(mprpc)
add_subdirectory(callee)
add_subdirectory(caller)

下面分别有服务端和客户端的测试用例代码,cmake分别是:

# callee

# bin
file(GLOB_RECURSE SRC_LIST "*.cpp" "*.cc")

add_compile_options(-g)

add_executable(callee ${SRC_LIST} ../user.pb.cc)
target_link_libraries(callee PRIVATE mprpc)
# caller

file(GLOB_RECURSE SRC_LIST "*.cpp" "*.cc")

add_compile_options(-g)

add_executable(caller ${SRC_LIST} ../user.pb.cc)
target_link_libraries(caller PRIVATE )
  • test构建文件
file(GLOB_RECURSE SRC_LIST "*.cpp" "*.cc")
add_executable(test_bin main.cpp ${SRC_LIST})
target_link_libraries(test_bin PRIVATE mprpc)
  • auto_build.sh

这个脚本的行为就是先检查项目下的构建情况,如果以前有就删除掉从新构建,然后执行cmake --install,最终会递归地执行到子目录src下的install操作。

#!/bin/bash

set -e

BUILD_ONLY=false
INSTALL_ONLY=false

case "${1:-}" in 
    --build)
        BUILD_ONLY=true
        ;;
    --install)
        INSTALL_ONLY=true
        ;;
    "")
        ;;
    *)
        echo "用法: $0 [--build|--install]"
        echo "  --build     仅构建编译"
        echo "  --install   仅安装"
        echo "  (无参数)    构建 + 安装"
        exit 1
        ;;
esac

PROJECT_DIR="$(pwd)"
BUILD_DIR="$PROJECT_DIR/build"


case "$BUILD_DIR" in
    "/"|"/*"|"/bin"|"/bin/"*|"/etc"|"/etc/"*|"/usr"|"/usr/"*|"/home"|"~")
    echo "✴ 拒绝删除系统目录:$BUILD_DIR"
    exit 1
    ;;
esac

# ================构建====================
if [ "$INSTALL_ONLY" = false ]; then

    if [ -d "$BUILD_DIR" ]; then
        echo "✴ 删除旧的构建目录:$BUILD_DIR"
        rm -rf "$BUILD_DIR"
    fi



    echo "✴ 配置CMake项目..."
    cmake -B "$(pwd)/build"
    echo "✴ 编译项目..."
    cmake --build "$(pwd)/build" -j16
    echo "✴ 构建完成"
fi

# ========== 安装 ==========
if [ "$BUILD_ONLY" = false ]; then
    echo "✴ 安装..."
    sudo cmake --install build
    echo "✴ 安装完毕"
    sudo ldconfig
fi
  • auto_clean.sh

这个脚本则是查找构建的时候build/install_manifest.txt下记录的安装文件,然后一一删除,清除安装文件。

#!/bin/bash

set -e


while read file; do
    sudo rm -rf "$file"
    echo "-- Removing: $file" 
done < build/install_manifest.txt

sudo rmdir "/usr/local/include/mprpc"
sudo rmdir "/usr/local/lib/cmake/mprpc"
echo "rmdir /usr/local/include/mprpc"
echo "rmdir /usr/local/lib/cmake/mprpc"
  • gen_proto.sh

这个脚本是通过扫描本地的proto文件进行grpc文件的生成.但是需要我们放在有proto文件的地方进行使用。

#!/bin/bash

set -euo pipefail

OUT_DIR=.

protoc --cpp_out="$OUT_DIR" \
    ./*.proto
echo "protoc完毕"

实现

本地服务如何发布成为rpc服务

现在先是演示可能的框架的使用实例,我们现在就是小白,思考可能的使用方式。

首先编写proto文件给出变量和服务

syntax = "proto3";

package fixbug;
option cc_generic_services = true;
message ResultCode {
    int32 errcode = 1;
    bytes errmsg = 2;
}

message LoginRequest {
    bytes name = 1;
    bytes pwd = 2;
}

message LoginResponse {
    ResultCode result = 1;
    bool success = 2;
}

/* 用户远程调用 */
service UserServiceRpc {
    /* 用户调用 */
    rpc Login(LoginRequest) returns (LoginResponse);
}

然后在当前目录调用./gen_grpc.sh生成相应的pb.h,pb.cc,grpc.pb.cc,grpc.pb.h.

我们新建一个UserService.cc文件,内容如下:

#include "../user.pb.h"
#include <absl/base/call_once.h>
#include <google/protobuf/service.h>
#include <grpcpp/support/status.h>

class UserService final : public fixbug::UserServiceRpc {
public:
    UserService() { }

    bool Login(std::string name, std::string pwd) {
        std::cout << "name:" << name << "\t" << "passwd:" << pwd << "\n";
        return true;
    }

    /**
     * @brief
     * 框架直接调用,当请求发来,框架直接调用相应的::fixbug::UserServiceRpc::Service::Login,然后我们可以在其中调用我们的业务Login
     *
     * @param context
     * @param request
     * @param response
     * @return ::grpc::Status
     */
    void Login(::google::protobuf::RpcController *controller, ::fixbug::LoginRequest const *request,
        ::fixbug::LoginResponse *response, ::google::protobuf::Closure *done) override {
        std::string name = request->name();
        std::string passwd = request->pwd();
        bool ok = this->Login(name, passwd);
        if (ok) {
            response->set_success(true);
            response->mutable_result()->set_errcode(0);
            response->mutable_result()->set_errmsg("");
        } else {
            response->set_success(false);
            response->mutable_result()->set_errcode(1);
            response->mutable_result()->set_errmsg("Unknown params");
        }

        done->Run();
    }
};

现在有了方法,有了框架怎么调用呢?

// example/main.cpp
#include "callee/UserService.cc"
#include "MprpcApplication.h"
#include "RpcProvider.h"
#include <iostream>

int main(int argc, char **argv) {
    MprpcApplication::Init(argc, argv);
    RpcProvider provider;
    provider.NotifyService(new UserService{});
    provider.Run();
}

那我们使用的时候应该是类似上面的,先是初始化框架,然后向其中发布/公开服务,然后将服务跑起来。

这样当客户端(服务的请求者caller)向我(服务的提供者callee)发送rpc请求的时候,我的框架应该能够识别对方的请求,比如是UserServer::Login这个服务,那么我的框架就会自动调用UserServer::Login执行完毕再将结果发给客户端。

peRIVQ1.png

MprpcApplication

这个类主要是设置初始化等相关操作,同时作为提供用户服务的总类,也就是这个类我们一般包含了所用的库的所有头文件,用户只需要包含此一即可。

根据程序的输入参数选择配置文件,初始化服务的ip和port 以及zookeeper的ip和port.

#pragma once

/**
 * @brief 框架基础类
 *
 */
#include "Config.h"
#include "MprpcApplication.h"
#include "MprpcChannel.h"
#include "MprpcController.h"
#include "MprpcProvider.h"
#include "muduo/noncopyable.h"

class Config;

class MprpcApplication : noncopyable {
public:
    static void Init(int argc, char **argv);
    static MprpcApplication &GetInstance();

    Config &GetConfig() {
        return _config;
    }

private:
    MprpcApplication();
    static Config &_config;
};


// .cc
#include "include/MprpcApplication.h"
#include "include/Config.h"
#include <cstdlib>
#include <iostream>
#include <string>
#include <unistd.h>

namespace {

void ShowArgsHelp() {
    std::cout << "format: command -i <configfile>\n";
}

} // namespace

void MprpcApplication::Init(int argc, char **argv) {
    if (argc < 2) {
        ShowArgsHelp();
        exit(EXIT_FAILURE);
    }
    std::string config_file;
    int c = 0;
    while ((c = getopt(argc, argv, "i:")) != -1) {
        switch (c) {
        case 'i': {
            config_file = optarg;
            break;
        }
        case '?': ShowArgsHelp(); exit(EXIT_FAILURE);
        default:  ShowArgsHelp(); exit(EXIT_FAILURE);
        }
    }
    _config.LoadConfigFile(config_file.c_str());
    // for (auto [k, v]: _config.GetMaps()) {
    //     std::cout << k << "=" << v << "\n";
    // }
}

Config &MprpcApplication::_config = Config::GetInstance();

MprpcApplication &MprpcApplication::GetInstance() {
    static MprpcApplication instance;
    return instance;
}

MprpcApplication::MprpcApplication() { }

Config配置类

配置类很简单,只需要读取从MprpcApplication传入文件路径,然后读取配置即可。读取的时候要处理完整,比如前后空格,等号左右空格等都要处理好.

配置格式如下

rpcserverip=127.0.0.1
rpcserverport=8888
zookeeperip=127.0.0.1
zookeeperport=2181
// .h
#pragma once

#include "muduo/noncopyable.h"
#include <string>
#include <unordered_map>

/**
 * @brief 配置文件类

    RpcServerIp RpcServerPort ZookeeperIp ZookeeperPort
 *
 */
class Config : noncopyable {
public:
    static Config &GetInstance();
    void LoadConfigFile(char const *file);
    std::string Get(std::string const &key);
    std::unordered_map<std::string, std::string> &GetMaps();

private:
    Config() { };
    std::unordered_map<std::string, std::string> _config;
};

// .cc
#include "include/Config.h"
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <string>

namespace {

std::string trim(std::string const &str) {
    auto start = str.find_first_not_of(" \t");
    if (start == std::string::npos) {
        return "";
    }
    auto end = str.find_last_not_of(" \t");
    return str.substr(start, end - start + 1);
}

std::pair<std::string, std::string> parse(std::string const &line) {
    auto idx = line.find('=');
    if (idx == -1) {
        return {};
    }
    std::string key;
    std::string value;
    key = trim(line.substr(0, idx));
    value = trim(line.substr(idx + 1));
    return {key, value};
}

} // namespace

Config &Config::GetInstance() {
    static Config instance;
    return instance;
}

void Config::LoadConfigFile(char const *file) {
    std::ifstream ifs(file);
    if (!ifs.is_open()) {
        std::cout << file << "open failed" << std::endl;
        exit(EXIT_FAILURE);
    }

    std::string line;
    while (std::getline(ifs, line)) {
        line = trim(line);

        if (line.empty() || line[0] == '#') {
            continue;
        }
        auto kv = parse(line);
        _config.insert(kv);
    }
}

std::string Config::Get(std::string const &key) {
    if (auto it = _config.find(key); it != _config.end()) {
        return it->second;
    }
    return {};
}

std::unordered_map<std::string, std::string> &Config::GetMaps() {
    return _config;
}

Loggers日志类

这个日志类主要是配合异步队列进行日志持久化存储的类。

启动一个线程,从队列中不断去除消息节点,然后对每一条消息进行文件写入存储。

  • 消息格式: 02:11:44 [INFO] 这是一条消息

  • 线程循环: 查看当前的日期是否和上次日期一直,是则直接写入消息
    否则关闭当前文件,重新创建当天的新文件。然后继续读取新消息写入

  • 文件的路径,默认是/tmp/mprpc,当然也可以选择路径,然后在这个路径下
    创建年-月目录,在年-月目录下,是年-月-日.log的文件

    例如:
    ├── mprpc
    │   └── 2026-04
    │       └── 2026-04-26.log
    
#include "include/Loggers.h"
#include <chrono>
#include <csignal>
#include <ctime>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <Logger.h>
#include <sstream>
#include <string>
#include <thread>

static std::atomic<Loggers *> g_logger_instance{nullptr};

namespace {

std::string getLogFilePath(std::string const &path) {
    auto now = std::chrono::system_clock::now();
    auto time_t_now = std::chrono::system_clock::to_time_t(now);
    std::tm tm = *std::localtime(&time_t_now);

    std::ostringstream oss;
    oss << std::put_time(&tm, "/%Y-%m/%Y-%m-%d"); // 按月和天分目录
    return path + oss.str() + ".log";
}

} // namespace

Loggers &Loggers::GetInstance() {
    static Loggers instance;
    g_logger_instance.store(&instance); // 保存指针供信号处理
    return instance;
}

void Loggers::signalHandler(int sig) {
    if (sig == SIGINT || sig == SIGTERM) {
        std::cout << "\nReceived signal " << sig << ", shutting down gracefully..." << std::endl;
        if (auto *instance = g_logger_instance.load()) {
            instance->shutdown(); // 请求关闭
        }
        // 设置一个短暂的超时等待日志写入
        std::this_thread::sleep_for(std::chrono::milliseconds(500));
        exit(sig); // 退出
    }
}

void Loggers::shutdown() {
    _shutdown_requested = true;
    _running = false;
    _queue.break_wait();

    // 等待后台线程处理完所有消息(最多等待3秒)
    if (_worker.joinable()) {
        auto start = std::chrono::steady_clock::now();
        while (
            !_queue.empty() && std::chrono::steady_clock::now() - start < std::chrono::seconds(3)) {
            std::this_thread::sleep_for(std::chrono::milliseconds(10));
        }
        _worker.join();
    }
}

/**
 * @brief 指定log目录,默认在/tmp/mprpc下
 *
 * @param path
 */
void Loggers::set_path(std::string const &path) {
    _path = path;
}

std::string Loggers::get_path() {
    return _path;
}

void Loggers::SetLogLevel(LogLevel level) {
    _log_level = level;
}

/* 将日志信息写入LogQueue */
void Loggers::log(LogLevel level, std::string msg) {
    _queue.push({level, msg, std::chrono::system_clock::now()});
}

Loggers::Loggers() : _running(true), _path("/tmp/mprpc") {
    std::signal(SIGINT, signalHandler);
    std::signal(SIGTERM, signalHandler);

    _worker = std::thread([this]() {
        std::string current_file;
        std::ofstream ofs;
        std::string last_date;

        while (_running) {
            auto now = std::chrono::system_clock::now();
            auto time_t_now = std::chrono::system_clock::to_time_t(now);
            std::tm tm = *std::localtime(&time_t_now);

            std::ostringstream oss;
            oss << std::put_time(&tm, "%Y-%m-%d");
            std::string today = oss.str();

            if (today != last_date) {
                std::string new_file = getLogFilePath(_path);

                std::error_code ec;
                std::filesystem::create_directories(
                    std::filesystem::path(new_file).parent_path(), ec);
                if (ec) {
                    std::cerr << "Can't create directory: " << ec.message() << std::endl;
                    continue;
                }

                if (ofs.is_open()) {
                    ofs.close();
                }
                ofs.open(new_file, std::ios::app);
                if (!ofs) {
                    std::cerr << "无法打开日志文件:" << new_file << std::endl;
                    continue;
                }

                current_file = new_file;
                last_date = today;
            }

            LogMessage logMsg;
            if (_queue.pop_with_timeout(logMsg, std::chrono::milliseconds(100))) {
                auto msg_time = std::chrono::system_clock::to_time_t(logMsg.timestamp);
                std::tm msg_tm = *std::localtime(&msg_time);
                std::ostringstream time_oss;
                time_oss << std::put_time(&msg_tm, "%H:%M:%S");
                std::string str
                    = time_oss.str() + " [" + log_level_name(logMsg.level) + "] " + logMsg.msg;
                if (ofs.is_open()) {
                    ofs << str << "\n";
                } else {
                    std::cerr << str << std::endl;
                }
            }
        }

        LogMessage log_msg;
        while (_queue.try_pop(log_msg)) {
            if (!ofs.is_open() && current_file.empty()) {
                std::cerr << log_msg.msg << std::endl;
            } else {
                auto msg_time = std::chrono::system_clock::to_time_t(log_msg.timestamp);
                std::tm msg_tm = *std::localtime(&msg_time);
                std::ostringstream time_oss;
                time_oss << std::put_time(&msg_tm, "%H:%M:%S");
                std::string str
                    = time_oss.str() + " [" + log_level_name(log_msg.level) + "] " + log_msg.msg;

                ofs << str << "\n";
            }
        }
        if (ofs.is_open()) {
            ofs << std::flush;
        }
    });
}

Loggers::~Loggers() {
    shutdown();
}

LogQueue异步队列

// .h

#pragma once

#include <chrono>
#include <condition_variable>
#include <mutex>
#include <queue>

/**
 * @brief 异步写日志队列
 *
 */

template <typename T>
class LogQueue {
public:
    void push(T data) {
        std::lock_guard<std::mutex> lock(_mtx);
        _queue.push(std::move(data));
    }

    T pop() {
        std::unique_lock<std::mutex> lock(_mtx);
        while (_queue.empty()) {
            _cv.wait(lock);
        }
        T data = std::move(_queue.front());
        _queue.pop();
        return data;
    }

    bool empty() {
        std::lock_guard<std::mutex> lock(_mtx);
        return _queue.empty();
    }

    bool pop_with_timeout(T &value, std::chrono::milliseconds timeout) {
        std::unique_lock<std::mutex> lock(_mtx);

        if (!_cv.wait_for(lock, timeout, [this]() { return !_queue.empty() || _broken; })) {
            return false;
        }

        if (_broken && _queue.empty()) {
            return false;
        }
        value = std::move(_queue.front());
        _queue.pop();
        return true;
    }

    bool try_pop(T &value) {
        std::lock_guard<std::mutex> lock(_mtx);
        if (_queue.empty()) {
            return false;
        }
        value = std::move(_queue.front());
        _queue.pop();
        return true;
    }

    void break_wait() {
        _broken = true;
        _cv.notify_all();
    }

private:
    std::queue<T> _queue;
    std::mutex _mtx;
    std::condition_variable _cv;
    std::atomic<bool> _broken{false};
};

MprpcChannel通道类(客户端方向)

MprpcChannel 的作用是 RPC 客户端的网络传输通道,负责将 Protobuf 的调用语义转换为网络字节流发送给服务端,并接收响应。

┌─────────────────────────────────────────────────┐
│                   RPC Client                     │
│                                                  │
│  用户代码                                        │
│  stub->Login(controller, &req, &res, done)      │
│         │                                        │
│         ▼                                        │
│  ┌──────────────────────┐                       │
│  │    UserServiceRpc    │  ← protoc 生成的Stub  │
│  │      _Stub           │                       │
│  │                      │                       │
│  │ 序列化参数,调用      │                       │
│  │ channel->CallMethod()│                       │
│  └──────────┬───────────┘                       │
│             │                                    │
│             ▼                                    │
│  ┌──────────────────────┐                       │
│  │   MprpcChannel       │  ← 你写的这个类       │
│  │                      │                       │
│  │ • 服务发现(ZK)       │                       │
│  │ • 建立TCP连接         │                       │
│  │ • 发送请求            │                       │
│  │ • 接收响应            │                       │
│  │ • 反序列化            │                       │
│  └──────────────────────┘                       │
└─────────────────────────────────────────────────┘

说白了就是客户端的网络模块,用于发送请求的通道.

发送的时候,我们自定义格式:

// 协议格式: [header_size(4字节)] [RpcHeader序列化] [请求参数序列化]
┌──────────┬─────────────────┬──────────────┐
│ 4 bytes  │   RpcHeader     │  Args        │
│头长度     │  service_name   │  LoginRequest│
│          │  method_name    │  序列化数据   │
│          │  args_size      │              │
└──────────┴─────────────────┴──────────────┘

其中RpcHeader通过protobuf进行序列化

syntax = "proto3";

package mprpc;

option cc_generic_services = true;
message RpcHeader {
    bytes service_name = 1;
    bytes method_name = 2;
    uint32 args_size = 3;
}

对应代码:

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;
/* 将protobuf结构RpcHeader序列化成字符串 */
if (rpcHeader.SerializeToString(&rpc_header_str)) {
    header_size = rpc_header_str.size();
} else {
    log_error("SerializeToString failed");
    controller->SetFailed("SerializeToString failed");
    return;
}
/* 
	然后send_rpc_str按照协议装填内容
	| 长度 | RpcHeader | 参数 |
*/
std::string send_rpc_str;
send_rpc_str.insert(0, (char *)&header_size, 4);  // 头部长度
send_rpc_str += rpc_header_str;                    // RpcHeader
send_rpc_str += args_str;                          // 请求参数

然后创建socket建立Tcp链接收发数据.

// 创建 socket
int clientfd = socket(AF_INET, SOCK_STREAM, 0);

// 延迟关闭(RAII)
Defer defer([clientfd]() { close(clientfd); });

// 连接服务端
connect(clientfd, (sockaddr *)&addr, sock_len);

// 发送请求
send(clientfd, send_rpc_str.c_str(), send_rpc_str.size(), 0);

// 接收响应
ssize_t len = recv(clientfd, buf, sizeof buf, 0);

我们看到其中的IP和PORT是直接硬编码写入的,非常的不灵活,之后我们使用zookeeper进行服务的查找.

其中也可以看到controller的使用,简单而言就是一个可以提前返回,通知错误信息,管理生命周期的变量,类似其他框架的context.

这些可以暂时忽略.

// .h
#pragma once

#include <google/protobuf/service.h>

using namespace ::google::protobuf;

class MprpcChannel : public ::google::protobuf::RpcChannel {
public:
    void CallMethod(MethodDescriptor const *method, RpcController *controller,
        Message const *request, Message *response, Closure *done) override;
};


// .cc

#include "include/MprpcChannel.h"
#include "Defer.h"
#include "rpcheader.pb.h"
#include "ZKClient.h"
#include <arpa/inet.h>
#include <Buffer.h>
#include <google/protobuf/descriptor.h>
#include <google/protobuf/message.h>
#include <google/protobuf/service.h>
#include <google/protobuf/stubs/port.h>
#include <Logger.h>
#include <netinet/in.h>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>

/**
 * @brief rpc调用者向服务端发送请求
    header_size + service_name method_name args_size + args
 *  4                   len                          args_size
 * @param method
 * @param controller
 * @param request
 * @param response
 * @param done
 */
void MprpcChannel::CallMethod(MethodDescriptor const *method, RpcController *controller,
    Message const *request, Message *response, Closure *done) {
    ::google::protobuf::ServiceDescriptor const *descriptor = method->service();
    std::string service_name = descriptor->name().data();
    std::string method_name = method->name().data();
    std::string args_str;
    // std::cout << service_name << "\t" << method_name << "\t" << args_str << std::endl;
    int args_size = 0;
    if (request->SerializeToString(&args_str)) {
        args_size = args_str.size();
    } else {
        log_error("SerializeToString failed");
        controller->SetFailed("SerializeToString failed");
        return;
    }
    // 定义rpc的请求header
    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 {
        log_error("SerializeToString failed");
        controller->SetFailed("SerializeToString failed");
        return;
    }
    std::string send_rpc_str;
    send_rpc_str.reserve(128);
    send_rpc_str.insert(0, (char *)&header_size, 4);
    send_rpc_str += rpc_header_str;
    send_rpc_str += args_str;
    int clientfd = socket(AF_INET, SOCK_STREAM, 0);
    if (clientfd == -1) {
        return;
    }
    /* 清理 */
    Defer defer([clientfd]() { close(clientfd); });
    
    std::string ip = "127.0.0.1";
    uint16_t port = 9999;
    /* 绑定地址 */
    sockaddr_in addr;
    addr.sin_port = htons(port);
    addr.sin_family = AF_INET;
    inet_pton(AF_INET, ip.c_str(), &addr.sin_addr);
    socklen_t sock_len = sizeof addr;
    if (connect(clientfd, (sockaddr *)&addr, sock_len) == -1) {
        log_error("Error connect");
        controller->SetFailed("Error connect");
        return;
    }
    send(clientfd, send_rpc_str.c_str(), send_rpc_str.size(), 0);
    // 接受响应值
    char buf[1024] = {0};
    ssize_t len = recv(clientfd, buf, sizeof buf, 0);

    if (len == -1 || len == 0) {
        log_error("recv res:{}", len);
        controller->SetFailed("Recv Error");
        return;
    }
    std::string response_str(buf, len);
    if (!response->ParseFromString(response_str)) {
        log_error("ParseFromString failed");
        controller->SetFailed("ParseFromString failed");
        return;
    }
}

MprpcProvider服务发布核心类(服务端方向)

1. 基本结构

  • ctx 结构体

封装一次 RPC 调用的上下文信息:

struct ctx {
    ::google::protobuf::Message *request;   // 请求消息
    ::google::protobuf::Message *response;  // 响应消息
    
    ctx(::google::protobuf::Message *req, ::google::protobuf::Message *res);
    ~ctx();  // 负责释放 request 和 response 的内存
};

这个结构的作用主要是为了一次通信回复之后,将request/response封装进入回调用于资源清理,防止内存泄漏.

  • ServiceInfo 结构
struct ServiceInfo {
    ::google::protobuf::Service *_service;  // Protobuf 服务对象指针
    std::unordered_map<std::string, 
        ::google::protobuf::MethodDescriptor const *> _methods;  // 方法名到方法描述符的映射
};
  • 成员变量
/* 映射表,目的是将发布的所有的服务和服务名称进行一个映射,方便我们根据服务名的到对应的服务 */
std::unordered_map<std::string, ServiceInfo> _services;
/* muduo时间循环 */
EventLoop *_loop;

2. 核心继承结构

protobuf的核心继承结构

类 职责 在 MprpcProvider 中的使用
Message 所有 Protobuf 消息的基类 作为 request/response 的泛型指针
Service RPC 服务抽象基类 存储用户注册的服务对象
ServiceDescriptor 描述一个服务的元信息 获取服务名、方法数量
MethodDescriptor 描述一个方法的元信息 执行 CallMethod 时的关键参数
Closure 回调接口 异步执行完毕后的回调
NewCallback 工厂函数,创建具体 Closure 绑定 SendRpcResponse 作为异步回调
┌─────────────────────────────────────────────────────────────┐
│                    ::google::protobuf                        │
└─────────────────────────────────────────────────────────────┘

                          ┌──────────────────┐
                          │     Message      │  (抽象基类)
                          │──────────────────│
                          │ + New()          │
                          │ + ParseFromString│
                          │ + SerializeToString│
                          │ + GetDescriptor()│
                          └────────┬─────────┘
                                   │ 继承
                    ┌──────────────┼──────────────┐
                    │              │              │
           ┌────────▼─────┐ ┌─────▼──────┐ ┌─────▼──────┐
           │   Request    │ │  Response  │ │  RpcHeader │
           │  (用户定义)  │ │ (用户定义) │ │ (框架定义) │
           └──────────────┘ └────────────┘ └────────────┘


                          ┌──────────────────┐
                          │    Service       │  (抽象基类)
                          │──────────────────│
                          │ + CallMethod()   │
                          │ + GetDescriptor()│
                          │ + GetRequestPrototype()│
                          │ + GetResponsePrototype()│
                          └────────┬─────────┘
                                   │ 继承
                          ┌────────▼─────────┐
                          │   Service子类    │
                          │   (用户定义)     │  ← 由 protoc 自动生成
                          │   通过NotifyService注册│
                          └──────────────────┘


    ┌─────────────────────────┐
    │   Descriptor (抽象基类) │
    └────────────┬────────────┘
                 │
    ┌────────────┼────────────────┐
    │            │                │
┌───▼─────┐ ┌───▼────────┐ ┌─────▼──────────┐
│Service  │ │ Method     │ │  Field         │
│Descriptor│ │Descriptor  │ │  Descriptor    │
│─────────│ │────────────│ │────────────────│
│+ name() │ │+ name()    │ │+ name()        │
│+ method │ │+ input_type│ │+ type()        │
│  _count()│ │+ output_   │ │+ number()      │
│+ method │ │  type()    │ │                │
│  (int)  │ │            │ │                │
└─────────┘ └────────────┘ └────────────────┘


┌───────────────────┐
│     Closure       │  (抽象基类/接口)
│───────────────────│
│ + Run()           │
└────────┬──────────┘
         │ 实现
┌────────▼──────────────────────────────┐
│ NewCallback<T, Args...>               │  ← NewCallback 工厂函数返回
│ (具体实现, 模板生成)                   │     的具体实现类
│───────────────────────────────────────│
│ 绑定 this 指针和成员函数               │
│ Run() → (obj->*method)(args...)       │
└───────────────────────────────────────┘


┌───────────────────┐
│  MethodDescriptor │  ← 方法描述符 (非继承关系,由ServiceDescriptor持有)
└───────────────────┘

关键的调用链路如下:

// 1. 通过 ServiceDescriptor 遍历方法
service->GetDescriptor()               // 获取 ServiceDescriptor*
    ->method(i)                        // 获取 MethodDescriptor*

// 2. 通过 MethodDescriptor 创建请求/响应
service->GetRequestPrototype(method_desc).New()   // 返回 Message* (Request 子类)
service->GetResponsePrototype(method_desc).New()  // 返回 Message* (Response 子类)

// 3. 异步执行方法
service->CallMethod(
    method_desc,        // MethodDescriptor*
    nullptr,            // RpcController (未使用)
    request,            // Message*
    response,           // Message*
    done                // Closure* (异步回调)
);

// 4. 回调触发
done->Run()  →  MprpcProvider::SendRpcResponse(conn, ctx)

3. NotifyService

功能:注册一个 Protobuf 服务

通过service基类指针拿到服务的描述类ServiceDescriptor,然后可以遍历服务的所有方法,构建方法名称->方法的映射.将服务信息和映射关系存入 _services 表

void MprpcProvider::NotifyService(::google::protobuf::Service *service) {
    /* 一个服务信息结构体 */
    ServiceInfo service_info;
    /* 拿到服务描述类,元信息 */
    ::google::protobuf::ServiceDescriptor const *descriptor = service->GetDescriptor();
    /* 拿到服务名称 */
    std::string service_name = descriptor->name().data();
    /* 服务的函数个数 */
    int method_count = descriptor->method_count();
    for (auto i = 0; i < method_count; ++i) {
        /* 获取一个函数descriptor */
        ::google::protobuf::MethodDescriptor const *method_descriptor = descriptor->method(i);
        /* 获取这个函数的名称 */
        std::string method_name = method_descriptor->name().data();
        /* 将<方法名称,函数descriptor>插入到映射表中 */
        service_info._methods.insert({method_name, method_descriptor});
    }
    service_info._service = service;
    /* 插入映射表 */
    _services.insert({std::move(service_name), service_info});
}

4. Run

显示从配置类拿到服务的配置,然后设置muduo网络库所需要的函数回调,我们这里设置了新链接回调(OnConnection),消息消息回调(OnMessage).

然后启动server,进行事件循环,等待客户端发来请求.

void MprpcProvider::Run() {
    std::string serverip = Config::GetInstance().Get("rpcserverip");
    std::string serverport = Config::GetInstance().Get("rpcserverport");
    InetAddress addr(std::stoi(serverport), serverip);
    _loop = new EventLoop;
    TcpServer server(_loop, addr, "RpcServer");

    log_info("Started in {}:{}", serverip, serverport);
    // std::cout << serverip << ":" << serverport << std::endl;

    server.setConnectionCallback(
        std::bind(&::MprpcProvider::OnConnection, this, std::placeholders::_1));
    server.setMessageCallback(std::bind(&::MprpcProvider::OnMessage, this, std::placeholders::_1,
        std::placeholders::_2, std::placeholders::_3));
    server.setThreadNum(4);
    
    log_info("RpcProvide Started");

    server.start();
    _loop->loop();
}

5. OnMessage

这个函数就是客户端发来信息,我们进行信息的反序列化,然后调用对用的服务方法,得到结果之后序列化发送给对端.

设计相应的协议,对方发来的字节流结构如下

|  len  |   proto内容 | args |
	4		   len    args_len

如何理解呢,就是对方发来的字节流,前4个字节是接下来的proto新的长度,我们先提取前四个字节拿到proto内容长度

uint32_t header_size;
recv_buf.copy((char *)&header_size, 4, 0);

然后提取header_size长度的字节内容

std::string rpc_header_str = recv_buf.substr(4, header_size);

拿到了这个字节内容如何反序列化,我们先看结构是啥样的:

首先是服务名称,然后是请求的方法名称,最后是参数的字节长度

syntax = "proto3";

package mprpc;
// 这个cc_generic_services选项不能缺少是生成rpc的关键
option cc_generic_services = true;
message RpcHeader {
    bytes service_name = 1;
    bytes method_name = 2;
    uint32 args_size = 3;
}

然后进行反序列化

mprpc::RpcHeader rpcHeader;
std::string service_name;
std::string method_name;
uint32_t args_size;
if (rpcHeader.ParseFromString(rpc_header_str)) {
    service_name = rpcHeader.service_name();
    method_name = rpcHeader.method_name();
    args_size = rpcHeader.args_size();
} else {
    log_error("反序列化失败:{}", rpc_header_str);
    return;
}

然后就可以拿到方法的参数了:

std::string args_str = recv_buf.substr(4 + header_size);

然后就可以根据服务名称从映射表拿到Service结构,然后拿到服务的基类指针,然后从中查找服务中的方法描述类.

 ServiceInfo &info = it->second;
    ::google::protobuf::Service *service = info._service; // 注意:protobuf::Service*

    auto mit = info._methods.find(method_name);
    if (mit == info._methods.end()) { /* ... */
        return;
    }

    ::google::protobuf::MethodDescriptor const *method_desc = mit->second;
    if (!method_desc) {
        log_error("bad method_desc");
    }

一切就绪,我们就可以创建request/response,response用于我们恢复对端请求的结果信息,request用于反序列化args_str拿到方法参数.

// 创建 request/response
    ::google::protobuf::Message *request = service->GetRequestPrototype(method_desc).New();
    ::google::protobuf::Message *response = service->GetResponsePrototype(method_desc).New();

if (!request->ParseFromString(args_str)) {
    log_debug("Parse args from request failed:{}", args_str);
    delete request;
    delete response;
    return;
}

然后我们就可以调用CallMethod方法执行对应的服务方法.这个done可以没有.如果传入nullptr就是同步调用,如果传done就是异步调用,那么需要给done传入回调函数.

由于request/response需要在使用后处理,按道理应该将其传入NewCallback中,在回调函数SendRpcResponse中清理,但是NewCallback最多只能三个参数,所以我们设计了ctx结构存放request和response用于一体清理.

SendRpcResponse是我们自己写的函数,目的是远端的实际业务代码完成之后需要将内容通过回调发送给对端,就是通过SendRpcResponse完成的.

ctx *c = new ctx(request, response);

    ::google::protobuf::Closure *done
        = ::google::protobuf::NewCallback<MprpcProvider, TcpConnectionPtr const &, ctx *>(
            this, &MprpcProvider::SendRpcResponse, conn, c);

    /*
        最后一个参数 done 是一个 Closure 回调。如果传 nullptr,表示同步调用——CallMethod
        会阻塞,直到方法执行完毕,然后你才能继续处理 response。
        但如果传了回调函数,就变成了异步调用:
            CallMethod 立即返回,不阻塞
            当 RPC 方法真正执行完毕后,框架会自动调用 done->Run()
            你的 SendRpcResponse 被执行,把 response 发回客户端
     */
    service->CallMethod(method_desc, nullptr, request, response, done);
void MprpcProvider::OnMessage(
    TcpConnectionPtr const &conn, Buffer *buffer, [[maybe_unused]] Timestamp) {
    std::string recv_buf = buffer->retrieveAllAsString();
    uint32_t header_size;
    recv_buf.copy((char *)&header_size, 4, 0);
    std::string rpc_header_str = recv_buf.substr(4, header_size);
    mprpc::RpcHeader rpcHeader;
    std::string service_name;
    std::string method_name;
    uint32_t args_size;
    if (rpcHeader.ParseFromString(rpc_header_str)) {
        service_name = rpcHeader.service_name();
        method_name = rpcHeader.method_name();
        args_size = rpcHeader.args_size();
    } else {
        log_error("反序列化失败:{}", rpc_header_str);
        return;
    }
    std::string args_str = recv_buf.substr(4 + header_size);
    log_debug("======================================");
    log_debug("RpcHeaderStr:{}", rpc_header_str);
    log_debug("ServiceName:{}", service_name);
    log_debug("MethodName:{}", method_name);
    log_debug("ArgsSize:{}", args_size);
    log_debug("ArgsStr:{}", args_str);
    log_debug("======================================");

    /* 获取Service和Method对象 */
    auto it = _services.find(service_name);
    if (it == _services.end()) { /* ... */
        return;
    }

    ServiceInfo &info = it->second;
    ::google::protobuf::Service *service = info._service; // 注意:protobuf::Service*

    auto mit = info._methods.find(method_name);
    if (mit == info._methods.end()) { /* ... */
        return;
    }

    ::google::protobuf::MethodDescriptor const *method_desc = mit->second;
    if (!method_desc) {
        log_error("bad method_desc");
    }

    // 创建 request/response
    ::google::protobuf::Message *request = service->GetRequestPrototype(method_desc).New();
    ::google::protobuf::Message *response = service->GetResponsePrototype(method_desc).New();

    if (!request->ParseFromString(args_str)) {
        log_debug("Parse args from request failed:{}", args_str);
        delete request;
        delete response;
        return;
    }

    ctx *c = new ctx(request, response);

    ::google::protobuf::Closure *done
        = ::google::protobuf::NewCallback<MprpcProvider, TcpConnectionPtr const &, ctx *>(
            this, &MprpcProvider::SendRpcResponse, conn, c);

    /*
        最后一个参数 done 是一个 Closure 回调。如果传 nullptr,表示同步调用——CallMethod
        会阻塞,直到方法执行完毕,然后你才能继续处理 response。
        但如果传了回调函数,就变成了异步调用:
            CallMethod 立即返回,不阻塞
            当 RPC 方法真正执行完毕后,框架会自动调用 done->Run()
            你的 SendRpcResponse 被执行,把 response 发回客户端
     */
    service->CallMethod(method_desc, nullptr, request, response, done);
}

6. SendRpcResponse

目的很简单,将response的结果序列化成字节,然后发送过去.然后``delete掉ctx结构体,析构函数会自动delete掉request和response`.

void MprpcProvider::SendRpcResponse(TcpConnectionPtr const &conn, ctx *c) {
    std::string reponse_str;
    if (!c->response->SerializeToString(&reponse_str)) {
        log_error("序列化失败");
        return;
    }

    conn->send(reponse_str);
    conn->shutdown();

    delete c;
}

简单测试

首先设计双方的服务结构,必须要双方都有这个结构,知道协议,才能沟通.

  • protobuf结构-user.proto
syntax = "proto3";

package fixbug;

option cc_generic_services = true;

message ResultCode {
    int32 errcode = 1;
    bytes errmsg = 2;
}

message LoginRequest {
    bytes name = 1;
    bytes pwd = 2;
}

message LoginResponse {
    ResultCode result = 1;
    bool success = 2;
}
// 用户服务
service UserServiceRpc {
    /* 用户调用 */
    rpc Login(LoginRequest) returns (LoginResponse);
}

使用gen-proto.sh脚本生成对应的user.pb.h,user.pb.cc

  • 服务端-callee

服务端实现一个继承了fixbug::UserServiceRpc类的业务类UserService(名字自己定).

然后首先实现我们的真实的业务的代码,比如注册方法Login,传入用户名和密码,判断是否存在...

同时还是实现一个虚函数void Login(::google::protobuf::RpcController *controller,::fixbug::LoginRequest const *request,::fixbug::LoginResponse *response, ::google::protobuf::Closure *done)
这个函数是脚本生成protobuf的cc代码的时候自动生成的虚函数,我们可以重写这个虚函数实现额外的逻辑.

void Login(::google::protobuf::RpcController *controller, ::fixbug::LoginRequest const *request,
           ::fixbug::LoginResponse *response, ::google::protobuf::Closure *done) override {
    std::string name = request->name();
    std::string passwd = request->pwd();
    bool ok = this->Login(name, passwd);
    if (ok) {
        response->set_success(true);
        response->mutable_result()->set_errcode(0);
        response->mutable_result()->set_errmsg("");
    } else {
        response->set_success(false);
        response->mutable_result()->set_errcode(1);
        response->mutable_result()->set_errmsg("Unknown params");
    }
    if (done) {
        done->Run();
    }
}

显示根据客户端的rpc请求的request拿到请求参数,然后调用实际的业务代码执行逻辑,拿到结果后将结果存入response,最后通过done发送给对端.这里的done->Run()执行的就是OnMessage函数中的NewCallback函数的回调:SendRpcResponse.

在main函数中,使用MprpcProvider注册服务UserService,然后Run开启rpc服务.

启动结果如下

❯ ./callee -i config.conf
2026-04-26 23:21:36.574849380 CST /home/vivek/Codes/Cpp/mprpc/example/callee/UserService.cc:114 [info] callee_start
2026-04-26 23:21:36.574904874 CST /home/vivek/Codes/Cpp/mprpc/example/callee/UserService.cc:116 [info] Init ok
UserService ptr in main: 0x55bc46d1b330
2026-04-26 23:21:36.575563138 CST /home/vivek/Codes/Cpp/mprpc/example/callee/UserService.cc:121 [info] 注册完成!
2026-04-26 23:21:36.575651657 CST /home/vivek/Codes/Cpp/mprpc/src/MprpcProvider.cc:51 [info] Started in 127.0.0.1:8888
2026-04-26 23:21:36.575662980 CST /home/vivek/Codes/Cpp/mprpc/src/ZKClient.cc:37 [info] Zookeeper Started in 127.0.0.1:2181
name:大狗熊	passwd:123456
doing local code
#include "../user.pb.h"
#include "Loggers.h"
#include "MprpcApplication.h"
#include "MprpcProvider.h"
#include <absl/base/call_once.h>
#include <Config.h>
#include <cstddef>
#include <cstdint>
#include <google/protobuf/service.h>
#include <grpcpp/support/status.h>
#include <Logger.h>
#include <string>

class UserService final : public fixbug::UserServiceRpc {
public:
    UserService() { }

    /*
    	实际业务代码
    */
    bool Login(std::string name, std::string pwd) {
        std::cout << "name:" << name << "\t" << "passwd:" << pwd << "\n";
        return true;
    }

    /**
     * @brief
     * 框架直接调用,当请求发来,框架直接调用相应的::fixbug::UserServiceRpc::Service::Login,然后我们可以在其中调用我们的业务Login
     *
     * @param context
     * @param request
     * @param response
     * @return ::grpc::Status
     */
    void Login(::google::protobuf::RpcController *controller, ::fixbug::LoginRequest const *request,
        ::fixbug::LoginResponse *response, ::google::protobuf::Closure *done) override {
        std::string name = request->name();
        std::string passwd = request->pwd();
        bool ok = this->Login(name, passwd);
        if (ok) {
            response->set_success(true);
            response->mutable_result()->set_errcode(0);
            response->mutable_result()->set_errmsg("");
        } else {
            response->set_success(false);
            response->mutable_result()->set_errcode(1);
            response->mutable_result()->set_errmsg("Unknown params");
        }
        if (done) {
            done->Run();
        }
    }
};

auto main(int argc, char **argv) -> int {
    // Loggers::GetInstance().set_path("/home/vivek/");
    LOG_INFO("/home");

    log_start();
    set_log_level(log_level::info);
    log_info("callee_start");
    MprpcApplication::Init(argc, argv);
    log_info("Init ok");
    MprpcProvider provider;
    auto *svc = new UserService{};
    printf("UserService ptr in main: %p\n", (void *)svc);
    provider.NotifyService(svc);
    log_info("注册完成!");
    provider.Run();
}
  • 客户端

客户端就是将对应rpc方法的参数存入request,调用UserServiceRpc_Stub的Login发送rpc请求,等待服务端回复.

UserServiceRpc_Stub就是protobuf帮我们生成的一个桩,也就是代理类,这个代理类需要一个Channel通道,代理类透过通道.与对方进行沟通交流.我们只负责输入请求参数,发起请求,然后等待结果.

启动结果如下:

/home/vivek/Codes/Cpp/mprpc/out/caller-i:/home/vivek/Codes/Cpp/mprpc/out/config.conf
2026-04-26 23:22:19.165707735 CST /home/vivek/Codes/Cpp/mprpc/example/caller/CallUserService.cc:12 [info] MprpcClient Started
2026-04-26 23:22:19.166588324 CST /home/vivek/Codes/Cpp/mprpc/src/ZKClient.cc:37 [info] Zookeeper Started in 127.0.0.1:2181
2026-04-26 23:22:19.170778641 CST /home/vivek/Codes/Cpp/mprpc/src/ZKClient.cc:50 [info] zookeeper_init_success!
// .cc
#include "CallGetFriends.h"
#include "CallLogin.h"
#include "CallRegister.h"
#include "MprpcApplication.h"
#include "user.pb.h"
#include <Logger.h>

inline void func_login(fixbug::UserServiceRpc_Stub *stub, MprpcController *controller) {
    fixbug::LoginRequest request;
    fixbug::LoginResponse response;
    request.set_name("大狗熊");
    request.set_pwd("123456");
    /* rpc请求 */
    stub->Login(controller, &request, &response, nullptr);
    /* rpc调用完成,调用结果 */
    if (!response.success()) {
        log_info("{}:{}", response.result().errmsg(), response.result().errcode());
    } else {
        log_debug("rpc调用成功!");
    }
}


auto main(int argc, char **argv) -> int {
    std::cout << argv[0] << argv[1] << ":" << argv[2] << std::endl;
    log_start();
    set_log_level(log_level::info);
    log_info("MprpcClient Started");
    MprpcApplication::Init(argc, argv);
    fixbug::UserServiceRpc_Stub stub(new MprpcChannel{});
    func_login(&stub, nullptr);
    return 0;
}

// .h

#pragma once

#include "google/protobuf/service.h"
#include <Buffer.h>
#include <Callbacks.h>
#include <EventLoop.h>
#include <google/protobuf/descriptor.h>
#include <google/protobuf/message.h>
#include <InetAddress.h>
#include <string>
#include <TcpConnection.h>
#include <Timestamp.h>
#include <unordered_map>
/**
 * @brief 服务发布类
 *
 */

class Service;

class MprpcProvider : noncopyable {
public:
    void NotifyService(::google::protobuf::Service *service);
    void Run();

private:
    struct ctx {
    public:
        ctx(::google::protobuf::Message *req, ::google::protobuf::Message *res)
            : request(req)
            , response(res) { }

        ~ctx() {
            delete request;
            delete response;
        }

        ::google::protobuf::Message *request;
        ::google::protobuf::Message *response;
    };

    /* 新连接回调 */ 
    void OnConnection(TcpConnectionPtr const &);
    /* 一次rpc请求来临,回调处理函数 */
    void OnMessage(TcpConnectionPtr const &, Buffer *, [[maybe_unused]] Timestamp);
    /* rpc请求处理完毕后,进行rpc的序列化rpc和网络发送,是用在OnMessage的回调函数 */
    void SendRpcResponse(TcpConnectionPtr const &, ctx *c);

private:
    /* 服务信息 */
    struct ServiceInfo {
        ::google::protobuf::Service *_service;
        std::unordered_map<std::string, ::google::protobuf::MethodDescriptor const *> _methods;
    };

    /* 映射表 */
    std::unordered_map<std::string, ServiceInfo> _services;
	/* muduo时间循环 */
    EventLoop *_loop;
};


// .cc

#include "include/MprpcProvider.h"
#include "include/Config.h"
#include "include/rpcheader.pb.h"
#include "ZKClient.h"
#include "zookeeper.h"
#include <Callbacks.h>
#include <EventLoop.h>
#include <google/protobuf/descriptor.h>
#include <google/protobuf/extension_set.h>
#include <google/protobuf/message.h>
#include <google/protobuf/stubs/callback.h>
#include <grpcpp/impl/service_type.h>
#include <grpcpp/server_context.h>
#include <grpcpp/support/async_unary_call.h>
#include <InetAddress.h>
#include <Logger.h>
#include <muduo/TcpServer.h>
#include <netinet/in.h>
#include <string>

#undef LOG_ENABLED
#define LOG_ENABLED true

void MprpcProvider::NotifyService(::google::protobuf::Service *service) {
    /* 一个服务信息结构体 */
    ServiceInfo service_info;
    ::google::protobuf::ServiceDescriptor const *descriptor = service->GetDescriptor();
    std::string service_name = descriptor->name().data();
    /* 服务的函数个数 */
    int method_count = descriptor->method_count();
    for (auto i = 0; i < method_count; ++i) {
        /* 获取一个函数descriptor */
        ::google::protobuf::MethodDescriptor const *method_descriptor = descriptor->method(i);
        /* 获取这个函数的名称 */
        std::string method_name = method_descriptor->name().data();
        /* 将<方法名称,函数descriptor>插入到映射表中 */
        service_info._methods.insert({method_name, method_descriptor});
    }
    service_info._service = service;
    /* 插入映射表 */
    _services.insert({std::move(service_name), service_info});
}

void MprpcProvider::Run() {
    std::string serverip = Config::GetInstance().Get("rpcserverip");
    std::string serverport = Config::GetInstance().Get("rpcserverport");
    InetAddress addr(std::stoi(serverport), serverip);
    _loop = new EventLoop;
    TcpServer server(_loop, addr, "RpcServer");

    log_info("Started in {}:{}", serverip, serverport);
    // std::cout << serverip << ":" << serverport << std::endl;

    server.setConnectionCallback(
        std::bind(&::MprpcProvider::OnConnection, this, std::placeholders::_1));
    server.setMessageCallback(std::bind(&::MprpcProvider::OnMessage, this, std::placeholders::_1,
        std::placeholders::_2, std::placeholders::_3));
    server.setThreadNum(4);

    ZKClient zkCli;
    zkCli.Start();

    for (auto &p: _services) {
        std::string service_path = "/" + p.first;
        zkCli.Create(service_path.c_str(), nullptr, 0);
        for (auto &mp: p.second._methods) {
            std::string method_path = service_path + "/" + mp.first;
            char method_path_data[128] = {0};
            sprintf(method_path_data, "%s:%d", serverip.data(), std::stoi(serverport));
            zkCli.Create(
                method_path.c_str(), method_path_data, strlen(method_path_data), ZOO_EPHEMERAL);
        }
    }

    log_info("RpcProvide Started");

    server.start();
    _loop->loop();
}

void MprpcProvider::OnConnection(TcpConnectionPtr const &conn) {
    if (!conn->connected()) {
        conn->shutdown();
    }
}

/**
 * @brief
     定义proto的message类型,进行序列化和反序列化
     len-RpcHeaderStr-ArgsStr
 *
 * @param conn
 * @param buffer
 */
void MprpcProvider::OnMessage(
    TcpConnectionPtr const &conn, Buffer *buffer, [[maybe_unused]] Timestamp) {
    std::string recv_buf = buffer->retrieveAllAsString();
    uint32_t header_size;
    recv_buf.copy((char *)&header_size, 4, 0);
    std::string rpc_header_str = recv_buf.substr(4, header_size);
    mprpc::RpcHeader rpcHeader;
    std::string service_name;
    std::string method_name;
    uint32_t args_size;
    if (rpcHeader.ParseFromString(rpc_header_str)) {
        service_name = rpcHeader.service_name();
        method_name = rpcHeader.method_name();
        args_size = rpcHeader.args_size();
    } else {
        log_error("反序列化失败:{}", rpc_header_str);
        return;
    }
    std::string args_str = recv_buf.substr(4 + header_size);
    log_debug("======================================");
    log_debug("RpcHeaderStr:{}", rpc_header_str);
    log_debug("ServiceName:{}", service_name);
    log_debug("MethodName:{}", method_name);
    log_debug("ArgsSize:{}", args_size);
    log_debug("ArgsStr:{}", args_str);
    log_debug("======================================");

    /* 获取Service和Method对象 */
    auto it = _services.find(service_name);
    if (it == _services.end()) { /* ... */
        return;
    }

    ServiceInfo &info = it->second;
    ::google::protobuf::Service *service = info._service; // 注意:protobuf::Service*

    auto mit = info._methods.find(method_name);
    if (mit == info._methods.end()) { /* ... */
        return;
    }

    ::google::protobuf::MethodDescriptor const *method_desc = mit->second;
    if (!method_desc) {
        log_error("bad method_desc");
    }

    // 创建 request/response
    ::google::protobuf::Message *request = service->GetRequestPrototype(method_desc).New();
    ::google::protobuf::Message *response = service->GetResponsePrototype(method_desc).New();

    if (!request->ParseFromString(args_str)) {
        log_debug("Parse args from request failed:{}", args_str);
        delete request;
        delete response;
        return;
    }

    ctx *c = new ctx(request, response);

    ::google::protobuf::Closure *done
        = ::google::protobuf::NewCallback<MprpcProvider, TcpConnectionPtr const &, ctx *>(
            this, &MprpcProvider::SendRpcResponse, conn, c);

    /*
        最后一个参数 done 是一个 Closure 回调。如果传 nullptr,表示同步调用——CallMethod
        会阻塞,直到方法执行完毕,然后你才能继续处理 response。
        但如果传了回调函数,就变成了异步调用:
            CallMethod 立即返回,不阻塞
            当 RPC 方法真正执行完毕后,框架会自动调用 done->Run()
            你的 SendRpcResponse 被执行,把 response 发回客户端
     */
    service->CallMethod(method_desc, nullptr, request, response, done);
}

void MprpcProvider::SendRpcResponse(TcpConnectionPtr const &conn, ctx *c) {
    std::string reponse_str;
    if (!c->response->SerializeToString(&reponse_str)) {
        log_error("序列化失败");
        return;
    }

    conn->send(reponse_str);
    conn->shutdown();

    delete c;
}

MprpcController控制类

目前客户端对于rpc请求结果的判定依赖于我们设定的response的内容.为了更加明确清晰的生命周期管理和消息传递,我们需要加入Controller.类似其他框架的context.

区别在于,response判断的是业务层面的成功与否,比如'密码错误','用户不存在'等.服务端是正常运行的,但是业务逻辑拒绝了你的请求.

而controller判断的是rpc层面的错误,比如网络问题,连接超时,服务未找到等.

我们实现一个MprpcController类继承自基类::google::protobuf::RpcController,实现其虚函数.

简单而言我们保存一个错误标志 _failed和错误信息_error_text

下面的虚函数顾名思义就是设置这些变量的.我们通过设置这些变量,可以帮助我们获取错误信息,提前返回等.

然后将controller加入到我们的代码中使用.在MprpcChannel中遇到序列化失败,参数错误等各种原因导致的提前返回returen,都可以使用controller进行设置错误信息之后再返回.

这样客户端在调用rpc请求之后,可以通过controller判断请求是否有错误,是否提前返回,错误信息是什么等.

例如,这里rpcHeader序列化失败,我们就可以使用controller存储信息.

// MprpcChannel
if (rpcHeader.SerializeToString(&rpc_header_str)) {
    header_size = rpc_header_str.size();
} else {
    log_error("SerializeToString failed");
    controller->SetFailed("SerializeToString failed");
    return;
}

这样客户端:

inline void func_register(fixbug::UserServiceRpc_Stub *stub, MprpcController *controller) {
    fixbug::RegisterRequest request2;
    fixbug::RegisterResponse response2;
    request2.set_id(222);
    request2.set_name("牛🐮");
    request2.set_pwd("🐮二");

    stub->Register(controller, &request2, &response2, nullptr);
    if (controller->Failed()) {
        std::cout << controller->ErrorText() << std::endl;
        return;
    }
    // log_info("{}", response2.success());
}

可以通过controller有没有出错,不需要等到这个rpc请求结果返回查看response的参数

// .h
#pragma once

#include <google/protobuf/service.h>

class MprpcController : public ::google::protobuf::RpcController {
public:
    MprpcController();

    virtual void Reset() override;
    virtual bool Failed() const override;
    virtual std::string ErrorText() const override;
    virtual void StartCancel() override;
    virtual void SetFailed(std::string const &reason) override;
    virtual bool IsCanceled() const override;
    virtual void NotifyOnCancel(::google::protobuf::Closure *callback) override;

private:
    bool _failed;
    std::string _errot_text;
};

// .cc

#include "include/MprpcController.h"

MprpcController::MprpcController() {
    _failed = false;
    _errot_text = "";
}

void MprpcController::Reset() {
    _failed = false;
    _errot_text = "";
}

bool MprpcController::Failed() const {
    return _failed;
}

std::string MprpcController::ErrorText() const {
    return _errot_text;
}

void MprpcController::StartCancel() { }

void MprpcController::SetFailed(std::string const &reason) {
    _failed = true;
    _errot_text = reason;
}

bool MprpcController::IsCanceled() const {
    return false;
}

void MprpcController::NotifyOnCancel(::google::protobuf::Closure *callback) { }

ZKClient-Zookeeper

基础不再赘述,参考另一篇Zookeeper指南

ZooKeeper 解决了分布式系统中最关键的服务发现问题,让 RPC 框架从"写死地址"变成"动态发现"。

服务端发布服务的时候,根据以下结构进行节点的创建:

/
└── UserServiceRpc/          (永久节点)
    ├── Login                (临时节点, data="192.168.1.100:8080")
    ├── Register             (临时节点, data="192.168.1.100:8080")
    └── GetFriends           (临时节点, data="192.168.1.100:8080")

首先创建一个永久性的服务节点,然后在这个节点下,根据rpc方法创建临时节点.

// 服务端启动时(MprpcProvider::Run)
for (auto &p: _services) {
    std::string service_path = "/" + p.first;  // /UserServiceRpc
    zkCli.Create(service_path.c_str(), nullptr, 0);
    
    for (auto &mp: p.second._methods) {
        std::string method_path = service_path + "/" + mp.first;
        // /UserServiceRpc/Login
        char method_path_data[128] = {0};
        sprintf(method_path_data, "%s:%d", serverip.data(), std::stoi(serverport));
        // data = "192.168.1.100:8080"
        zkCli.Create(method_path.c_str(), method_path_data, 
                    strlen(method_path_data), ZOO_EPHEMERAL);  // 临时节点!
    }
}

那么客户端使用的时候呢,就先根据服务名和方法名去zookeeper进行查询,拿到IP和PORT之后,再选择向服务端发送rpc请求.

// 客户端调用时
ZKClient zkCli;
zkCli.Start();
std::string method_path = "/" + service_name + "/" + method_name;
// 查 /UserServiceRpc/Login
std::string host_data = zkCli.GetData(method_path.c_str());
// 得到 "192.168.1.100:8080"

// 解析地址
int idx = host_data.find(":");
std::string ip = host_data.substr(0, idx);
uint16_t port = std::stoi(host_data.substr(idx + 1));

// 连接这个地址
connect(clientfd, (sockaddr *)&addr, sock_len);

为了方便实用我们简单封装了zookeeper的api.

// .
#pragma once

#include <semaphore.h>
#include <string>
#include <vector>
#include <zookeeper/zookeeper.h>

class ZKClient {
public:
    ZKClient();
    ~ZKClient();
    void Start();
    void Create(char const *path, char const *data, int datalen, int state = 0);
    std::string GetData(char const *path, Stat *stat = nullptr);
    void SetData(char const *path, char const *data, int datalen, int version = 1);
    void Delete(char const *path, int version = 1);
    bool Exists(char const *path);
    std::vector<std::string> GetChildren(char const *path);
    void Close();
    void Reconnect();

    // 获取原始句柄
    zhandle_t *GetHandle() const {
        return _zhandle;
    }

private:
    zhandle_t *_zhandle;
    int _timeout;
    std::string _host;
};


// .cc

#include "include/ZKClient.h"
#include "Logger.h"
#include "MprpcApplication.h"
#include <cstdlib>
#include <semaphore.h>
#include <string>
#include <zookeeper.h>

namespace {

void global_watcher(zhandle_t *zh, int type, int state, char const *path, void *watcherCtx) {
    if (type == ZOO_SESSION_EVENT) {
        if (state == ZOO_CONNECTED_STATE) {
            sem_t *sem = (sem_t *)zoo_get_context(zh);
            sem_post(sem);
        } else if (state == ZOO_EXPIRED_SESSION_STATE) {
            log_error("Session expired");
            // 1. 关闭旧会话
            zookeeper_close(zh);
            // 下面的步骤是可能需要做的,这里不予演示
            // 2. 重新创建链接
            // 3. 重新创建所有临时节点
            // 4. 重新注册所有watcher
        }
    }
}

} // namespace

ZKClient::ZKClient() : _zhandle(nullptr), _timeout(30000) { }

ZKClient::~ZKClient() {
    Close();
}

void ZKClient::Start() {
    std::string host = MprpcApplication::GetInstance().GetConfig().Get("zookeeperip");
    std::string port = MprpcApplication::GetInstance().GetConfig().Get("zookeeperport");
    _host = host + ":" + port;
    log_info("Zookeeper Started in {}", _host);
    _zhandle = zookeeper_init(_host.c_str(), global_watcher, 30000, nullptr, nullptr, 0);
    if (_zhandle == nullptr) {
        log_error("Zookeeper_init Error");
        exit(EXIT_FAILURE);
    }

    sem_t sem;
    sem_init(&sem, 0, 0);

    zoo_set_context(_zhandle, &sem);

    sem_wait(&sem);
    sem_destroy(&sem);
    log_info("zookeeper_init_success!");
}

void ZKClient::Close() {
    if (_zhandle) {
        zookeeper_close(_zhandle);
        _zhandle = nullptr;
        log_info("Zookeeper connection closed");
    }
}

void ZKClient::Create(char const *path, char const *data, int datalen, int state) {
    char path_buf[128];
    int bufflen = sizeof path_buf;
    int flag;
    flag = zoo_exists(_zhandle, path, 0, nullptr);
    if (flag == ZNONODE) /* 节点不存在 */ {
        flag = zoo_create(
            _zhandle, path, data, datalen, &ZOO_OPEN_ACL_UNSAFE, state, path_buf, bufflen);
        if (flag == ZOK) {
            std::cout << "znode create success:" << path << std::endl;
        } else {
            log_error("znode create failed,with path {} ", path);
            exit(EXIT_FAILURE);
        }
    }
}

void ZKClient::Reconnect() {
    log_info("Attempting to reconnect to ZooKeeper...");
    Close(); // 关闭旧的

    _zhandle = zookeeper_init(_host.c_str(), global_watcher, _timeout, nullptr, nullptr, 0);
    if (_zhandle == nullptr) {
        log_error("Reconnect failed: zookeeper_init error");
        exit(EXIT_FAILURE);
    }

    sem_t sem;
    sem_init(&sem, 0, 0);
    zoo_set_context(_zhandle, &sem);
    sem_wait(&sem);
    sem_destroy(&sem);

    log_info("Reconnected to ZooKeeper successfully");
}

std::string ZKClient::GetData(char const *path, Stat *stat) {
    /*
        将stat作为传出参数,用户可以接受stat结构数据
    */
    char buffer[1024];
    int buffer_len = sizeof buffer;
    int flag = zoo_get(_zhandle, path, 0, buffer, &buffer_len, stat);
    if (flag != ZOK) {
        log_error("GetData failed :{}", zerror(flag));
        return "";
    }

    return std::string(buffer, buffer_len);
}

void ZKClient::SetData(char const *path, char const *data, int datalen, int version) {
    int flag = zoo_set(_zhandle, path, data, datalen, version);
    if (flag == ZOK) {
        log_info("SetData success for path: {}", path);
    } else {
        log_error("SetData failed for path {}: {}", path, zerror(flag));
        exit(EXIT_FAILURE);
    }
}

void ZKClient::Delete(char const *path, int version) {
    int flag = zoo_delete(_zhandle, path, version);
    if (flag == ZOK) {
        log_info("Delete success for path: {}", path);
    } else if (flag == ZNONODE) {
        log_info("Delete: node does not exist, path: {}", path);
    } else {
        log_error("Delete failed for path {}: {}", path, zerror(flag));
        exit(EXIT_FAILURE);
    }
}

bool ZKClient::Exists(char const *path) {
    int flag = zoo_exists(_zhandle, path, 0, nullptr);
    return (flag == ZOK);
}

std::vector<std::string> ZKClient::GetChildren(char const *path) {
    std::vector<std::string> children;
    struct String_vector strings;
    int flag = zoo_get_children(_zhandle, path, 0, &strings);

    if (flag != ZOK) {
        log_error("GetChildren failed for path {}: {}", path, zerror(flag));
        return children;
    }

    for (int i = 0; i < strings.count; ++i) {
        children.emplace_back(strings.data[i]);
    }

    deallocate_String_vector(&strings);
    return children;
}

整体构建安装

回到项目根目录,输入一下命令

./auto_build.sh

根据提示输入密码,进行项目构建,编译和安装.

如果需要卸载清理执行:

./auto_clean.sh

安装完毕之后,就可以使用cmake进行发现和链接.

find_package(mprpc REQUIRED)
target_link_libraries(project PRIVATE mprpc)
posted @ 2026-04-27 09:28  大胖熊哈  阅读(17)  评论(0)    收藏  举报