文章目录
简单介绍这几个项目的关系
- ProtoBuf是一种序列化数据结构的协议
- protoc是一个能把proto数据结构转换为各种语言代码的工具
- RPC是一种通信协议
- gRPC是一种使用ProtoBuf作为接口描述语言的一个RPC实现方案
RPC
在分布式计算,远程过程调用(Remote Procedure Call,缩写为 RPC)是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一个地址空间(通常为一个开放网络的一台计算机)的子程序,而程序员就像调用本地程序一样,无需额外地为这个交互作用编程(无需关注细节)。RPC是一种服务器-客户端(Client/Server)模式,经典实现是一个通过发送请求-接受回应进行信息交互的系统。
RPC是一种进程间通信的模式,程序分布在不同的地址空间里。如果在同一主机里,RPC可以通过不同的虚拟地址空间(即便使用相同的物理地址)进行通讯,而在不同的主机间,则通过不同的物理地址进行交互。许多技术(常常是不兼容)都是基于这种概念而实现的。
流程如下:
- 客户端调用客户端stub(client stub)。这个调用是在本地,并将调用参数push到栈(stack)中。
- 客户端stub(client stub)将这些参数包装,并通过系统调用发送到服务端机器。打包的过程叫marshalling(如:XML、JSON、protobuf)
- 客户端本地操作系统发送信息至服务器。(可通过自定义TCP协议或HTTP/2传输)
- 服务器系统将信息传送至服务端stub(server stub)。
- 服务端stub(server stub)解析信息。该过程叫unmarshalling。
- 服务端stub(server stub)调用程序,并通过类似的方式返回给客户端。
ProtoBuf
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
标记一下,让使用者有个过渡时间。支持标注字段编号或者字段名字。 - 注释风格:支持C/C++风格
//
和/* ... */
message定义
以下是官方文档中的例子
- 规范的写法是每一个字段都应该制定它的规则(
required
,optional
,repeated
) - 每一个字段后面都会有一个数字(大于等于1),这个数字叫做分配字段编号(Assigning Field Numbers),它必须是一个唯一的数字,这些数字用于标识消息二进制格式的字段,一旦使用了消息类型,就不应更改这些数字。
- 我们可以通过
[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扩展
message的最后可以使用extensions
关键字保留的扩展字段编号,扩展的地方使用extend
声明同一个消息并进行扩展。
message Foo {
// ...
extensions 100 to 199;
}
extend Foo {
optional int32 bar = 126;
}
- 嵌套扩展
以下这种嵌套扩展bar属于Baz这个message
message Baz {
extend Foo {
optional int32 bar = 126;
}
...
}
Services定义
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);
}
protoc(Protocol Compiler)
要生成Java、Python、C ++、Go、Ruby、Objective-C或C#代码,您需要使用.proto文件中定义的消息类型,需要在.proto上运行协议缓冲区编译器协议。如果尚未安装编译器,请下载软件包并按照自述文件中的说明进行操作。对于Go,还需要为编译器安装一个特殊的代码生成器插件,请阅读Go Generated Code。
$ 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
protoc-gen-go
protocol buffer编译器需要一个插件来生成Go代码。使用Go 1.16或更高版本通过运行以下命令安装它:
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
通过在调用protoc
时传递go_opt
标志来提供特定于protoc-gen-go
的标志。可以传递多个go_opt
标志。例如,运行时:
$ protoc --proto_path=src --go_out=out --go_opt=paths=source_relative foo.proto bar/baz.proto
编译器将会从src
目录读取文件foo.proto
和bar/baz.proto
文件。生成文件会存放在out
目录中,生成的文件名是foo.pb.go
和bar/baz.pb.go
,它会主动创建向对应的子目录。
gRPC
gRPC(gRPC Remote Procedure Calls)是Google发起的一个开源远程过程调用(Remote procedure call)系统。该系统基于HTTP/2协议传输,使用Protocol Buffers作为接口描述语言。
其他功能:
- 认证(authentication)
- 双向流(bidirectional streaming)
- 流控制(flow control)
- 超时(timeouts)
可能的使用场景:
- 内部微服务之间的通信。
- 高数据负载(gRPC 使用协议缓冲区,其速度最高可比 REST 调用快七倍)。
- 您只需要一个简单的服务定义,不需要编写完整的客户端库。
- 在gRPC服务器中使用流式传输gRPC来构建响应更快的应用和 API。
编译gRPC(mac)
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
编译examples/cpp/helloworld
这里我们使用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
代码理解(examples/cpp/helloworld)
proto文件(helloworld.proto)
- proto文件中option选项的java_xx字段只用于Android平台,objc_xx字段只用于Mac和iOS。使用proto会生成这几个文件
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
生成的- protoc会自动生成message的实现,但是
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;
}
helloworld.grpc.pb.h(cc)文件
- 实现了
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
同步RPC例子greeter_client.cc和greeter_server.cc
- greeter_client.cc
// 客户端例子中它进一步封装了
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_server.cc
// 继承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;
}
};
异步RPC例子greeter_async_client.cc和greeter_async_server.cc
- greeter_async_client.cc
// 客户端例子中它进一步封装了
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_;
};
- greeter_async_server.cc
它这里也是做了一层封装,不是很好理解,因为它递归调用自己的次数比较多,实际上接口很简答的。
- 调用
Server
的RequestSayHello把Callback加入ServerCompletionQueue
中 - 然后调用
ServerCompletionQueue
的Next
(cq_->Next(&tag, &ok))接口等待客户端请求消息 - 当我们收到客户端请求的时候就会走到下一步,我们调用Callback对象的Proceed()接口去处理客户端请求,此时Callback对象的状态变为PROCESS,所以它会走PROCESS分支,服务端处理完之后调用
responder_.Finish
返回处理后的结果给到客户端。同时在一开始的时候我们又会创建一个新的Callback加入等待队列。 - 在调用
responder_.Finish
之前我们需要把状态修改为FINISH,因为responder_.Finish
也是异步的,所以此时我们又阻塞在cq_->Next(&tag, &ok)
位置,等待responder_.Finish
执行结束再次唤醒我们。 - 此时我们又接着执行把Callback对象的Proceed()接口,这次我们执行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_;
};