grpc和protocol buffer使用

grpc

grpc是Google的rpc开源框架,它使用了protocol buffer(Google的另一个开源工具)作为接口定义语言和消息传递格式。

大概的使用方法是:

  1. 使用protocol buffer语言编写接口定义。
  2. 使用protocol buffer工具将接口编译成目标语言代码,包括服务端的接口框架和客户端的接口stub定义,数据的序列化和反序列化过程等通用代码。
  3. 开发人员在服务端编写接口实现。
  4. 开发人员在客户端调用接口函数。
    grpc允许定义4中接口函数:
//一个参数和一个返回值:
rpc SayHello(HelloRequest) returns (HelloResponse){
}
//一个参数和流式返回
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse){
}
//流式参数和一个返回
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse) {
}
//流式参数和流式返回
rpc LotsOfGreetings(stream HelloRequest) returns (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 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 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也是如此。

你可能感兴趣的:(go语言,后端)