当前位置: 首页 > news >正文

手写RPC

手写RPC

本文通过一个完整的 RPC 范例,演示在 C++ 中使用 gRPC 的开发流程,包括同步服务端/客户端以及异步服务器的实现。读者将学会如何定义服务接口、生成代码、实现业务逻辑、编译运行,并理解异步模型的核心机制。


一、gRPC C++ 开发环境快速回顾

在开始编写代码前,请确保已正确安装 gRPC 和 Protocol Buffers。简要步骤(以 Ubuntu 20.04 为例):

# 安装依赖
sudo apt install build-essential autoconf libtool pkg-config cmake# 安装 protobuf(推荐源码编译,版本 3.21.12)
wget https://github.com/protocolbuffers/protobuf/releases/download/v21.12/protobuf-cpp-3.21.12.tar.gz
tar -xzf protobuf-cpp-3.21.12.tar.gz
cd protobuf-3.21.12
./configure
make -j$(nproc)
sudo make install
sudo ldconfig# 安装 gRPC(版本 1.46.0)
git clone --depth 1 --branch v1.46.0 https://github.com/grpc/grpc.git
cd grpc
git submodule update --init --recursive
mkdir -p cmake/build
cd cmake/build
cmake -DgRPC_INSTALL=ON -DgRPC_BUILD_TESTS=OFF ../..
make -j$(nproc)
sudo make install
sudo ldconfig

验证安装:

protoc --version          # 应显示 libprotoc 3.21.12
g++ --version             # 应支持 C++14/17

二、手写 RPC 范例完整流程

我们将实现一个简单的用户注册/登录服务,命名为 im.login。服务提供两个 RPC 方法:RegisterLogin

2.1 创建工程目录

mkdir -p grpc_example_cpp/im_login
cd grpc_example_cpp/im_login

目录结构建议:

im_login/
├── proto/
│   └── im.login.proto
├── server.cc
├── client.cc
├── CMakeLists.txt
└── build/

2.2 编写 proto 文件

proto/im.login.proto 中定义服务和消息:

syntax = "proto3";package im.login;// 注册请求
message RegisterRequest {string username = 1;string password = 2;
}// 注册响应
message RegisterResponse {int32 status = 1;       // 0: 成功, 其他: 错误码string message = 2;     // 提示信息int32 user_id = 3;      // 注册成功后返回的用户ID
}// 登录请求
message LoginRequest {string username = 1;string password = 2;
}// 登录响应
message LoginResponse {int32 status = 1;string message = 2;int32 user_id = 3;
}// 服务定义
service LoginService {rpc Register (RegisterRequest) returns (RegisterResponse);rpc Login (LoginRequest) returns (LoginResponse);
}

要点说明

  • syntax = "proto3" 声明使用 proto3 语法。
  • package im.login 定义命名空间,生成 C++ 代码时转换为 namespace im::login
  • 每个 message 字段有唯一编号,用于二进制序列化。
  • 建议在响应中包含 statusmessage 字段,便于错误处理。

2.3 生成 C++ 代码

使用 protoc 生成代码,需要指定 grpc_cpp_plugin 路径(若未配置环境变量,可通过 which grpc_cpp_plugin 查找)。

# 生成 protobuf 序列化代码
protoc --proto_path=proto --cpp_out=. proto/im.login.proto# 生成 gRPC 服务代码
protoc --proto_path=proto --grpc_out=. --plugin=protoc-gen-grpc=`which grpc_cpp_plugin` proto/im.login.proto

执行后,当前目录下会生成四个文件:

  • im.login.pb.him.login.pb.cc:消息类的定义与实现。
  • im.login.grpc.pb.him.login.grpc.pb.cc:服务基类(LoginService::Service)和客户端存根(LoginService::Stub)。

2.4 编写同步服务端程序

创建 server.cc,实现服务逻辑。

2.4.1 包含头文件和命名空间

#include <iostream>
#include <memory>
#include <string>#include <grpcpp/grpcpp.h>
#include "im.login.grpc.pb.h"using grpc::Server;
using grpc::ServerBuilder;
using grpc::ServerContext;
using grpc::Status;
using im::login::LoginRequest;
using im::login::LoginResponse;
using im::login::RegisterRequest;
using im::login::RegisterResponse;
using im::login::LoginService;

2.4.2 实现服务类

继承生成的 LoginService::Service,重写 RegisterLogin 方法。

class LoginServiceImpl final : public LoginService::Service {
public:// 注册逻辑Status Register(ServerContext* context, const RegisterRequest* request,RegisterResponse* reply) override {std::cout << "Register request: username=" << request->username()<< ", password=" << request->password() << std::endl;// 模拟注册:假设总是成功,分配一个自增IDstatic int next_id = 1000;reply->set_status(0);reply->set_message("Registration successful");reply->set_user_id(next_id++);return Status::OK;}// 登录逻辑Status Login(ServerContext* context, const LoginRequest* request,LoginResponse* reply) override {std::cout << "Login request: username=" << request->username()<< ", password=" << request->password() << std::endl;// 模拟登录:假设任意用户名密码都可登录,返回固定IDreply->set_status(0);reply->set_message("Login successful");reply->set_user_id(12345);  // 演示用固定IDreturn Status::OK;}
};

2.4.3 启动服务

main 函数中配置并启动服务器。

void RunServer() {std::string server_address("0.0.0.0:50051");LoginServiceImpl service;ServerBuilder builder;builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());builder.RegisterService(&service);// 可选:设置一些性能参数builder.AddChannelArgument(GRPC_ARG_KEEPALIVE_TIME_MS, 5000);     // 5秒心跳builder.AddChannelArgument(GRPC_ARG_KEEPALIVE_PERMIT_WITHOUT_CALLS, 1);builder.AddChannelArgument(GRPC_ARG_HTTP2_MAX_PING_STRIKES, 0);std::unique_ptr<Server> server(builder.BuildAndStart());std::cout << "Server listening on " << server_address << std::endl;server->Wait();  // 阻塞直到服务器关闭
}int main() {RunServer();return 0;
}

2.5 编写客户端程序

创建 client.cc,实现客户端调用。

2.5.1 包含头文件和命名空间

#include <iostream>
#include <memory>
#include <string>#include <grpcpp/grpcpp.h>
#include "im.login.grpc.pb.h"using grpc::Channel;
using grpc::ClientContext;
using grpc::Status;
using im::login::LoginRequest;
using im::login::LoginResponse;
using im::login::RegisterRequest;
using im::login::RegisterResponse;
using im::login::LoginService;

2.5.2 封装客户端类

class LoginClient {
public:LoginClient(std::shared_ptr<Channel> channel): stub_(LoginService::NewStub(channel)) {}// 注册方法bool Register(const std::string& username, const std::string& password) {RegisterRequest request;request.set_username(username);request.set_password(password);RegisterResponse response;ClientContext context;Status status = stub_->Register(&context, request, &response);if (status.ok()) {std::cout << "Register success: " << response.message()<< ", user_id = " << response.user_id() << std::endl;return true;} else {std::cout << "Register failed: " << status.error_code() << ": "<< status.error_message() << std::endl;return false;}}// 登录方法bool Login(const std::string& username, const std::string& password) {LoginRequest request;request.set_username(username);request.set_password(password);LoginResponse response;ClientContext context;Status status = stub_->Login(&context, request, &response);if (status.ok()) {std::cout << "Login success: " << response.message()<< ", user_id = " << response.user_id() << std::endl;return true;} else {std::cout << "Login failed: " << status.error_code() << ": "<< status.error_message() << std::endl;return false;}}private:std::unique_ptr<LoginService::Stub> stub_;
};

2.5.3 编写 main 函数

int main() {auto channel = grpc::CreateChannel("localhost:50051",grpc::InsecureChannelCredentials());LoginClient client(channel);// 测试注册client.Register("alice", "123456");// 测试登录client.Login("alice", "123456");return 0;
}

2.6 编译与运行

使用 CMake 构建。编写 CMakeLists.txt 如下:

cmake_minimum_required(VERSION 3.10)
project(im_login LANGUAGES CXX)set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)# gRPC 通常通过 CONFIG 模式提供;Protobuf 在 Ubuntu 上更常见的是 FindProtobuf 模块模式。
find_package(gRPC CONFIG REQUIRED)
find_package(Protobuf REQUIRED)# 直接使用已生成的源码文件,避免因不同发行版的 CMake 宏差异导致配置失败。
set(GEN_SRCSim.login.pb.ccim.login.grpc.pb.cc
)# 添加服务端可执行文件
add_executable(server server.cc ${GEN_SRCS})
target_include_directories(server PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${Protobuf_INCLUDE_DIRS})
target_link_libraries(server PRIVATE gRPC::grpc++ protobuf::libprotobuf)# 添加客户端可执行文件
add_executable(client client.cc ${GEN_SRCS})
target_include_directories(client PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${Protobuf_INCLUDE_DIRS})
target_link_libraries(client PRIVATE gRPC::grpc++ protobuf::libprotobuf)

编译步骤:

mkdir build && cd build
cmake ..
make

运行:

# 终端1:启动服务端
./server# 终端2:运行客户端
./client

预期输出:

Register success: Registration successful, user_id = 1000
Login success: Login successful, user_id = 12345

服务端控制台会打印请求信息。


三、异步服务器编写

同步服务器在业务逻辑耗时(如数据库查询、文件处理)时会阻塞线程,影响吞吐量。异步服务器通过状态机和事件队列,将业务逻辑与 I/O 解耦,可显著提高并发能力。

3.1 异步处理核心概念

  • CompletionQueue:gRPC 提供的事件队列,用于存放已完成的操作(如接收到请求、发送响应完成)。
  • CallData 状态机:每个 RPC 请求对应一个状态对象,通常包含三个状态:
    • CREATE:请求已创建,等待被框架接收。
    • PROCESS:请求已接收,正在处理业务逻辑。
    • FINISH:业务处理完成,等待发送响应。
  • 异步服务端流程
    1. 创建 CompletionQueue,并将服务注册到 ServerBuilder
    2. 为每种 RPC 方法预先创建 CallData 对象并调用 RequestXxx 注册到队列。
    3. 主线程循环从 CompletionQueue 取出事件,根据 tag 调用对应对象的 Proceed
    4. Proceed 中根据状态执行相应操作:接收请求 → 处理业务(可能提交到线程池)→ 发送响应 → 销毁对象。

3.2 多接口异步实现(继承方式)

当服务包含多个 RPC 方法时,可定义基类 CallData,然后为每个方法创建派生类,实现各自的业务逻辑。

3.2.1 定义基类 CallData

#include <grpcpp/grpcpp.h>
#include "im.login.grpc.pb.h"using grpc::ServerCompletionQueue;
using grpc::ServerContext;// 基类,定义公共接口
class CallData {
public:virtual ~CallData() = default;virtual void Proceed() = 0;
};

3.2.2 为 Register 方法实现派生类

class RegisterCallData : public CallData {
public:RegisterCallData(im::login::LoginService::AsyncService* service,ServerCompletionQueue* cq): service_(service), cq_(cq), responder_(&ctx_), status_(CREATE) {Proceed();  // 注册请求}void Proceed() override {if (status_ == CREATE) {status_ = PROCESS;service_->RequestRegister(&ctx_, &request_, &responder_, cq_, cq_, this);} else if (status_ == PROCESS) {// 创建新的 CallData 以接收下一个请求new RegisterCallData(service_, cq_);// 处理业务逻辑(这里模拟耗时操作,可提交到线程池)std::cout << "Register: " << request_.username() << std::endl;// 构造响应static int next_id = 1000;reply_.set_status(0);reply_.set_message("Registration successful");reply_.set_user_id(next_id++);status_ = FINISH;responder_.Finish(reply_, grpc::Status::OK, this);} else {delete this;}}private:im::login::LoginService::AsyncService* service_;ServerCompletionQueue* cq_;ServerContext ctx_;im::login::RegisterRequest request_;im::login::RegisterResponse reply_;grpc::ServerAsyncResponseWriter<im::login::RegisterResponse> responder_;enum CallStatus { CREATE, PROCESS, FINISH };CallStatus status_;
};

3.2.3 为 Login 方法实现派生类

类似地,创建 LoginCallData

class LoginCallData : public CallData {
public:LoginCallData(im::login::LoginService::AsyncService* service,ServerCompletionQueue* cq): service_(service), cq_(cq), responder_(&ctx_), status_(CREATE) {Proceed();}void Proceed() override {if (status_ == CREATE) {status_ = PROCESS;service_->RequestLogin(&ctx_, &request_, &responder_, cq_, cq_, this);} else if (status_ == PROCESS) {new LoginCallData(service_, cq_);   // 为下一个请求创建实例// 业务逻辑std::cout << "Login: " << request_.username() << std::endl;reply_.set_status(0);reply_.set_message("Login successful");reply_.set_user_id(12345);status_ = FINISH;responder_.Finish(reply_, grpc::Status::OK, this);} else {delete this;}}private:im::login::LoginService::AsyncService* service_;ServerCompletionQueue* cq_;ServerContext ctx_;im::login::LoginRequest request_;im::login::LoginResponse reply_;grpc::ServerAsyncResponseWriter<im::login::LoginResponse> responder_;enum CallStatus { CREATE, PROCESS, FINISH };CallStatus status_;
};

3.2.4 异步服务端主循环

void RunAsyncServer() {std::string server_address("0.0.0.0:50051");im::login::LoginService::AsyncService service;ServerBuilder builder;builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());builder.RegisterService(&service);auto cq = builder.AddCompletionQueue();auto server = builder.BuildAndStart();std::cout << "Async server listening on " << server_address << std::endl;// 预先创建处理类实例,以接收请求new RegisterCallData(&service, cq.get());new LoginCallData(&service, cq.get());void* tag;bool ok;while (true) {// 阻塞等待完成事件if (!cq->Next(&tag, &ok)) break;// 触发对应对象的 Proceedstatic_cast<CallData*>(tag)->Proceed();}
}int main() {RunAsyncServer();return 0;
}

3.3 异步处理的优势

  • 高并发:主线程只处理事件分发,不被业务逻辑阻塞。
  • 可扩展:可将耗时业务提交到独立线程池,进一步提高吞吐量。
  • 资源节约:无需为每个请求创建线程,避免线程创建销毁开销。

注意:异步服务器代码复杂度较高,对于简单场景,同步服务器已足够。在性能要求高或业务逻辑耗时时,再考虑异步实现。

3.4 编译异步服务器

CMakeLists.txt 中添加:

# 添加异步服务端可执行文件
add_executable(server_async server_async.cc ${GEN_SRCS})
target_include_directories(server_async PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${Protobuf_INCLUDE_DIRS})
target_link_libraries(server_async PRIVATE gRPC::grpc++ protobuf::libprotobuf)

编译运行,用同步客户端测试,应得到相同结果。


四、常见问题与总结

4.1 常见错误及解决方案

错误现象 可能原因 解决方法
protoc 找不到 grpc_cpp_plugin 未安装或环境变量未配置 使用 which grpc_cpp_plugin 找到路径,在命令中指定 --plugin=protoc-gen-grpc=路径
编译时未找到 grpcpp/grpcpp.h 未正确安装 gRPC 或 CMake 未找到包 检查安装路径,设置 CMAKE_PREFIX_PATH 或使用 find_packageCONFIG 模式
运行时连接失败 服务端未启动或端口被占用 使用 netstat -tlnp 检查端口;确认服务端地址为 0.0.0.0 或正确 IP
异步服务器未收到请求 未预先创建 CallData 对象 在启动前必须为每个 RPC 方法创建至少一个实例
内存泄漏 CallData 未正确删除 确保在 FINISH 状态执行 delete this
http://www.jsqmd.com/news/555678/

相关文章:

  • 2026年羊绒衫厂家推荐:高端商务通勤羊绒衫靠谱厂家及用户口碑分析 - 十大品牌推荐
  • 智能工作流引擎:多智能体系统任务编排的高效解决方案
  • 计算机毕业设计springboot停车场管理系统设计与实现 基于SpringBoot的智慧停车服务平台设计与实现 智能停车场运营系统开发与优化
  • 【Python 教程】如何将 JSON 数据转换为 Excel 工作表
  • 干货测评|2026年靠谱一键生成论文工具榜单,高质初稿轻松写
  • 机器视觉如何赋予机器“三维双眼”——3D重建技术全景指南
  • RPCS3开源模拟器完全指南:让PS3游戏在PC重生的实用方案
  • GD32F407实战指南:GPIO模拟IIC驱动24C08 EEPROM数据持久化
  • 如何通过Agent-S实现智能化CI/CD流水线构建与优化
  • Phi-4-Reasoning-Vision部署教程:解决双卡算力分配不均的4个调试技巧
  • SQLiteGo:国产 ARM (aarch64) 银河麒麟 SQLite 数据库管理和数据分析工具分享
  • EmbeddingGemma-300m部署全攻略:从安装到应用场景解析
  • 终极指南:如何用MiniCPM-V 1.0构建高效轻量级多模态大模型应用
  • Vue前端集成lingbot-depth-pretrain-vitl-143D可视化组件
  • 深度剖析Mac Mouse Fix:开源鼠标驱动架构演进与性能优化实战
  • HsMod:炉石传说游戏增强框架完全部署指南
  • windows10 Qt5.15.14 msvc2019 编译部署
  • PyTorch 2.x实战:torch.compile如何让你的模型训练速度翻倍(附详细性能对比)
  • 前后端框架模式对比(golang)
  • ComfyUI工作流迁移实战指南:7个关键策略打造无缝创作体验
  • YOLOv12官版镜像5分钟快速部署:零基础搭建实时目标检测环境
  • 告别格式迷宫:3个让图片处理效率提升10倍的隐藏功能
  • SenseVoice-Small模型服务监控与日志收集实战
  • 飞牛NAS系统上玩转Docker版OpenWrt:从网卡名识别到完整旁路由搭建指南
  • 从协议栈到信号修复:一份给硬件工程师的UCIe实战避坑手册
  • 别再只会用示波器了!用STM32做一个便携式多功能频率计,测频/测周期/测占空比全搞定
  • 掌握AI专著生成技巧,借助优质工具,快速产出高质量专著
  • UVM调试必备:如何用uvm_info宏精准控制日志输出(附实战代码)
  • 通义千问1.5-1.8B-Chat-GPTQ-Int4长文本处理技巧:突破上下文窗口限制的实践
  • OpenClaw配置备份术:GLM-4.7-Flash模型迁移与灾难恢复