Protobuf 是谷歌开源的序列化和反序列化工具,比Json压缩效率更高。越来越广泛应用于RPC服务中。使用Protobuf定义的服务格式如下:
service FooService {
rpc GetFoo (GetFooRequest) returns (GetFooResponse);
}
message GetFooRequest {
string id = 1;
}
message GetFooResponse {
string fooName = 1;
int64 fooValue = 2;
}
下面是使用Protobuf定义gRPC服务时推荐使用的最佳实践。
1.命名一致性
RPC函数应该遵循一致的命名约定——这将使得查找函数更容易,因为您可以推断出您要查找的对象的名称。它还允许编写自我记录的代码——如果方法清楚地说明它们所做的事情,那么理解一个API就容易得多。
rpc GetFoo (GetFooRequest) returns (GetFooResponse);
rpc GetBar (GetBarRequest) returns (GetBarResponse);
rpc CreateOrUpdateFoo (CreateOrUpdateFooRequest)
returns (CreateOrUpdateFooResponse);
rpc CreateOrUpdateBar (CreateOrUpdateBarRequest)
returns (CreateOrUpdateBarResponse);
另一件要注意的事情是尽量避免根据消息所包含的内容来命名消息。这可能会导致一些问题,因为protobuf消息可能会随着时间的推移而改变。
message FooIdAndName { // bad
int64 id = 1;
string name = 2;
}
message FooObject { // better - at some point Foo will have more
int64 id = 1;
string name = 2;
}
2. 在API层确保消息的完整性
Protobuf 消息定义了结构化的对象。作为服务所有者,您可以确保正在接收的数据被解析为特定的结构。作为服务使用者,您可以确保响应以特定的结构返回,带有您期望的字段。
message GetFooRequest {
int64 fooId = 1;
}
message GetFooResponse {
string fooName = 1;
boolean active = 2;
repeated string tags = 3;
}
在上面的例子中,Service Owner相信GetFooRequest会带有一个64-bit 的整型字段“fooId”。而另一端Client知道它将收到一个GetFooResponse 包含特定字段和取值。
- Unique messages for RPC requests and responses
protobuf中的RPC定义接收请求并返回响应。跨多个RPC方法重用输入或输出消息可能很有吸引力,这可能会节省设置的时间,但是一定要记住,api可能会随时间变化,您可能不希望将两个单独的RPC调用紧密地耦合在一起。如果您最终跨RPC调用重用相同的消息对象,那么您很可能会处于这样一种状态:为一个调用设置了一些字段,而为另一个调用忽略了这些字段。这使得RPC客户的生活更加困难,因为他们需要考虑消息对象有哪些字段,以及需要在其上设置哪些字段。服务器也有一个更困难的工作,因为它需要知道忽略某些字段,例如,如果使用消息更新数据库中的一行。
// don't do this!
message CreateFoo (Foo) returns (Foo);
message SetFooName (Foo) returns (Foo);
message DeactivateFoo (Foo) returns (Foo);
message Foo {
int64 id = 1; // set when returned but ignored when creating
boolean fooActive = 2; // ignored in CreateFoo/DeactivateFoo
string name = 3; // ignored in DeactivateFoo
}
相反,如果您为每个RPC定义使用专门构建的消息,则可以随着时间独立地演进它们。
message CreateFoo (CreateFooRequest) returns (CreateFooResponse);
message SetFooName (SetFooNameRequest) returns (SetFooNameResponse);
message DeactivateFoo (DeactivateFooRequest)
returns (DeactivateFooResponse);
message CreateFooRequest {
string name = 1;
}
message CreateFooResponse {
int64 id = 1;
string name = 2;
bool active = 3;
}
message SetFooNameRequest {
int64 id = 1;
string name = 2;
}
message SetFooNameResponse {
}
message DeactivateFooRequest {
int64 id = 1;
}
message DeactivateFooResponse {
}
在上面的例子中,如果后来为了一个目的,需要DeactivateFoo 添加一个用户名的字段,那么在DeactivateFooRequest 中添加不会有任何问题,因为该请求就是为这个case量身定做的。
还有一点需要注意的是,保持message结构简洁。如果您的消息结构开始通过几个可选设置的字段进行演化,那么您应该停下来考虑是否需要通过单独的RPC调用提供功能,并使用它自己的专用消息定义。记录一个有很多可选字段的结构可能会有点棘手,因此很多时候您确实希望将功能分解为不同的RPC方法。
- 大方的使用Struct
Protobuf的一个优势是它在不破坏已有用户的前提下,可自由添加字段。这允许向现有RPC方法添加功能和可选特性,而不必重新实现该方法。尽管如此,为了能够利用这个特性,您必须小心地在希望添加字段的地方使用struct。
rpc GetFooHistory (GetFooHistoryRequest)
returns (GetFooHistoryResponse)
message GetFooHistoryResponse {
repeated FooHistoryResponseItem items = 1;
string cursor = 2;
}
message FooHistoryResponseItem {
int64 fooId;
}
在上面的例子中,我们对重复项使用了一个结构体,而不是普通的int64,因为这允许我们在将来向响应项列表中添加更多的字段。例如,如果我们想要向每个条目添加foo名称,我们可以简单地将它添加到FooHistoryResponseItem结构体中,它将与fooId一起可用。如果我们使用原始的重复int64,这将是不可能的。
另一个要点是,protobuf允许您使用google.protobuf.Empty来实现空的请求或响应。空的消息。虽然这在一开始看起来很方便,但是它把您置于一个在将来扩展RPC方法的困境中——您不能向它添加字段,所以您基本上只能在RPC方法的生命周期中使其为空。为了使您的服务定义经得起时间的检验,应该使用专门构建的消息,即使它一开始是空的。这为您在将来扩展它提供了灵活性。
- 不要让一个字段影响另一个字段的意思
Protocol buffer通常有多个字段。这些字段应该总是相互独立的—不应该有一个字段影响另一个字段的语义。
// don't do this!
message Foo {
int64 timestamp = 1;
bool timestampIsCreated; // true means timestamp is created time,
// false means that it is updated time
这导致了混乱——客户端现在需要特殊的逻辑来知道如何基于另一个字段来解释一个字段。相反,应该使用多个字段,或者使用protobuf “one of” 特性。
// better, but still not ideal because the fields are mutually
// exclusive - only one will be set
message Foo {
int64 createdTimestamp;
int64 updatedTimestamp;
}
// this is ideal; one will be set, and that is enforced by protobuf
message Foo {
oneof timestamp {
int64 created;
int64 updated;
}
}
- 枚举定义在起始位置处应该有一个“UNKNOWN”值
Protobuf不支持“空” Enum的概念——获取枚举字段的值总是会返回某个值,即使这个值从来都不是由调用方/接收方设置的。
// don't do this
enum ChangeType {
CREATE = 0;
UPDATE = 1;
DELETE = 2;
}
message Foo {
ChangeType changeType = 1;
}
采用上述例子,即使发送方没有对changeType进行赋值,那么接收方也无法知道,这个字段是空还是已经进行赋值。因此,为了可以让接收方可以判断是否为空,最好的实践是在0处设置“未定义”。
enum ChangeType {
UNKNOWN_CHANGE_TYPE = 0;
CREATE = 1;
UPDATE = 2;
DELETE = 3;
}
顺便说一下,protobuf的作用域规则规定enum常量值在protobuf文件中必须是唯一的,因此,这里使用 UNKNOWN_CHANGE_TYPE而不是 UNKNOWN.
网友评论