一、使用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文件// // 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_caststd::chrono::milliseconds( now.time_since_epoch()).count() % 1000; return oss.str(); } // // 客户端类 // 封装了调用 gRPC 服务的操作 // class GreeterClient { public: // 构造函数 // 参数: channel - 到服务端的通信通道 // // 构造函数使用初始化列表创建 RPC 存根Stub // Stub 是客户端的代理对象负责处理网络通信和序列化 GreeterClient(std::shared_ptrgrpc::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_caststd::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_ptrexample::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// // 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_caststd::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_ptrgrpc::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# # CMake 配置文件: gRPC C 示例项目 # # 基本项目配置 # 指定需要的最低 CMake 版本 cmake_minimum_required(VERSION 3.14) # 定义项目名称和使用的编程语言 (CXX C) project(gRPC_example CXX) # 设置 C 标准为 C17 set(CMAKE_CXX_STANDARD 17) # 强制要求使用 C17不允许降级 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 文件搜索路径 --pluginprotoc-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})README.md# 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 运行效果