手写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 方法:Register 和 Login。
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 字段有唯一编号,用于二进制序列化。
- 建议在响应中包含
status和message字段,便于错误处理。
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.h和im.login.pb.cc:消息类的定义与实现。im.login.grpc.pb.h和im.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,重写 Register 和 Login 方法。
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;
// 模拟注册:假设总是成功,分配一个自增ID
static 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;
// 模拟登录:假设任意用户名密码都可登录,返回固定ID
reply->set_status(0);
reply->set_message("Login successful");
reply->set_user_id(12345); // 演示用固定ID
return 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_SRCS
im.login.pb.cc
im.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:业务处理完成,等待发送响应。
- 异步服务端流程:
- 创建
CompletionQueue,并将服务注册到ServerBuilder。 - 为每种 RPC 方法预先创建
CallData对象并调用RequestXxx注册到队列。 - 主线程循环从
CompletionQueue取出事件,根据tag调用对应对象的Proceed。 - 在
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;
// 触发对应对象的 Proceed
static_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_package 的 CONFIG 模式 |
| 运行时连接失败 | 服务端未启动或端口被占用 | 使用 netstat -tlnp 检查端口;确认服务端地址为 0.0.0.0 或正确 IP |
| 异步服务器未收到请求 | 未预先创建 CallData 对象 |
在启动前必须为每个 RPC 方法创建至少一个实例 |
| 内存泄漏 | CallData 未正确删除 |
确保在 FINISH 状态执行 delete this |

浙公网安备 33010602011771号