概述
grpc
grpc是Google的rpc开源框架,它使用了protocol buffer(Google的另一个开源工具)作为接口定义语言和消息传递格式。
大概的使用方法是:
- 使用protocol buffer语言编写接口定义。
- 使用protocol buffer工具将接口编译成目标语言代码,包括服务端的接口框架和客户端的接口stub定义,数据的序列化和反序列化过程等通用代码。
- 开发人员在服务端编写接口实现。
- 开发人员在客户端调用接口函数。
grpc允许定义4中接口函数:
//一个参数和一个返回值:
rpc SayHello(HelloRequest) returns (HelloResponse){
}
//一个参数和流式返回
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse){
}
//流式参数和一个返回
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse) {
}
//流式参数和流式返回
rpc LotsOfGreetings(stream HelloRequest) returns (stream HelloResponse) {
}
一个例子:
syntax = "proto3";
service RouteGuide{
rpc GetFeature(Point) returns (Feature) {}
rpc ListFeatures(Rectangle) returns (stream Feature) {}
rpc RecordRoute(stream Point) returns (RouteSummary) {}
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
}
这样一个文件,使用protoc 编译以后生成的go语言的服务端接口代码:
// RouteGuideServer is the server API for RouteGuide service.
type RouteGuideServer interface {
GetFeature(context.Context, *Point) (*Feature, error)
ListFeatures(*Rectangle, RouteGuide_ListFeaturesServer) error
RecordRoute(RouteGuide_RecordRouteServer) error
RouteChat(RouteGuide_RouteChatServer) error
}
GetFeature是一个接受一个Point返回一个Feature值的普通接口,使用pb生成的函数拥有两个参数,context和*Point,其中context是为了同步调用的cancel功能实现。返回值有两个一个是定义的返回值*Feature,另外一个是error,这个error是用于返回grpc调用错误的,所有的grpc生成接口函数都有。
ListFeatures是接受一个Rectangle,返回一个Feature流的接口,使用pb生成的接口将返回的流使用参数传入,也就是*Rectangle是原来的参数,RouteGuide_ListFeaturesServer用于返回流数据。函数的返回值依旧是一个error。
type RouteGuide_ListFeaturesServer interface {
Send(*Feature) error
grpc.ServerStream
}
RecordRoute是一个接受一个流,返回一个值。pb生成的接口将返回值和流合并到一个接口中,返回值变成一个error。
type RouteGuide_RecordRouteServer interface {
SendAndClose(*RouteSummary) error
Recv() (*Point, error)
grpc.ServerStream
}
RouteChat是一个双向流,pb生成接口同样合并为一个参数,返回error·。
type RouteGuide_RouteChatServer interface {
Send(*RouteNote) error
Recv() (*RouteNote, error)
grpc.ServerStream
}
这些pb生成的接口实际上是封装了grpc.ServerStream的流发送和接受, grpc.ServerStream更底层的实现这里不讨论。
type RouteGuide_ListFeaturesServer interface {
Send(*Feature) error
grpc.ServerStream
}
type routeGuideListFeaturesServer struct {
grpc.ServerStream
}
func (x *routeGuideListFeaturesServer) Send(m *Feature) error {
return x.ServerStream.SendMsg(m)
}
服务端接口的实现
要自己定义一个结构,实现pb生成的接口,执行真正的服务端逻辑。
非流式函数的实现:
// GetFeature returns the feature at the given point.
func (s *routeGuideServer) GetFeature(ctx context.Context, point *pb.Point) (*pb.Feature, error) {
for _, feature := range s.savedFeatures {
if proto.Equal(feature.Location, point) {
return feature, nil
}
}
// No feature was found, return an unnamed feature
return &pb.Feature{Location: point}, nil
}
Chat双向流函数的实现:
// RouteChat receives a stream of message/location pairs, and responds with a stream of all
// previous messages at each of those locations.
func (s *routeGuideServer) RouteChat(stream pb.RouteGuide_RouteChatServer) error {
for {
in, err := stream.Recv()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
key := serialize(in.Location)
s.mu.Lock()
s.routeNotes[key] = append(s.routeNotes[key], in)
// Note: this copy prevents blocking other clients while serving this one.
// We don't need to do a deep copy, because elements in the slice are
// insert-only and never modified.
rn := make([]*pb.RouteNote, len(s.routeNotes[key]))
copy(rn, s.routeNotes[key])
s.mu.Unlock()
for _, note := range rn {
if err := stream.Send(note); err != nil {
return err
}
}
}
}
实现了接口之后,需要启动一个TCP服务,监听服务的端口:
flag.Parse()
lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", *port))
...
grpcServer := grpc.NewServer(opts...)
pb.RegisterRouteGuideServer(grpcServer, newServer())
grpcServer.Serve(lis)
pb生成的客户端接口:
type RouteGuideClient interface {
GetFeature(ctx context.Context, in *Point, opts ...grpc.CallOption) (*Feature, error)
ListFeatures(ctx context.Context, in *Rectangle, opts ...grpc.CallOption) (RouteGuide_ListFeaturesClient, error)
RecordRoute(ctx context.Context, opts ...grpc.CallOption) (RouteGuide_RecordRouteClient, error)
RouteChat(ctx context.Context, opts ...grpc.CallOption) (RouteGuide_RouteChatClient, error)
}
客户端生成的函数每一个都有一个context,还有一个或者多个callOptions。context是控制grpc调用的cancel和超时的,callOption是一个接口:
type CallOption interface {
before(*callInfo) error
after(*callInfo)
}
可以设置在grpc调用之前或者之后的动作,这个动作接受一个叫callInfo的参数,这个参数有一些字段,用于配置执行的动作,最常用的之一就是设置https的证书。
// callInfo contains all related configuration and information about an RPC.
type callInfo struct {
compressorType string
failFast bool
stream ClientStream
maxReceiveMessageSize *int
maxSendMessageSize *int
creds credentials.PerRPCCredentials
contentSubtype string
codec baseCodec
maxRetryRPCBufferSize int
}
另外,pb生成的客户端将流式的结构都放到了返回值中,也就是说,客户端调用grpc返回一个包含流的接口,客户端后续可以使用这个接口发送或者接受数据。
stream, err := client.RecordRoute(ctx)
if err != nil {
log.Fatalf("%v.RecordRoute(_) = _, %v", client, err)
}
for _, point := range points {
if err := stream.Send(point); err != nil {
log.Fatalf("%v.Send(%v) = %v", stream, point, err)
}
}
reply, err := stream.CloseAndRecv()
...
参考文献:
gRPC Basics - Go
gRPC Concepts
proto3
proto3语法
定义一个Message
syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
第一行必须是syntax = “proto3”;否则编译器会认为是proto2语法
字段的序号,也就是最后=的数字是和序列化相关的,一旦接口发布出去,就不能改变。如果后续版本想删除这个字段或者重命名这个字段,需要使用reserved将废除的字段和序号保留起来,以免被误作它用。
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
如果重新利用序号或者字段名称,会造成版本不兼容,引起严重程序错误或者Bug
后续版本增加新的字段不影响之前的版本使用,新的字段在旧的客户端不会被解析,旧的客户端发送的数据新字段使用默认值。
序号19000到19999是proto3的保留字段,不能使用。
proto3的字段类型定义和go语言的类型定义吻合的很好,对于其他语言的类型对应关系,参见:https://developers.google.com/protocol-buffers/docs/proto3
默认值
string 空字符串
bytes 空字节串
bool false
数字类型 0
枚举 0值枚举项,每一个枚举必须定义一个0值项
自定义的message,和语言实现有关
枚举的定义
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
Corpus corpus = 4;
}
枚举可以定义在最外层,也可以嵌套在message中,第一个枚举值必须是0.
枚举也可以使用reserved保留序号和字段。
默认的枚举的两个项不能有相同的值,但是可以通过指定allow_alias改变
enum EnumAllowingAlias {
option allow_alias = true;
UNKNOWN = 0;
STARTED = 1;
RUNNING = 1;
}
enum EnumNotAllowingAlias {
UNKNOWN = 0;
STARTED = 1;
// RUNNING = 1; // Uncommenting this line will cause a compile error inside Google and a warning message outside.
}
message中可以使用其他message类型,如果其他message类型不在本proto文件中定义,可以使用import将其他proto文件导入。
proto文件的导入只能使用直接定义的类型,导入文件的导入文件类型不能被使用,这个规定造成一个麻烦就是如果很多文件引用了同一个文件,如果想移动这个文件的位置,就要到所有引用这个文件的文件中修改路径。proto给出一个解决方案,public导入
// new.proto
// All definitions are moved here
// old.proto
// This is the proto that all clients are importing.
import public "new.proto";
import "other.proto";
// client.proto
import "old.proto";
// You use definitions from old.proto and new.proto, but not other.proto
导入文件的寻找方式
编译器protoc 的-I 参数指定了一系列路径,按照指定的顺序寻找。一般指定-I为工程的根路径。
message升级注意事项
- 不要改变任何已经存在的字段
- 增加新字段不影响兼容性,只要处理好字段的default value。
- 对于不想让继续使用的字段,使用reserved保留起来
- int32, uint32, int64, uint64, bool是兼容的,但是会发生截断和溢出等现象。
- sint32,sint64是兼容的,但是不与其他类型的整数兼容
- string和bytes是兼容的,条件是bytes保存的是utf-8编码的字符
- bytes可以和嵌入的message兼容,条件是bytes是那个类型的message序列化后的二进制数据
- fixed32和sfixed32兼容,fixed64和sfixed64兼容
- enum和int32,int64,uint32,uint64兼容,但是如果给enum赋值了一个没有定义的整数,处理的方式和语言实现相关
- 将一个字段变成一个新的oneof是二进制兼容的,将多个字段放入一个新的oneof可能是安全的,要求这多个字段一次只能赋值一个,将任何字段移入已经存在的oneof是二进制不兼容的。
Unknown Field
解析不了的字段都是Unknown Field,比如旧的版本解析新的二进制流,那些新定义的字段就是Unknown Field。在pb3.5版本以前,Unknown Field就直接被扔掉了,在3.5之后,Unknown Field会被保留。
被保留有啥用?
Any
Any有点像C语言中的void *,当不知道内嵌的message是啥类型的时候,可以使用Any。Any中保存的是bytes。解析Any需要使用运行时方法判断类型,然后解析。
import "google/protobuf/any.proto";
message ErrorStatus {
string message = 1;
repeated google.protobuf.Any details = 2;
}
// Storing an arbitrary message type in Any.
NetworkErrorDetails details = ...;
ErrorStatus status;
status.add_details()->PackFrom(details);
// Reading an arbitrary message from Any.
ErrorStatus status = ...;
for (const Any& detail : status.details()) {
if (detail.Is<NetworkErrorDetails>()) {
NetworkErrorDetails network_error;
detail.UnpackTo(&network_error);
... processing network_error ...
}
}
使用Any需要import
google/protobuf/any.proto
Oneof
Oneof 有点像C语言中的union,多个字段共享内存。如果给多个字段设置了值,会保留最后一个设置。
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
Oneof中不能添加repeated类型的字段。解析Oneof字段不同的语言有不同的API。
Map
map<key_type, value_type> map_field = N;
key_type可以是整型或者字符类型,value_type不能是另外一个map,其他类型都可以。
Map类型不能使用repeated。
如果只提供了key,没有提供value,有些语言会用默认值初始化value,有些语言不会序列化这个key。
对于没有实现Map的语言,Map会被序列化成一个键值对的列表。
Packages
可以将proto文件按照package组织。
package foo.bar;
message Open { ... }
message Foo {
...
foo.bar.Open open = 1;
...
}
pb的package定义会影响编译出来的代码。比如C++会形成相应的命名空间,Java和Go会形成相应的包。
JSON Mapping
如果一个value在JSON中是null,被转化成pb的时候,使用默认值;如果一个value在pb中是默认值,被转化成JSON的时候key会被删除掉,这可能引起麻烦,但是是可以配置的。
转化规则
message被转化成为JSON对象。
enum被转化为string,对应的value的名字,JSON转化成pb的时候可以用名字也可以用整数。
map<K,V> key和value都转化成字符串形式。
repeated V转化成[v, …]
bool转化成true或者false
string就是string
bytes转化成base64的string。
int32,fixed32,uint32转化成整数。
int64,fixed64,uint64转化成字符串。
float,double转化成浮点数,或者"Nan", “Infinity”
Any如果value是一个值转化为{"@type": xxx, "value": yyy}
如果value是一个对象,转化为一个对象。@type在对象里边,如此递归。
Timestamp转化为字符串,格式为"1972-01-01T10:00:20.021Z"
Duration 转化为string,格式为"1.000340012s", "1s"
文档中剩下的类型没有使用过,参见https://developers.google.com/protocol-buffers/docs/proto3#json
Options
java_package,用于生成Java包,如果没有这个option,默认使用proto的package路径,但是它一般不是逆序的url格式,所有不符合java的规范。
java_multiple_files,表示文件中最外层的messag,enum等都是包级别的定义,而不是在包名命名的class中。
optimize_for,优化级别,可以有SPEED,CODE_SIZE,LITE_RUNTIME选项。默认是SPEED。
objc_class_prefix,Objective-C class的前缀。
代码生成
--proto_path=IMPORT_PATH
指定了.proto文件的搜索路径,如果没有指定,从当前路径开始搜索。可以指定多个,会按顺序搜索。-I是简洁的写法。
--go_out=DST_DIR
制定了生成的go代码的位置。如果DST_DIR是以.zip结尾的,pb编译器会自动打包压缩成zip。.jar也是如此。
最后
以上就是顺心大象为你收集整理的grpc和protocol buffer使用的全部内容,希望文章能够帮你解决grpc和protocol buffer使用所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复