官网 https://grpc.io/
中文文档 http://doc.oschina.net/grpc/
环境搭建
安装依赖
1 apt install -y build-essential autoconf libtool pkg-config cmake
Clone gRPC repo
1 git clone --recurse-submodules -b v1.56.0 --depth 1 --shallow-submodules https://github.com/grpc/grpc
C++ 构建并安装gRPC和Protocol Buffers
1 2 3 4 5 6 7 8 9 10 11 12 13 export MY_INSTALL_DIR=$HOME/.local mkdir -p $MY_INSTALL_DIR export PATH="$MY_INSTALL_DIR/bin:$PATH" mkdir -p cmake/build pushd cmake/build cmake -DgRPC_INSTALL=ON \ -DgRPC_BUILD_TESTS=OFF \ -DCMAKE_INSTALL_PREFIX=$MY_INSTALL_DIR \ ../.. make -j 4 make install popd
这里我们安装了gRPC的头文件和库文件,以及一个工具protoc
。
Python 安装gRPC和gRPC工具(包含 protoc
)
1 2 pip install grpcio pip install grpcio-tools
定义接口和数据
.proto文件
.proto
文件用于定义数据(请求参数message和返回值message)和接口(service)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 syntax = "proto3"; // 表示这个文件遵循的proto语法的版本 package calc; // 对C++来说表示生成代码的namespace,如果不指定则为全局 // 定义服务接口 service Calc { rpc Add (AddRequest) returns (AddReply) {} } // 定义Add方法的请求参数 message AddRequest { int32 a = 1; int32 b = 2; } // 定义Add方法的返回数据 message AddReply { int32 sum = 1; }
从.proto文件生成代码
protoc
工具用于将使用ProtoBuf
定义的.proto
文件编译成特定编程语言的源代码。
.proto
文件描述了数据和接口的定义。
例如通过.proto生成C++代码的示例:
1 protoc --cpp_out=./output_directory ./protos/proto_file.proto
会在output_directory中生成一个protos文件夹,里面有.proto同名的*.pb.h
和*.pb.cc
文件。
这里有一个细节,生成protos文件夹是因为指定的.proto文件带有路径,生成的路径和指定.proto的路径相同,如果想把.h和.cc文件直接生成在指定的文件夹中,可以通过-I参数指定.proto文件的路径而不在指定.proto文件时携带路径。即
1 protoc --cpp_out=./output_directory -I./protos proto_file.proto
如果使用cmake构建项目,通常将这个生成过程写在CMakeList.txt中,用add_custom_command命令完成。
这里生成的.h和.cc中只包含了对数据(.proto中的message)的封装,而不包含接口(.proto中的service)的黏合剂,要生成service的代码需要利用grpc_cpp_plugin
,可以通过在protoc
命令中增加参数一并生成接口和数据的代码。
1 protoc --cpp_out=./output_directory --grpc_out=./output_directory --plugin=protoc-gen-grpc=/bin/grpc_cpp_plugin -I./protos proto_file.proto
会在output_directory中生成.proto同名的*.pb.h
和*.pb.cc
文件,以及*.grpc.pb.h
和*.grpc.pb.cc
,*.grpc.pb.*
里面就包含了根据service生成的代码。
Python版的protoc
是已模块的形式安装的,使用方法
1 python -m grpc_tools.protoc --python_out=./output_directory --pyi_out=./output_directory --grpc_out=./output_directory --plugin=protoc-gen-grpc=/bin/grpc_python_plugin -I./protos proto_file.proto
另外其实上面通过C源码编译安装的protoc工具也是支持生成python代码的,但python的protoc模块不能生成C 版本的代码。
项目文件存放结构
不是必须的,我感觉这样存放会比较方便,protos
和protos_gen
是客户端和服务器共用的,而且必须一致,所以只存统一的一份,这里的protos_gen
就是由protos
生成的意思也就是上面的output_directory
目录。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 . ├── calc_client │ ├── CMakeLists.txt │ └── src │ └── main.cpp ├── calc_server │ ├── CMakeLists.txt │ └── src │ └── main.cpp ├── protos │ └── calc.proto └── protos_gen ├── calc.grpc.pb.cc ├── calc.grpc.pb.h ├── calc.pb.cc └── calc.pb.h
CMake构建
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 cmake_minimum_required (VERSION 3.10 )project (calc_server CXX C)set (CMAKE_CXX_STANDARD 17 )set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17" )set (CMAKE_C_FLAGS_DEBUG " -std=c99 -g -ggdb -O0 -Wall -Wno-unused-function -fpic -fPIC -D_DEBUG" )set (CMAKE_CXX_FLAGS_DEBUG " -std=c++17 -g -ggdb -O0 -Wall -Wno-unused-function -fpic -fPIC -D_DEBUG" )set (CMAKE_C_FLAGS_RELEASE " -std=c99 -O3 -Wall -Wno-unused-function -fpic -fPIC" )set (CMAKE_CXX_FLAGS_RELEASE " -std=c++17 -O3 -Wall -Wno-unused-function -fpic -fPIC" )option (protobuf_MODULE_COMPATIBLE TRUE )find_package (Protobuf CONFIG REQUIRED)message (STATUS "Using protobuf ${Protobuf_VERSION}" )find_package (gRPC CONFIG REQUIRED)message (STATUS "Using gRPC ${gRPC_VERSION}" )include_directories ( /include include ../protos_gen ) link_directories ( /lib64 lib ) aux_source_directory (src SRC_LIST)aux_source_directory (../protos_gen PROTOBUF_SRC_LIST) add_executable (${PROJECT_NAME} ${SRC_LIST} ${PROTOBUF_SRC_LIST} ) target_link_libraries (${PROJECT_NAME} pthread absl::flags absl::flags_parse protobuf::libprotobuf gRPC::grpc++_reflection gRPC::grpc++ )
服务端和客户端的CMakeLists.txt
都可以套用上面的模板
cpp服务端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 #include <iostream> #include "absl/flags/flag.h" #include "absl/flags/parse.h" #include "absl/strings/str_format.h" #include <grpcpp/ext/proto_server_reflection_plugin.h> #include <grpcpp/grpcpp.h> #include <grpcpp/health_check_service_interface.h> #include "calc.grpc.pb.h" using namespace std;ABSL_FLAG (uint16_t , port, 50051 , "Server port for the service" );class CalcServiceImpl final : public calc::Calc::Service { grpc::Status Add (grpc::ServerContext* context, const calc::AddRequest* request, calc::AddReply* reply) override { auto a = request->a (); auto b = request->b (); auto sum = a + b; reply->set_sum (sum); return grpc::Status::OK; } }; int main (int argc, char ** argv) { absl::ParseCommandLine (argc, argv); uint16_t port = absl::GetFlag (FLAGS_port); std::string server_address = absl::StrFormat ("0.0.0.0:%d" , port); CalcServiceImpl service; grpc::EnableDefaultHealthCheckService (true ); grpc::reflection::InitProtoReflectionServerBuilderPlugin (); grpc::ServerBuilder builder; builder.AddListeningPort (server_address, grpc::InsecureServerCredentials ()); builder.RegisterService (&service); std::unique_ptr<grpc::Server> server (builder.BuildAndStart()) ; std::cout << "Server listening on " << server_address << std::endl; server->Wait (); return 0 ; }
在启动时可以指定端口号替换默认的端口号
1 ./calc-server --port=12345
cpp客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 #include <iostream> #include <memory> #include "absl/flags/flag.h" #include "absl/flags/parse.h" #include <grpcpp/grpcpp.h> #include "calc.grpc.pb.h" ABSL_FLAG (std::string, target, "localhost:50051" , "Server address" );using namespace std;class CalcClient {public : CalcClient (shared_ptr<grpc::Channel> channel) : _stub(calc::Calc::NewStub (channel)) {} int Add (int a, int b) { calc::AddRequest request; request.set_a (a); request.set_b (b); calc::AddReply reply; grpc::ClientContext context; grpc::Status status = _stub->Add (&context, request, &reply); if (status.ok ()) { return reply.sum (); } else { cout << status.error_code () << ": " << status.error_message () << endl; return 0 ; } } private : unique_ptr<calc::Calc::Stub> _stub; }; int main (int argc, char ** argv) { absl::ParseCommandLine (argc, argv); string target_str = absl::GetFlag (FLAGS_target); cout << "target_str=" << target_str << endl; CalcClient client (grpc::CreateChannel(target_str, grpc::InsecureChannelCredentials())) ; int a, b; while (cin >> a >> b) { auto sum = client.Add (a, b); cout << a << " + " << b << " = " << sum << endl; } return 0 ; }
同样地,在启动时可以指定服务器IP和端口号替换默认参数
1 ./calc-client --target=127.0.0.1:12345
python服务端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 from concurrent import futuresimport grpcimport syssys.path.append("../protos_gen" ) from calc_pb2 import AddReply from calc_pb2_grpc import CalcServicer, add_CalcServicer_to_server class Calc (CalcServicer ): def Add (self, request, context ): return AddReply(sum =request.a + request.b) def main (): port = '50051' server = grpc.server(futures.ThreadPoolExecutor(max_workers=10 )) add_CalcServicer_to_server(Calc(), server) server.add_insecure_port('0.0.0.0:' + port) server.start() print ("Server started, listening on " + port) server.wait_for_termination() if __name__ == '__main__' : main()
python客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import grpcimport syssys.path.append("../protos_gen" ) from calc_pb2 import AddRequest from calc_pb2_grpc import CalcStub class Calc : def __init__ (self, server ): self.channel = grpc.insecure_channel(server) self.stub = CalcStub(self.channel) def __del__ (self ): del self.stub self.channel.close() def Add (self, **kw ): response = self.stub.Add(AddRequest(**kw)) return response.sum def main (): client = Calc(server='localhost:50051' ) s = client.Add(a=1 , b=2 ) print (f"1+2={s} " ) if __name__ == '__main__' : main()