From 32c5d1c1608d81c90b80b61daaf3c2e1f5d2002e Mon Sep 17 00:00:00 2001 From: Esun Kim Date: Fri, 19 Jan 2024 13:58:53 -0800 Subject: [PATCH] [Example] Added gRPC C++ generic API example (#35411) Closes #35411 PiperOrigin-RevId: 599930218 --- examples/cpp/generic_api/BUILD | 40 ++++++ examples/cpp/generic_api/CMakeLists.txt | 70 ++++++++++ examples/cpp/generic_api/README.md | 36 ++++++ examples/cpp/generic_api/greeter_client.cc | 112 ++++++++++++++++ examples/cpp/generic_api/greeter_server.cc | 143 +++++++++++++++++++++ 5 files changed, 401 insertions(+) create mode 100644 examples/cpp/generic_api/BUILD create mode 100644 examples/cpp/generic_api/CMakeLists.txt create mode 100644 examples/cpp/generic_api/README.md create mode 100644 examples/cpp/generic_api/greeter_client.cc create mode 100644 examples/cpp/generic_api/greeter_server.cc diff --git a/examples/cpp/generic_api/BUILD b/examples/cpp/generic_api/BUILD new file mode 100644 index 00000000000..ed1507d1fec --- /dev/null +++ b/examples/cpp/generic_api/BUILD @@ -0,0 +1,40 @@ +# Copyright 2023 the gRPC authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +licenses(["notice"]) + +cc_binary( + name = "greeter_client", + srcs = ["greeter_client.cc"], + defines = ["BAZEL_BUILD"], + deps = [ + "//:grpc++", + "//examples/protos:helloworld_cc_grpc", + "@com_google_absl//absl/flags:flag", + "@com_google_absl//absl/flags:parse", + ], +) + +cc_binary( + name = "greeter_server", + srcs = ["greeter_server.cc"], + defines = ["BAZEL_BUILD"], + deps = [ + "//:grpc++", + "//examples/protos:helloworld_cc_grpc", + "@com_google_absl//absl/flags:flag", + "@com_google_absl//absl/flags:parse", + "@com_google_absl//absl/strings:str_format", + ], +) diff --git a/examples/cpp/generic_api/CMakeLists.txt b/examples/cpp/generic_api/CMakeLists.txt new file mode 100644 index 00000000000..44e3820f0ab --- /dev/null +++ b/examples/cpp/generic_api/CMakeLists.txt @@ -0,0 +1,70 @@ +# Copyright 2023 the gRPC authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# cmake build file for C++ helloworld example. +# Assumes protobuf and gRPC have been installed using cmake. +# See cmake_externalproject/CMakeLists.txt for all-in-one cmake build +# that automatically builds all the dependencies before building helloworld. + +cmake_minimum_required(VERSION 3.8) + +project(GenericAPI C CXX) + +include(../cmake/common.cmake) + +# Proto files +get_filename_component(hw_proto "../../protos/helloworld.proto" ABSOLUTE) +get_filename_component(hw_proto_path "${hw_proto}" PATH) + +# Generated sources +set(hw_proto_srcs "${CMAKE_CURRENT_BINARY_DIR}/helloworld.pb.cc") +set(hw_proto_hdrs "${CMAKE_CURRENT_BINARY_DIR}/helloworld.pb.h") +set(hw_grpc_srcs "${CMAKE_CURRENT_BINARY_DIR}/helloworld.grpc.pb.cc") +set(hw_grpc_hdrs "${CMAKE_CURRENT_BINARY_DIR}/helloworld.grpc.pb.h") +add_custom_command( + OUTPUT "${hw_proto_srcs}" "${hw_proto_hdrs}" "${hw_grpc_srcs}" "${hw_grpc_hdrs}" + COMMAND ${_PROTOBUF_PROTOC} + ARGS --grpc_out "${CMAKE_CURRENT_BINARY_DIR}" + --cpp_out "${CMAKE_CURRENT_BINARY_DIR}" + -I "${hw_proto_path}" + --plugin=protoc-gen-grpc="${_GRPC_CPP_PLUGIN_EXECUTABLE}" + "${hw_proto}" + DEPENDS "${hw_proto}") + +# Include generated *.pb.h files +include_directories("${CMAKE_CURRENT_BINARY_DIR}") + +# hw_grpc_proto +add_library(hw_grpc_proto + ${hw_grpc_srcs} + ${hw_grpc_hdrs} + ${hw_proto_srcs} + ${hw_proto_hdrs}) +target_link_libraries(hw_grpc_proto + ${_REFLECTION} + ${_GRPC_GRPCPP} + ${_PROTOBUF_LIBPROTOBUF}) + +# Targets greeter_(client|server) +foreach(_target + greeter_client greeter_server) + add_executable(${_target} "${_target}.cc") + target_link_libraries(${_target} + hw_grpc_proto + absl::flags + absl::flags_parse + ${_REFLECTION} + ${_GRPC_GRPCPP} + ${_PROTOBUF_LIBPROTOBUF}) +endforeach() diff --git a/examples/cpp/generic_api/README.md b/examples/cpp/generic_api/README.md new file mode 100644 index 00000000000..bdb725f8ede --- /dev/null +++ b/examples/cpp/generic_api/README.md @@ -0,0 +1,36 @@ +# Generic API Example + +## Overview + +While generated stub code is often the simpler and best choice for sending and handling API calls, +generic APIs offer unique advantages in specific scenarios, such as proxy implementation. +Their ability to manage multiple message types with a single function makes them particularly handy +in these cases. This example demonstrates how to use generic APIs to achieve this flexibility. + +This example implements `greeter_callback_client` and `greeter_callback_server` using the generic APIs. +Therefore, looking at the difference would be helpful to understand how to use generic APIs. + +### Try it! + +Once you have working gRPC, you can build this example using either bazel or cmake. + +Run the server, which will listen on port 50051: + +```sh +$ ./greeter_server +``` + +Run the client (in a different terminal): + +```sh +$ ./greeter_client +``` + +If things go smoothly, you will see the client output: + +``` +### Send: SayHello(name=World) +Ok. ReplyMessage=Hello World +### Send: SayHello(name=gRPC) +Ok. ReplyMessage=Hello gRPC +``` diff --git a/examples/cpp/generic_api/greeter_client.cc b/examples/cpp/generic_api/greeter_client.cc new file mode 100644 index 00000000000..8435ad6c2e3 --- /dev/null +++ b/examples/cpp/generic_api/greeter_client.cc @@ -0,0 +1,112 @@ +// Copyright 2023 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include +#include + +#include "absl/flags/flag.h" +#include "absl/flags/parse.h" +#include "absl/strings/str_format.h" + +#include +#include + +#ifdef BAZEL_BUILD +#include "examples/protos/helloworld.grpc.pb.h" +#else +#include "helloworld.grpc.pb.h" +#endif + +ABSL_FLAG(std::string, target, "localhost:50051", "Server address"); + +using grpc::Channel; +using grpc::ClientContext; +using grpc::Status; +using helloworld::Greeter; +using helloworld::HelloReply; +using helloworld::HelloRequest; + +using ProtoGenericStub = + ::grpc::TemplatedGenericStub<::google::protobuf::Message, + ::google::protobuf::Message>; + +class GreeterClient { + public: + GreeterClient(std::shared_ptr channel) + : stub_(new ProtoGenericStub(channel)) {} + + // Assembles the client's payload, sends it and prints the response back + // from the server. + void SayHello(const std::string& user) { + // Data we are sending to the server. + HelloRequest request; + request.set_name(user); + // Container for the data we expect from the server. + HelloReply reply; + // Context for the client. It could be used to convey extra information to + // the server and/or tweak certain RPC behaviors. + ClientContext context; + // The actual RPC. + std::mutex mu; + std::condition_variable cv; + bool done = false; + Status status; + std::cout << absl::StrFormat("### Send: SayHello(name=%s)", user) + << std::endl; + // Send a unary call using a generic stub. Unlike generated subs, + // this requires to specify the name of call. + stub_->UnaryCall(&context, "/helloworld.Greeter/SayHello", + grpc::StubOptions(), &request, &reply, [&](Status s) { + status = std::move(s); + std::lock_guard lock(mu); + done = true; + cv.notify_one(); + }); + std::unique_lock lock(mu); + while (!done) { + cv.wait(lock); + } + // Handles the reply + if (status.ok()) { + std::cout << absl::StrFormat("Ok. ReplyMessage=%s", reply.message()) + << std::endl; + } else { + std::cout << absl::StrFormat("Failed. Code=%d Message=%s", + status.error_code(), status.error_message()) + << std::endl; + } + } + + private: + // Instead of `Greeter::Stub`, it uses `ProtoGenericStub` to send any calls. + std::unique_ptr stub_; +}; + +int main(int argc, char** argv) { + absl::ParseCommandLine(argc, argv); + // Instantiate the client. It requires a channel, out of which the actual RPCs + // are created. This channel models a connection to an endpoint specified by + // the argument "--target=" which is the only expected argument. + std::string target_str = absl::GetFlag(FLAGS_target); + // We indicate that the channel isn't authenticated (use of + // InsecureChannelCredentials()). + GreeterClient greeter( + grpc::CreateChannel(target_str, grpc::InsecureChannelCredentials())); + greeter.SayHello("World"); + greeter.SayHello("gRPC"); + return 0; +} diff --git a/examples/cpp/generic_api/greeter_server.cc b/examples/cpp/generic_api/greeter_server.cc new file mode 100644 index 00000000000..76d4e39a2ba --- /dev/null +++ b/examples/cpp/generic_api/greeter_server.cc @@ -0,0 +1,143 @@ +// Copyright 2023 gRPC authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include + +#include "absl/flags/flag.h" +#include "absl/flags/parse.h" +#include "absl/strings/str_format.h" +#include "absl/synchronization/mutex.h" + +#include +#include + +#ifdef BAZEL_BUILD +#include "examples/protos/helloworld.grpc.pb.h" +#else +#include "helloworld.grpc.pb.h" +#endif + +ABSL_FLAG(uint16_t, port, 50051, "Server port for the service"); + +using grpc::ByteBuffer; +using grpc::CallbackGenericService; +using grpc::CallbackServerContext; +using grpc::GenericCallbackServerContext; +using grpc::ProtoBufferReader; +using grpc::ProtoBufferWriter; +using grpc::Server; +using grpc::ServerBuilder; +using grpc::ServerGenericBidiReactor; +using grpc::Status; +using grpc::StatusCode; +using helloworld::HelloReply; +using helloworld::HelloRequest; + +// Logic and data behind the server's behavior. +class GreeterServiceImpl final : public CallbackGenericService { + ServerGenericBidiReactor* CreateReactor( + GenericCallbackServerContext* context) override { + if (context->method() == "/helloworld.Greeter/SayHello") { + // Let the SayHello reactor handle this now on. + return new SayHelloReactor(); + } else { + // Forward this to the implementation of the base calss returning + // UNIMPLEMENTED. + return CallbackGenericService::CreateReactor(context); + } + } + + class SayHelloReactor : public ServerGenericBidiReactor { + public: + SayHelloReactor() { StartRead(&request_); } + + private: + Status OnSayHello(const HelloRequest& request, HelloReply* reply) { + if (request.name() == "") { + return Status(StatusCode::INVALID_ARGUMENT, "name is not specified"); + } + reply->set_message(absl::StrFormat("Hello %s", request.name())); + return Status::OK; + } + + void OnDone() override { delete this; } + void OnReadDone(bool ok) override { + if (!ok) { + return; + } + Status result; + // Deserialize a request message + HelloRequest request; + result = grpc::GenericDeserialize( + &request_, &request); + if (!result.ok()) { + Finish(result); + return; + } + // Call the SayHello handler + HelloReply reply; + result = OnSayHello(request, &reply); + if (!result.ok()) { + Finish(result); + return; + } + // Serialize a reply message + bool own_buffer; + result = grpc::GenericSerialize( + reply, &response_, &own_buffer); + if (!result.ok()) { + Finish(result); + return; + } + StartWrite(&response_); + } + void OnWriteDone(bool ok) override { + Finish(ok ? Status::OK + : Status(StatusCode::UNKNOWN, "Unexpected failure")); + } + ByteBuffer request_; + ByteBuffer response_; + }; + + private: + absl::Mutex mu_; +}; + +void RunServer(uint16_t port) { + std::string server_address = absl::StrFormat("0.0.0.0:%d", port); + GreeterServiceImpl service; + grpc::EnableDefaultHealthCheckService(true); + ServerBuilder builder; + // Listen on the given address without any authentication mechanism. + builder.AddListeningPort(server_address, grpc::InsecureServerCredentials()); + // Register "service" as the instance through which we'll communicate with + // clients. In this case it corresponds to an *synchronous* service. + builder.RegisterCallbackGenericService(&service); + // Finally assemble the server. + std::unique_ptr server(builder.BuildAndStart()); + std::cout << "Server listening on " << server_address << std::endl; + + // Wait for the server to shutdown. Note that some other thread must be + // responsible for shutting down the server for this call to ever return. + server->Wait(); +} + +int main(int argc, char** argv) { + absl::ParseCommandLine(argc, argv); + RunServer(absl::GetFlag(FLAGS_port)); + return 0; +}