在分布式计算,远程过程调用(Remote Procedure Call,缩写为 RPC)是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一个地址空间(通常为一个开放网络的一台计算机)的子程序,而程序员就像调用本地程序一样,无需额外地为这个交互作用编程(无需关注细节)。RPC是一种服务器-客户端(Client/Server)模式,经典实现是一个通过发送请求-接受回应进行信息交互的系统。
RPC是一种进程间通信的模式,程序分布在不同的地址空间里。如果在同一主机里,RPC可以通过不同的虚拟地址空间(即便使用相同的物理地址)进行通讯,而在不同的主机间,则通过不同的物理地址进行交互。许多技术(常常是不兼容)都是基于这种概念而实现的。
流程如下:
Protocol Buffers(简称ProtoBuf)是一种序列化数据结构的协议。对于透过管道(pipeline)或存储资料进行通信的程序开发上是很有用的。这个方法包含一个接口描述语言,描述一些数据结构,并提供程序工具根据这些>描述产生代码,用于将这些数据结构产生或解析资料流。官方文档-Language Guide (proto3)
double
, float
, int32
, int64
, uint32
, uint64
, sint32
, sint64
, fixed32
, fixed64
, sfixed32
, sfixed64
, bool
, string
, bytes
。数字类型(double
, float
, int32
, int64
…)默认值是0,string
类型默认值是空字符,bytes默认值是空byte,bool类型的默认值是false,enum类型的默认值是枚举列表中的一个值。required
, 可选optional
, 重复repeated
。由于历史原因repeated
字段可能不能有效的编码,所以我们需要字段的后面添加一个选项[packed=true]
reserved
,由于消息更新当某些字段到xx版本可能要去掉,在去掉之前的应该使用reserved
标记一下,让使用者有个过渡时间。支持标注字段编号或者字段名字。//
和/* ... */
以下是官方文档中的例子
required
, optional
, repeated
)[default = xx]
给字段设置一个默认值message SearchRequest {
required string query = 1;
optional int32 page_number = 2;
optional int32 result_per_page = 3 [default = 10];
enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
optional Corpus corpus = 4 [default = UNIVERSAL];
}
message Outer { // Level 0
message MiddleAA { // Level 1
message Inner { // Level 2
required int64 ival = 1;
optional bool booly = 2;
}
}
message MiddleBB { // Level 1
message Inner { // Level 2
required int32 ival = 1;
optional bool booly = 2;
}
}
}
message的最后可以使用extensions
关键字保留的扩展字段编号,扩展的地方使用extend
声明同一个消息并进行扩展。
message Foo {
// ...
extensions 100 to 199;
}
extend Foo {
optional int32 bar = 126;
}
message Baz {
extend Foo {
optional int32 bar = 126;
}
...
}
ProtoBuf还支持定义RPC服务,在这个例子中SearchService是一个RPC服务,Search是服务提供的方法,这个方法的请求参数是SearchRequest,返回值是SearchResponse。
protoc
会为我们生成一个SearchService接口和一个对应的stub
实现。stub
将所有调用转发到RpcChannel
,RpcChannel
是一个抽象接口,需要实现消息序列化和数据传输。在这个例子中也就是这样调用SearchService::NewStub(new MyRpcChannel())->Search
。Service
接口,在这个例子中也就是继承SearchService
并实现Search
接口。service SearchService {
rpc Search(SearchRequest) returns (SearchResponse);
}
要生成Java、Python、C ++、Go、Ruby、Objective-C或C#代码,您需要使用.proto文件中定义的消息类型,需要在.proto上运行协议缓冲区编译器协议。如果尚未安装编译器,请下载软件包并按照自述文件中的说明进行操作。对于Go,还需要为编译器安装一个特殊的代码生成器插件,可以在GitHub上的golang /protobuf
存储库中找到此代码和安装说明。
$ protoc [OPTION] PROTO_FILES
OPTION | 说明 |
---|---|
-IPATH, --proto_path=PATH | 指定imports的搜索目录,如果不指定则默认使用当前目录 |
–plugin=EXECUTABLE | 指定插件 |
–cpp_out=OUT_DIR | 指定生成C++头文件和源文件的目录 |
–csharp_out=OUT_DIR | 指定生成C#源文件的目录 |
–java_out=OUT_DIR | 指定生成Java源文件的目录 |
–js_out=OUT_DIR | 指定生成JavaScript源文件的目录 |
–objc_out=OUT_DIR | 指定生成Objective-C头文件和源文件的目录 |
–php_out=OUT_DIR | 指定生成PHP头文件和源文件的目录 |
–python_out=OUT_DIR | 指定生成Python源文件的目录 |
–ruby_out=OUT_DIR | 指定生成Ruby源文件的目录 |
$ mkdir out
$ protoc --cpp_out=out helloworld.proto route_guide.proto
gRPC(gRPC Remote Procedure Calls)是Google发起的一个开源远程过程调用(Remote procedure call)系统。该系统基于HTTP/2协议传输,使用Protocol Buffers作为接口描述语言。
其他功能:
可能的使用场景:
grpc支持的构建工具有makefile、cmake、ninja(gn)、bazel等,其中makefile不支持生成plugin,因为我这里仅仅是为了跑它的测试程序用于学习的,所以就使用cmake了。请阅读官方文档详细了解他的构建。
# 下载代码
$ git clone -b RELEASE_TAG_HERE https://github.com/grpc/grpc
$ cd grpc
$ git submodule update --init
# 编译
$ mkdir out && cd out
$ cmake .. -DgRPC_INSTALL=ON && make -j8 && make install
这里我们使用Makefile编译,直接执行编译会遇到如下编译和链接错误,这里是因为没有指定absl的搜索路径和没有链接CoreFoundation导致的
/usr/local/include/grpcpp/impl/codegen/sync.h:35:10: fatal error: 'absl/synchronization/mutex.h' file not found
#include "absl/synchronization/mutex.h"
Undefined symbols for architecture x86_64:
"_CFRelease", referenced from:
absl::lts_2020_09_23::time_internal::cctz::local_time_zone() in libabsl_time_zone.a(time_zone_lookup.cc.o)
修改Makefile如下:
index 8030ba2be2..de1fd6fb7f 100644
--- a/examples/cpp/helloworld/Makefile
+++ b/examples/cpp/helloworld/Makefile
@@ -17,13 +17,14 @@
HOST_SYSTEM = $(shell uname | cut -f 1 -d_)
SYSTEM ?= $(HOST_SYSTEM)
CXX = g++
-CPPFLAGS += `pkg-config --cflags protobuf grpc`
+CPPFLAGS += `pkg-config --cflags protobuf grpc` -I../../../third_party/abseil-cpp
CXXFLAGS += -std=c++11
ifeq ($(SYSTEM),Darwin)
LDFLAGS += -L/usr/local/lib `pkg-config --libs protobuf grpc++`\
-pthread\
-lgrpc++_reflection\
- -ldl
+ -ldl\
+ -framework CoreFoundation -labsl_time_zone
else
LDFLAGS += -L/usr/local/lib `pkg-config --libs protobuf grpc++`\
-pthread\
编译步骤如下
$ cd examples/cpp/helloworld
$ make -j8
运行,开两个终端一个先运行服务端greeter_server
,另一个运行greeter_client
会打印Greeter received: Hello world
helloworld.grpc.pb.cc
, helloworld.grpc.pb.h
, helloworld.pb.cc
, helloworld.pb.h
。helloworld.pb.cc
和 helloworld.pb.h
是protoc生成的helloworld.grpc.pb.cc
和helloworld.grpc.pb.h
是grpc_cpp_plugin
生成的RpcChannel
和Server
需要我们自己实现,这里gRPC帮我们做了序列化和传输相关的工作。syntax = "proto3";
option java_multiple_files = true;
option java_package = "io.grpc.examples.helloworld";
option java_outer_classname = "HelloWorldProto";
option objc_class_prefix = "HLW";
package helloworld;
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}
Greeter::Stub
,并实现了SayHello(同步)
和PrepareAsyncSayHello(异步)
函数,客户端可以直接调用Greeter::Stub
对象的SayHello或者PrepareAsyncSayHello方法Greeter::Service(同步)
和Greeter::AsyncService(异步)
类,服务端需要继承其中一个Service并实现里面的抽象函数SayHello
就好了namespace helloworld {
// The greeting service definition.
class Greeter final {
public:
static constexpr char const* service_full_name() {
return "helloworld.Greeter";
}
class Stub final : public StubInterface {
public:
Stub(const std::shared_ptr< ::grpc::ChannelInterface>& channel){
}
::grpc::Status SayHello(::grpc::ClientContext* context, const ::helloworld::HelloRequest& request, ::helloworld::HelloReply* response) override;
std::unique_ptr< ::grpc::ClientAsyncResponseReader< ::helloworld::HelloReply>> PrepareAsyncSayHello(::grpc::ClientContext* context, const ::helloworld::HelloRequest& request, ::grpc::CompletionQueue* cq) {
return std::unique_ptr< ::grpc::ClientAsyncResponseReader< ::helloworld::HelloReply>>(PrepareAsyncSayHelloRaw(context, request, cq));
}
class experimental_async final : public StubInterface::experimental_async_interface {
public:
void SayHello(::grpc::ClientContext* context, const ::helloworld::HelloRequest* request, ::helloworld::HelloReply* response, std::function<void(::grpc::Status)>) override;
};
class experimental_async_interface* experimental_async() override {
return &async_stub_; }
private:
std::shared_ptr< ::grpc::ChannelInterface> channel_;
class experimental_async async_stub_{
this};
const ::grpc::internal::RpcMethod rpcmethod_SayHello_;
};
...
class Service : public ::grpc::Service {
public:
Service();
virtual ~Service();
// Sends a greeting
virtual ::grpc::Status SayHello(::grpc::ServerContext* context, const ::helloworld::HelloRequest* request, ::helloworld::HelloReply* response);
};
template <class BaseClass>
class WithAsyncMethod_SayHello : public BaseClass {
// disable synchronous version of this method
::grpc::Status SayHello(::grpc::ServerContext* /*context*/, const ::helloworld::HelloRequest* /*request*/, ::helloworld::HelloReply* /*response*/) override {
abort();
return ::grpc::Status(::grpc::StatusCode::UNIMPLEMENTED, "");
}
void RequestSayHello(::grpc::ServerContext* context, ::helloworld::HelloRequest* request, ::grpc::ServerAsyncResponseWriter< ::helloworld::HelloReply>* response, ::grpc::CompletionQueue* new_call_cq, ::grpc::ServerCompletionQueue* notification_cq, void *tag) {
::grpc::Service::RequestAsyncUnary(0, context, request, response, new_call_cq, notification_cq, tag);
}
};
typedef WithAsyncMethod_SayHello<Service > AsyncService;
...
} // end namespace helloworld
客户端例子中它进一步封装了
class GreeterClient {
public:
GreeterClient(std::shared_ptr<Channel> channel) : stub_(Greeter::NewStub(channel)) {
}
std::string SayHello(const std::string& user) {
// 要发送给服务端的数据
HelloRequest request;
request.set_name(user);
// 从服务端接收到的数据
HelloReply reply;
// 一个客户端上下文,暂时不深究
ClientContext context;
// RPC操作
Status status = stub_->SayHello(&context, request, &reply);
// 检查状态,并返回
if (status.ok()) {
return reply.message(); } else {
return "RPC failed"; }
}
private:
std::unique_ptr<Greeter::Stub> stub_;
};
// 继承Greeter::Service,并重写SayHello方法
class GreeterServiceImpl final : public Greeter::Service {
Status SayHello(ServerContext* context, const HelloRequest* request, HelloReply* reply) override {
std::string prefix("Hello ");
reply->set_message(prefix + request->name());
return Status::OK;
}
};
客户端例子中它进一步封装了
class GreeterClient {
public:
explicit GreeterClient(std::shared_ptr<Channel> channel) : stub_(Greeter::NewStub(channel)) {
}
std::string SayHello(const std::string& user) {
// 要发送给服务端的数据
HelloRequest request;
request.set_name(user);
// 从服务端接收到的数据
HelloReply reply;
// 一个客户端上下文,暂时不深究
ClientContext context;
// 我们用来与gRPC运行时异步通信的生产者-消费者队列
CompletionQueue cq;
// Storage for the status of the RPC upon completion.
Status status;
// 调用stub_->PrepareAsyncSayHello() 会返回一个RPC对象,暂时不深究这个对象
std::unique_ptr<ClientAsyncResponseReader<HelloReply> > rpc(stub_->PrepareAsyncSayHello(&context, request, &cq));
// 初始化RPC调用
rpc->StartCall();
// 这里是真正的执行,reply是我们期待的返回值,status是返回状态,1是一个tag(我们通过tag判断是哪个调用返回的值)
rpc->Finish(&reply, &status, (void*)1);
void* got_tag;
bool ok = false;
// 会一直阻塞等待rpc返回,我们通过tag区分是哪一个调用
GPR_ASSERT(cq.Next(&got_tag, &ok));
// 在这个例子中我们只有一个调用,这个tag是1,这个这个tag一定是1,如果不是1就直接assert掉
GPR_ASSERT(got_tag == (void*)1);
// 检查状态,并返回
if (status.ok()) {
return reply.message(); } else {
return "RPC failed"; }
}
private:
std::unique_ptr<Greeter::Stub> stub_;
};
Server
的RequestSayHello把Callback加入ServerCompletionQueue
中ServerCompletionQueue
的Next
(cq_->Next(&tag, &ok))接口等待客户端请求消息responder_.Finish
返回处理后的结果给到客户端。同时在一开始的时候我们又会创建一个新的Callback加入等待队列。responder_.Finish
之前我们需要把状态修改为FINISH,因为responder_.Finish
也是异步的,所以此时我们又阻塞在cq_->Next(&tag, &ok)
位置,等待responder_.Finish
执行结束再次唤醒我们。class ServerImpl final {
public:
~ServerImpl() {
server_->Shutdown(); cq_->Shutdown(); }
// This can be run in multiple threads if needed.
void Run() {
new CallData(&service_, cq_.get());
void* tag; // uniquely identifies a request.
bool ok;
while (true) {
GPR_ASSERT(cq_->Next(&tag, &ok));
GPR_ASSERT(ok);
static_cast<CallData*>(tag)->Proceed();
}
}
private:
// Class encompasing the state and logic needed to serve a request.
class CallData {
public:
CallData(Greeter::AsyncService* service, ServerCompletionQueue* cq) : service_(service), cq_(cq), responder_(&ctx_), status_(CREATE) {
Proceed();
}
void Proceed() {
if (status_ == CREATE) {
status_ = PROCESS;
service_->RequestSayHello(&ctx_, &request_, &responder_, cq_, cq_, this);
} else if (status_ == PROCESS) {
new CallData(service_, cq_);
std::string prefix("Hello ");
reply_.set_message(prefix + request_.name());
status_ = FINISH;
responder_.Finish(reply_, Status::OK, this);
} else {
GPR_ASSERT(status_ == FINISH);
delete this;
}
}
private:
Greeter::AsyncService* service_;
ServerCompletionQueue* cq_;
ServerContext ctx_;
HelloRequest request_;
HelloReply reply_;
ServerAsyncResponseWriter<HelloReply> responder_;
enum CallStatus {
CREATE, PROCESS, FINISH };
CallStatus status_; // The current serving state.
};
std::unique_ptr<ServerCompletionQueue> cq_;
Greeter::AsyncService service_;
std::unique_ptr<Server> server_;
};