一、使用C++举例
二、场景是让一个客户端发送消息到客户端,整个消息传递通过grpc来承载.要达到这样的目的,需要写代码时候要做哪些事情?
三、要实现这个例子的目的,其实真正只需要涉及3个文件.
client.cpp---模拟客户端发送消息的文件
server.cpp---模拟服务端接收消息的文件
example.proto----定义消息(消息格式),和服务端处理消息的函数.目的是让protoc根据这个设定自动生成一部分xxx.grpc.pb.cc(帮着生成一部分符合grpc逻辑的代码)代码和xxx.pb.cc(帮忙生成一部分序列化和反序列化的)代码.
四、我用AI生成了这个实例的代码.

注意这里CMakeLists.txt和README.md文件是方便编译和代码提示的文件,本身不涉及这个例子的功能代码.
example.proto文件
syntax = "proto3";
package example;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
client.cpp文件
cpp
// ============================================================
// gRPC 客户端程序
// 功能: 连接到服务端,调用远程的 SayHello 和 SayHelloAgain 方法
// ============================================================
// ========== C++ 标准库头文件 ==========
#include <iostream> // 标准输入输出,用于打印结果
#include <memory> // 智能指针库,用于 std::unique_ptr 和 std::shared_ptr
#include <string> // C++ 字符串类
#include <iomanip> // 用于格式化输出
#include <vector> // 用于存储字节数据
#include <chrono> // 用于获取时间戳
#include <ctime> // 用于时间格式化
#include <sstream> // 用于字符串流
// ========== gRPC 头文件 ==========
#include <grpcpp/grpcpp.h> // gRPC 核心库
#include "example.grpc.pb.h" // 由 example.proto 生成的客户端存根
// ============================================================
// 获取当前时间戳的辅助函数
// ============================================================
std::string GetCurrentTimestamp() {
// 获取当前时间点
auto now = std::chrono::system_clock::now();
// 转换为 time_t
std::time_t now_time = std::chrono::system_clock::to_time_t(now);
// 转换为本地时间
std::tm local_tm = *std::localtime(&now_time);
// 格式化为字符串 HH:MM:SS.mmm
std::ostringstream oss;
oss << std::setfill('0') << std::setw(2) << local_tm.tm_hour << ":"
<< std::setfill('0') << std::setw(2) << local_tm.tm_min << ":"
<< std::setfill('0') << std::setw(2) << local_tm.tm_sec << "."
<< std::setfill('0') << std::setw(3)
<< std::chrono::duration_cast<std::chrono::milliseconds>(
now.time_since_epoch()).count() % 1000;
return oss.str();
}
// ============================================================
// 客户端类
// 封装了调用 gRPC 服务的操作
// ============================================================
class GreeterClient {
public:
// ========== 构造函数 ==========
// 参数: channel - 到服务端的通信通道
//
// 构造函数使用初始化列表创建 RPC 存根(Stub)
// Stub 是客户端的代理对象,负责处理网络通信和序列化
GreeterClient(std::shared_ptr<grpc::Channel> channel)
: stub_(example::Greeter::NewStub(channel)) {}
// ========== 调用 SayHello RPC 方法 ==========
// 参数: user - 要发送给服务端的名字
// 返回: 服务端的响应消息
std::string SayHello(const std::string& user) {
// ========== 1. 准备请求 ==========
// 创建请求对象
example::HelloRequest request;
// 设置请求参数
// set_name() 是 protoc 自动生成的 setter 方法
request.set_name(user);
// ========== 【打印客户端发送的消息】==========
std::cout << "========== 客户端发送 SayHello 请求 ==========" << std::endl;
std::cout << "【时间】" << GetCurrentTimestamp() << std::endl;
std::cout << "【原始消息内容】" << std::endl;
std::cout << " name: " << request.name() << std::endl;
// 打印 protobuf 的调试字符串(显示所有字段)
std::cout << "【Protobuf DebugString】" << std::endl;
std::cout << " " << request.DebugString() << std::endl;
// 手动序列化获取原始字节
std::string serialized_data;
request.SerializeToString(&serialized_data);
std::cout << "【序列化后的二进制数据】" << std::endl;
std::cout << " 大小: " << serialized_data.size() << " 字节" << std::endl;
std::cout << " 十六进制: ";
for (size_t i = 0; i < serialized_data.size(); i++) {
printf("%02X ", (unsigned char)serialized_data[i]);
}
std::cout << std::endl;
std::cout << "=============================================" << std::endl << std::endl;
// ========== 2. 准备接收响应 ==========
// 创建响应对象(用于接收服务端的回复)
example::HelloReply reply;
// 创建客户端上下文
// ClientContext 可以设置超时、添加元数据等
grpc::ClientContext context;
// 记录开始时间
auto start_time = std::chrono::high_resolution_clock::now();
// ========== 3. 发起 RPC 调用 ==========
// stub_->SayHello() 会自动完成以下操作:
// 1. 将 request 序列化为二进制
// 2. 通过 HTTP/2 发送到服务端
// 3. 等待服务端响应
// 4. 将响应反序列化到 reply
// 5. 返回调用状态
grpc::Status status = stub_->SayHello(&context, request, &reply);
// 计算耗时
auto end_time = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);
// ========== 4. 处理调用结果 ==========
if (status.ok()) {
// RPC 调用成功,返回服务端的响应消息
// message() 是 protoc 自动生成的 getter 方法
// ========== 【打印服务端返回的消息】==========
std::cout << "========== 客户端收到 SayHello 响应 ==========" << std::endl;
std::cout << "【时间】" << GetCurrentTimestamp() << std::endl;
std::cout << "【耗时】" << duration.count() << " ms" << std::endl;
std::cout << "【原始消息内容】" << std::endl;
std::cout << " message: " << reply.message() << std::endl;
std::cout << "【Protobuf DebugString】" << std::endl;
std::cout << " " << reply.DebugString() << std::endl;
std::cout << "=============================================" << std::endl << std::endl;
return reply.message();
} else {
// RPC 调用失败,返回错误信息
std::cout << "【时间】" << GetCurrentTimestamp() << std::endl;
std::cout << "【错误】RPC failed: " << status.error_message() << std::endl;
return "RPC failed";
}
}
private:
// ========== 成员变量 ==========
// stub_: RPC 存根,用于调用远程服务
// unique_ptr 表示独占所有权,自动管理内存
std::unique_ptr<example::Greeter::Stub> stub_;
};
// ============================================================
// 程序入口
// ============================================================
int main(int argc, char** argv) {
// ========== 【程序启动】==========
std::cout << "╔══════════════════════════════════════════════════════════════╗" << std::endl;
std::cout << "║ gRPC 客户端程序启动 ║" << std::endl;
std::cout << "║ 启动时间: " << GetCurrentTimestamp() << " ║" << std::endl;
std::cout << "╚══════════════════════════════════════════════════════════════╝" << std::endl;
std::cout << std::endl;
// ========== 1. 定义服务端地址 ==========
std::string target_str = "localhost:50052";
// ========== 2. 创建客户端对象 ==========
// grpc::CreateChannel() 创建到服务端的通信通道
// grpc::InsecureChannelCredentials() 表示不加密的连接(仅用于开发)
GreeterClient greeter(grpc::CreateChannel(
target_str, grpc::InsecureChannelCredentials()));
std::cout << "【连接信息】目标地址: " << target_str << std::endl;
std::cout << std::endl;
// ========== 3. RPC 调用 ==========
std::string user("World"); // 准备参数
std::cout << "【操作】开始 RPC 调用: SayHello(\"" << user << "\")" << std::endl;
std::cout << std::endl;
std::string reply = greeter.SayHello(user); // 调用远程方法
std::cout << "【最终输出】Greeter received: " << reply << std::endl;
std::cout << std::endl;
// ========== 【程序结束】==========
std::cout << "╔══════════════════════════════════════════════════════════════╗" << std::endl;
std::cout << "║ gRPC 客户端程序结束 ║" << std::endl;
std::cout << "║ 结束时间: " << GetCurrentTimestamp() << " ║" << std::endl;
std::cout << "╚══════════════════════════════════════════════════════════════╝" << std::endl;
// ========== 5. 程序结束 ==========
return 0;
}
server.cpp
cpp
// ============================================================
// gRPC 服务端程序
// 功能: 监听 50052 端口,响应客户端的 SayHello 和 SayHelloAgain 请求
// ============================================================
// ========== C++ 标准库头文件 ==========
#include <iostream> // 标准输入输出,用于打印日志
#include <memory> // 智能指针库,用于 std::unique_ptr 和 std::shared_ptr
#include <string> // C++ 字符串类
#include <cstdlib> // 用于 exit()
#include <chrono> // 用于获取时间戳
#include <ctime> // 用于时间格式化
#include <sstream> // 用于字符串流
#include <iomanip> // 用于格式化输出
// ========== gRPC 头文件 ==========
#include <grpcpp/grpcpp.h> // gRPC 核心库
#include "example.grpc.pb.h" // 由 example.proto 生成的服务基类
// ============================================================
// 获取当前时间戳的辅助函数
// ============================================================
std::string GetCurrentTimestamp() {
// 获取当前时间点
auto now = std::chrono::system_clock::now();
// 转换为 time_t
std::time_t now_time = std::chrono::system_clock::to_time_t(now);
// 转换为本地时间
std::tm local_tm = *std::localtime(&now_time);
// 格式化为字符串 HH:MM:SS.mmm
std::ostringstream oss;
oss << std::setfill('0') << std::setw(2) << local_tm.tm_hour << ":"
<< std::setfill('0') << std::setw(2) << local_tm.tm_min << ":"
<< std::setfill('0') << std::setw(2) << local_tm.tm_sec << "."
<< std::setfill('0') << std::setw(3)
<< std::chrono::duration_cast<std::chrono::milliseconds>(
now.time_since_epoch()).count() % 1000;
return oss.str();
}
// ============================================================
// 服务实现类
// 继承自 example::Greeter::Service (由 protoc 自动生成的基类)
// ============================================================
class GreeterServiceImpl final : public example::Greeter::Service {
public:
// ========== 实现 SayHello RPC 方法 ==========
// 当客户端调用 SayHello 时,gRPC 框架会自动调用这个方法
//
// 参数说明:
// context - RPC 调用的上下文信息(如超时时间、元数据等)
// request - 客户端发送的请求(只读指针)
// reply - 要返回给客户端的响应(需要填充数据)
//
// 返回值:
// grpc::Status - 表示调用成功或失败的状态
grpc::Status SayHello(grpc::ServerContext* context,
const example::HelloRequest* request,
example::HelloReply* reply) override {
// ========== 【服务端接收到的消息】==========
std::cout << "========== 服务端收到 SayHello 请求 ==========" << std::endl;
std::cout << "【时间】" << GetCurrentTimestamp() << std::endl;
std::cout << "【gRPC 框架已自动反序列化】" << std::endl;
std::cout << "【原始消息内容】" << std::endl;
std::cout << " name: " << request->name() << std::endl;
// 打印 protobuf 的调试字符串(显示所有字段)
std::cout << "【Protobuf DebugString】" << std::endl;
std::cout << " " << request->DebugString() << std::endl;
// 手动序列化显示接收到的原始字节
std::string serialized_data;
request->SerializeToString(&serialized_data);
std::cout << "【序列化后的二进制数据】" << std::endl;
std::cout << " 大小: " << serialized_data.size() << " 字节" << std::endl;
std::cout << " 十六进制: ";
for (size_t i = 0; i < serialized_data.size(); i++) {
printf("%02X ", (unsigned char)serialized_data[i]);
}
std::cout << std::endl;
std::cout << "=============================================" << std::endl;
// ========== 业务逻辑 ==========
// 拼接 "Hello " 和客户端发来的名字
// request->name() 获取客户端发送的名字
std::string response_msg = "Hello " + request->name();
// ========== 【准备返回响应】==========
reply->set_message(response_msg);
std::cout << "【服务端准备返回响应】" << std::endl;
std::cout << " 时间: " << GetCurrentTimestamp() << std::endl;
std::cout << " message: " << response_msg << std::endl;
std::cout << "=============================================" << std::endl << std::endl;
// 返回 OK 状态,告诉 gRPC 这次调用成功
return grpc::Status::OK;
}
};
// ============================================================
// 启动服务器的函数
// ============================================================
void RunServer() {
// ========== 【服务器启动】==========
std::cout << "╔══════════════════════════════════════════════════════════════╗" << std::endl;
std::cout << "║ gRPC 服务端程序启动 ║" << std::endl;
std::cout << "║ 启动时间: " << GetCurrentTimestamp() << " ║" << std::endl;
std::cout << "╚══════════════════════════════════════════════════════════════╝" << std::endl;
std::cout << std::endl;
// ========== 1. 定义服务器地址 ==========
// "0.0.0.0" 表示监听所有网卡(可以被任何 IP 访问)
// "50052" 是端口号
std::string server_address("0.0.0.0:50052");
// ========== 2. 创建服务实现对象 ==========
// 这个对象包含了我们实现的 RPC 方法
GreeterServiceImpl service;
// ========== 3. 创建 gRPC 服务器构建器 ==========
// ServerBuilder 是建造者模式,用于配置和启动服务器
grpc::ServerBuilder builder;
// ========== 4. 配置服务器 ==========
// 添加监听端口和认证方式
// InsecureServerCredentials() 表示不加密(仅用于开发环境)
builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());
// 注册服务实现
// 告诉服务器:收到 Greeter 服务的请求时,调用 service 对象的方法
builder.RegisterService(&service);
// ========== 5. 构建并启动服务器 ==========
// BuildAndStart() 同时完成:
// 1. 创建服务器对象
// 2. 绑定端口
// 3. 启动监听线程
std::unique_ptr<grpc::Server> server(builder.BuildAndStart());
// ========== 错误检查 ==========
// 如果服务器启动失败(比如端口被占用),server 会是 nullptr
if (!server) {
std::cerr << "Failed to start server on " << server_address << std::endl;
std::cerr << "The port may already be in use. Try:" << std::endl;
std::cerr << " 1. Kill the process using port 50052:" << std::endl;
std::cerr << " lsof -i :50052 # find the process" << std::endl;
std::cerr << " kill -9 <PID> # kill it" << std::endl;
std::cerr << " 2. Or change the port in server.cpp" << std::endl;
exit(1); // 退出程序
}
// 输出启动成功的日志
std::cout << "【服务端已启动】监听地址: " << server_address << std::endl;
std::cout << "等待客户端连接...\n" << std::endl;
// ========== 6. 等待请求 ==========
// Wait() 会阻塞主线程,持续处理客户端请求
// 只有在服务器被关闭时才会返回
server->Wait();
}
// ============================================================
// 程序入口
// ============================================================
int main(int argc, char** argv) {
// 启动服务器(程序会阻塞在这里,直到服务器被关闭)
RunServer();
// 理论上不会执行到这里,因为 Wait() 是无限阻塞的
return 0;
}
CMakeLists.txt
cpp
# ============================================================
# CMake 配置文件: gRPC C++ 示例项目
# ============================================================
# ========== 基本项目配置 ==========
# 指定需要的最低 CMake 版本
cmake_minimum_required(VERSION 3.14)
# 定义项目名称和使用的编程语言 (CXX = C++)
project(gRPC_example CXX)
# 设置 C++ 标准为 C++17
set(CMAKE_CXX_STANDARD 17)
# 强制要求使用 C++17,不允许降级
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# ========== 查找依赖包 ==========
# 查找 PkgConfig 工具(用于查找系统库)
# REQUIRED 表示必须找到,找不到就报错
find_package(PkgConfig REQUIRED)
# 使用 pkg-config 查找 gRPC 库
# grpc++ 是 gRPC 的 C++ 库包名
# 查找成功后,会设置 GRPC_PKG_INCLUDE_DIRS 和 GRPC_PKG_LDFLAGS 变量
pkg_check_modules(GRPC_PKG REQUIRED grpc++)
# 使用 pkg-config 查找 protobuf 库
# protobuf 是 Protocol Buffers 的包名
# 查找成功后,会设置 PROTOBUF_PKG_INCLUDE_DIRS 和 PROTOBUF_PKG_LDFLAGS 变量
pkg_check_modules(PROTOBUF_PKG REQUIRED protobuf)
# ========== 查找 grpc_cpp_plugin 工具 ==========
# protoc 的 gRPC 插件,用于生成 gRPC 代码
find_program(GRPC_CPP_PLUGIN grpc_cpp_plugin)
# 输出找到的插件路径,方便调试
message(STATUS "Found grpc_cpp_plugin: ${GRPC_CPP_PLUGIN}")
# ========== 设置头文件搜索路径 ==========
# 当前源代码目录(用于找到我们自己写的头文件)
include_directories(${CMAKE_CURRENT_SOURCE_DIR})
# 构建目录(用于找到 protoc 生成的头文件)
include_directories(${CMAKE_CURRENT_BINARY_DIR})
# gRPC 的头文件路径
include_directories(${GRPC_PKG_INCLUDE_DIRS})
# protobuf 的头文件路径
include_directories(${PROTOBUF_PKG_INCLUDE_DIRS})
# ========== 定义 .proto 文件 ==========
# 指定要编译的 protobuf 文件
set(PROTO_FILES "${CMAKE_CURRENT_SOURCE_DIR}/example.proto")
# ========== 遍历每个 .proto 文件并生成代码 ==========
foreach(PROTO_FILE ${PROTO_FILES})
# 获取文件名(不带路径和扩展名)
# 例如: /path/to/example.proto → example
get_filename_component(PROTO_NAME ${PROTO_FILE} NAME_WE)
# ========== 定义生成的文件路径 ==========
# gRPC 生成的 C++ 源文件和头文件
set(PROTO_SRCS "${CMAKE_CURRENT_BINARY_DIR}/${PROTO_NAME}.grpc.pb.cc")
set(PROTO_HDRS "${CMAKE_CURRENT_BINARY_DIR}/${PROTO_NAME}.grpc.pb.h")
# protobuf 生成的 C++ 源文件和头文件
set(PROTO_PB_SRCS "${CMAKE_CURRENT_BINARY_DIR}/${PROTO_NAME}.pb.cc")
set(PROTO_PB_HDRS "${CMAKE_CURRENT_BINARY_DIR}/${PROTO_NAME}.pb.h")
# ========== 自定义命令: 调用 protoc 生成代码 ==========
# add_custom_command: 在构建过程中执行自定义命令
# OUTPUT: 指定这个命令会生成哪些文件
add_custom_command(
OUTPUT ${PROTO_SRCS} ${PROTO_HDRS} ${PROTO_PB_SRCS} ${PROTO_PB_HDRS}
# COMMAND: 要执行的命令(protoc 编译器)
COMMAND protoc
# ARGS: 传递给 protoc 的参数
ARGS --grpc_out "${CMAKE_CURRENT_BINARY_DIR}" # gRPC 代码输出目录
--cpp_out "${CMAKE_CURRENT_BINARY_DIR}" # C++ 代码输出目录
-I "${CMAKE_CURRENT_SOURCE_DIR}" # .proto 文件搜索路径
--plugin=protoc-gen-grpc="${GRPC_CPP_PLUGIN}" # 指定 gRPC 插件
"${PROTO_FILE}" # 要编译的 .proto 文件
# DEPENDS: 指定依赖关系,当 .proto 文件改变时重新生成
DEPENDS ${PROTO_FILE}
# COMMENT: 执行时显示的消息
COMMENT "Generating gRPC files from ${PROTO_FILE}"
)
# ========== 标记这些文件是自动生成的 ==========
# 告诉 CMake 这些文件是由构建系统生成的,不是用户手动写的
set_source_files_properties(${PROTO_SRCS} ${PROTO_PB_SRCS} PROPERTIES GENERATED TRUE)
endforeach()
# ========== 构建服务端可执行文件 ==========
# add_executable: 创建一个可执行文件目标
add_executable(server
server.cpp # 服务端源代码
${PROTO_SRCS} # gRPC 生成的代码
${PROTO_PB_SRCS} # protobuf 生成的代码
)
# 为服务端设置头文件搜索路径
target_include_directories(server PRIVATE ${GRPC_PKG_INCLUDE_DIRS} ${PROTOBUF_PKG_INCLUDE_DIRS})
# 为服务端链接需要的库
# ${GRPC_PKG_LDFLAGS}: 包含所有 gRPC 相关的库及其依赖
# ${PROTOBUF_PKG_LDFLAGS}: 包含 protobuf 库
target_link_libraries(server ${GRPC_PKG_LDFLAGS} ${PROTOBUF_PKG_LDFLAGS})
# ========== 构建客户端可执行文件 ==========
add_executable(client
client.cpp # 客户端源代码
${PROTO_SRCS} # gRPC 生成的代码(和服务端共用)
${PROTO_PB_SRCS} # protobuf 生成的代码(和服务端共用)
)
# 为客户端设置头文件搜索路径
target_include_directories(client PRIVATE ${GRPC_PKG_INCLUDE_DIRS} ${PROTOBUF_PKG_INCLUDE_DIRS})
# 为客户端链接需要的库
target_link_libraries(client ${GRPC_PKG_LDFLAGS} ${PROTOBUF_PKG_LDFLAGS})
cpp
# gRPC C++ 示例
## 项目结构
```
.
├── example.proto # Protocol Buffers 服务定义
├── server.cpp # gRPC 服务端实现
├── client.cpp # gRPC 客户端实现
├── CMakeLists.txt # CMake 构建配置
└── README.md # 本文件
```
## 前置要求
### macOS (使用 Homebrew)
```bash
# 安装 gRPC 和 protobuf
brew install grpc protobuf cmake pkg-config
# 如果有旧版 protobuf 在 /usr/local/include,需要移除
sudo mv /usr/local/include/google/protobuf /usr/local/include/google/protobuf.bak
```
## 构建项目
```bash
mkdir build && cd build
cmake ..
make
```
## 运行
### 启动服务端(终端1)
```bash
./server
# 输出: Server listening on 0.0.0.0:50051
```
### 运行客户端(终端2)
```bash
./client
# 输出:
# Greeter received: Hello World
# Greeter received: Hello again World
```
运行效果
