分类:proto| 发布时间:2025-01-10 08:55:00
出乎意料的是,要打造一个面向未来的 API 非常困难。 本文档中的建议权衡利弊,以利于长期、无错误的演进。
针对 proto3 进行了更新。欢迎进行修补!
本文档是 Proto 最佳实践 的补充。 它不是 Java/C++/Go 和其他 API 的解决方案。
如果您在代码审查中发现 proto 偏离了这些准则,请让作者关注此主题并帮助传播。
注意 这些准则仅供参考,许多准则都有记录在案的例外情况。 例如,如果您正在编写性能至关重要的后端,则可能需要牺牲灵活性或安全性来提高速度。 本主题将帮助您更好地了解权衡利弊并做出适合您情况的决定。
您的原型很有可能会被那些不知道您在编写或修改时在想什么的人继承和使用。 记录每个字段的方式对对您的系统知之甚少的新团队成员或客户有用。
一些具体的例子:
// Bad: Option to enable Foo
// Good: Configuration controlling the behavior of the Foo feature.
message FeatureFooConfig {
// Bad: Sets whether the feature is enabled
// Good: Required field indicating whether the Foo feature
// is enabled for account_id. Must be false if account_id's
// FOO_OPTIN Gaia bit is not set.
optional bool enabled;
}
// Bad: Foo object.
// Good: Client-facing representation of a Foo (what/foo) exposed in APIs.
message Foo {
// Bad: Title of the foo.
// Good: Indicates the user-supplied title of this Foo, with no
// normalization or escaping.
// An example title: "Picture of my cat in a box <3 <3 !!!"
optional string title [(max_length) = 512];
}
// Bad: Foo config.
// Less-Bad: If the most useful comment is re-stating the name, better to omit
// the comment.
FooConfig foo_config = 3;
用尽可能少的文字记录每个字段的约束、期望和解释。
您可以使用自定义 proto 注释。 请参阅自定义选项来定义跨语言常量,如上例中的 max_length。 proto2 和 proto3 均支持。
随着时间的推移,接口的文档会越来越长。篇幅过长会影响清晰度。 如果文档确实不清楚,请进行修复,但要从整体上看待它并力求简洁。
如果您向客户端公开的顶层 proto 与您存储在磁盘上的原型相同,那么您将遇到麻烦。 随着时间的推移,越来越多的二进制文件将依赖于您的 API,这使得更改变得更加困难。 您需要自由地更改存储格式而不影响您的客户端。 对代码进行分层,以便模块可以处理客户端 protos、存储 protos 或进行转译。
为什么?您可能想要变更底层存储系统。 您可能想要以不同的方式规范化或非规范化数据。 您可能会意识到,客户端公开的 protos 的某些部分适合存储在 RAM 中,而其他部分适合存储在磁盘上。
当涉及到在顶级请求或响应中嵌套一个或多个级别的 protos 时,分离存储和通信 protos 的理由并不那么充分,并且取决于您愿意将客户端与这些原型耦合到多紧密。
维护转译层需要付出成本,但一旦您有了客户并必须进行第一次存储更改,它很快就会得到回报。
您可能会倾向于共享 protos 并在 “需要时” 进行拆分。 由于拆分的成本很高,并且没有明确的地方放置内部字段,在不知道这些内部知识的情况下,您的 API 会累积客户不理解或者依赖的字段。
通过拆分 proto 文件,您的团队将知道在哪里添加内部字段而不会污染您的 API。 在早期,通信的 proto 可以与自动转译层(想想:字节复制或 proto 反射)使用相同标签。 原型注释也可以由自动转译层提供支持。
以下是规则的例外情况:
请注意,如果您正在实现日志记录系统或基于 proto 的通用存储系统包装器之类的东西,那么您可能希望尽可能不透明地让客户的消息传输到您的存储后端,这样您就不会创建依赖关系。 考虑使用扩展或 Encode Opaque Data in Strings by Web-safe Encoding Binary Proto Serialization。
不要创建仅接受 Foo
的 UpdateFooRequest
。
如果客户端不保留未知字段,他们将不会拥有 GetFooResponse
的最新字段,从而导致往返过程中数据丢失。
有些系统不保留未知字段。除非应用程序明确删除未知字段,否则 Proto2 和 proto3 实现会保留未知字段。
一般来说,公共 API 应该在服务器端删除未知字段,以防止通过未知字段进行安全攻击。
例如,当服务器将来开始将它们用作新字段时,垃圾未知字段可能会导致服务器失败。
缺少文档,对可选字段的处理不明确。
UpdateFoo
会清除该字段吗?当客户端不知道该字段时,您将面临数据丢失的风险。
它不接触字段吗?那么客户端如何清除该字段?两者都不好。
Foo
包含 Bar
,则 FooMask
包含 BarMask
。PromoteEmployeeRequest
、SetEmployeePayRequest
、TransferEmployeeRequest
等,而不是 UpdateEmployeeRequest
。自定义更新方法比非常灵活的更新方法更易于监控、审计和保护。 它们也更容易实现和调用。大量的它们会增加 API 的认知负荷。
本文档其他地方描述的许多陷阱都可以通过此规则解决。例如:
通过将重复字段包装在消息中,可以告诉客户端存储中未保存此重复字段,而不是在此特定调用中未设置。
请求之间共享的常见请求选项自然不遵循此规则。读取和写入字段掩码也不在此规则中。
您的顶级 proto 几乎始终应是可以独立增长的其他消息的容器。
即使您今天只需要一个原始类型,将其包装在消息中也可以为您提供扩展该类型并在返回类似值的其他方法之间共享该类型的明确途径。 例如:
message MultiplicationResponse {
// Bad: What if you later want to return complex numbers and have an
// AdditionResponse that returns the same multi-field type?
optional double result;
// Good: Other methods can share this type and it can grow as your
// service adds new features (units, confidence intervals, etc.).
optional NumericResult result;
}
message NumericResult {
optional double real_value;
optional double complex_value;
optional UnitType units;
}
顶级原始字段的一个例外是:不透明字符串(或字节),它们对 proto 进行编码,但仅在服务器上构建和解析。 如果字符串实际上是结构化 proto 的编码,则延续令牌、版本信息令牌和 ID 都可以作为字符串返回。
如果您将布尔值用于字段,请确保该字段确实只描述了两种可能的状态(针对所有时间,而不仅仅是现在和不久的将来)。 通常使用更灵活的 枚举、整数或 message 是值得的。
例如,在返回帖子流时,开发人员可能需要根据 UX 的当前模拟来指示帖子是否应以两列呈现。 尽管布尔值是目前所需要的,但没有什么可以阻止 UX 在未来版本中引入两行帖子、三列帖子或四方帖子。
message GooglePlusPost {
// Bad: Whether to render this post across two columns.
optional bool big_post;
// Good: Rendering hints for clients displaying this post.
// Clients should use this to decide how prominently to render this
// post. If absent, assume a default rendering.
optional LayoutConfig layout_config;
}
message Photo {
// Bad: True if it's a GIF.
optional bool gif;
// Good: File format of the referenced photo (for example, GIF, WebP, PNG).
optional PhotoType type;
}
在将状态添加到混淆概念的枚举时要谨慎。
如果某个状态为枚举引入了新维度或暗示了多种应用行为,您几乎肯定会需要另一个字段。
使用 int64 作为对象的标识符很诱人。但是你应该选择使用字符串。
这允许您根据需要更改 ID 空间并减少发生冲突的可能性。2^64 不再像以前那么大。
您还可以将结构化标识符编码为字符串,以鼓励客户端将其视为不透明的 blob。 您仍然必须有一个支持字符串的 proto,但您可以将 proto 序列化为字符串字段(编码为网络安全 Base64),这会从客户端公开的 API 中删除任何内部详细信息。 在这种情况下,请遵循以下准则。
message GetFooRequest {
// Which Foo to fetch.
optional string foo_id;
}
// Serialized and websafe-base64-encoded into the GetFooRequest.foo_id field.
message InternalFooRef {
// Only one of these two is set. Foos that have already been
// migrated use the spanner_foo_id and Foos still living in
// Caribou Storage Server have a classic_foo_id.
optional bytes spanner_foo_id;
optional int64 classic_foo_id;
}
如果您一开始就使用自己的序列化方案将 ID 表示为字符串,那么事情很快就会变得很奇怪。 这就是为什么最好在 proto 中定义 ID 字段为字符串。
这样在网络上传输时效率较低,proto 使用者的工作量更大,而且会让阅读文档的人感到困惑。 您的客户端还必须对编码感到疑惑:列表是否以逗号分隔? 我是否正确地转义了这些不受信任的数据?数字是否以 10 为基数? 最好让客户端发送实际消息或原始类型。 这样在网络上传输时更紧凑,对客户端来说也更清晰。
当您的服务需要为多种语言的客户端提供服务时,这种情况会变得尤其糟糕。 现在,每个客户端都必须选择正确的解析器或构建器 - 或者更糟 - 编写一个。
更一般地说,选择正确的原始类型。请参阅 Protocol Buffer 语言指南中的标量值类型表。
使用 JavaScript 客户端,很容易在 API 的字段中返回 HTML 或 JSON。 这会将您的 API 与特定 UI 绑定。以下是三个具体的危险:
除了初始页面加载之外,通常最好返回数据并使用客户端模板在客户端上构建 HTML。
如果您确实在客户端可见字段(延续令牌、序列化 ID、版本信息等)中编码不透明数据,请记录客户端应将其视为不透明 blob。 始终使用二进制 proto 序列化,切勿为这些字段使用文本格式或您自己设计的内容。 当您需要扩展不透明字段中编码的数据时,如果您尚未使用 protocol buffer 序列化,您会发现自己正在重新发明它。
定义一个内部 proto 来保存将进入不透明字段的字段(即使您只需要一个字段),将此内部 proto 序列化为字节,然后将结果以 Web 安全 base-64 编码到您的字符串字段中。
使用 proto 序列化的一个罕见例外:精心构建的替代格式带来的紧凑性是值得的。
您向客户端公开的 API 应该仅用于描述如何与您的系统交互。 在其中包含任何其他内容都会给试图理解它的人增加认知负担。
在响应 protos 中返回调试数据曾经是一种常见做法,但我们有更好的方法。 RPC 响应扩展(也称为“侧通道”)允许您使用一个 proto 描述您的客户端界面,使用另一个 proto 描述您的调试界面。
同样,在响应 proto 中返回实验名称曾经是一种日志记录便利——不成文的约定是客户端将在后续操作中将这些实验名称返回。 实现相同目的的公认方法是在分析管道中进行日志连接。
一个例外:
如果您需要持续的实时分析并且机器预算很少,则运行日志连接可能会令人望而却步。 在成本是决定性因素的情况下,提前对日志数据进行非规范化可能会有所帮助。 如果您需要日志数据往返传输,请将其作为不透明 blob 发送给客户端,并记录请求和响应字段。
注意:如果您需要在每个请求上返回或往返隐藏数据,那么您就隐藏了使用服务的真实成本,这也不是什么好事。
message FooQuery {
// Bad: If the data changes between the first query and second, each of
// these strategies can cause you to miss results. In an eventually
// consistent world (that is, storage backed by Bigtable), it's not uncommon
// to have old data appear after the new data. Also, the offset- and
// page-based approaches all assume a sort-order, taking away some
// flexibility.
optional int64 max_timestamp_ms;
optional int32 result_offset;
optional int32 page_number;
optional int32 page_size;
// Good: You've got flexibility! Return this in a FooQueryResponse and
// have clients pass it back on the next query.
optional string next_page_token;
}
分页 API 的最佳实践是使用不透明的延续令牌(称为 next_page_token ),该令牌由您序列化的内部 proto 支持,然后使用 WebSafeBase64Escape (C++) 或 BaseEncoding.base64Url().encode (Java)。 该内部 proto 可能包含许多字段。 重要的是,它为您带来了灵活性,并且(如果您选择)它可以为您的客户带来结果的稳定性。
不要忘记将此协议的字段验证为不可信的输入,请参阅 Encode opaque data in strings。
message InternalPaginationToken {
// Track which IDs have been seen so far. This gives perfect recall at the
// expense of a larger continuation token--especially as the user pages
// back.
repeated FooRef seen_ids;
// Similar to the seen_ids strategy, but puts the seen_ids in a Bloom filter
// to save bytes and sacrifice some precision.
optional bytes bloom_filter;
// A reasonable first cut and it may work for longer. Having it embedded in
// a continuation token lets you change it later without affecting clients.
optional int64 max_timestamp_ms;
}
message Foo {
// Bad: The price and currency of this Foo.
optional int price;
optional CurrencyType currency;
// Better: Encapsulates the price and currency of this Foo.
optional CurrencyAmount price;
}
只有内聚性高的字段才应该嵌套。 如果字段确实相关,您通常希望在服务器内部将它们一起传递。 如果它们在消息中一起定义,那就更容易了。想想:
CurrencyAmount calculateLocalTax(CurrencyAmount price, Location where)
如果您的 CL 引入了一个字段,但该字段以后可能会有相关字段,请先将其放入自己的消息中以避免这种情况:
message Foo {
// DEPRECATED! Use currency_amount.
optional int price [deprecated = true];
// The price and currency of this Foo.
optional google.type.Money currency_amount;
}
嵌套消息的问题在于,虽然 CurrencyAmount
可能是 API 中其他位置重用的热门候选,但 Foo.CurrencyAmount
可能不是。
在最坏的情况下,Foo.CurrencyAmount
被重用,但 Foo
特定的字段会泄漏到其中。
虽然 松散耦合 通常被认为是开发系统时的最佳实践,但在设计 .proto 文件时,这种做法可能并不总是适用。
在某些情况下,紧密耦合两个信息单元(通过将一个单元嵌套在另一个单元内)可能有意义。
例如,如果您正在创建一组现在看起来相当通用的字段,但您预计以后会向其中添加专门的字段,则嵌套消息会阻止其他人从此 .proto
文件中的其他地方引用该消息。
message Photo {
// Bad: It's likely PhotoMetadata will be reused outside the scope of Photo,
// so it's probably a good idea not to nest it and make it easier to access.
message PhotoMetadata {
optional int32 width = 1;
optional int32 height = 2;
}
optional PhotoMetadata metadata = 1;
}
message FooConfiguration {
// Good: Reusing FooConfiguration.Rule outside the scope of FooConfiguration
// tightly-couples it with likely unrelated components, nesting it dissuades
// from doing that.
message Rule {
optional float multiplier = 1;
}
repeated Rule rules = 1;
}
// Recommended: use google.protobuf.FieldMask
// Alternative one:
message FooReadMask {
optional bool return_field1;
optional bool return_field2;
}
// Alternative two:
message BarReadMask {
// Tag numbers of the fields in Bar to return.
repeated int32 fields_to_return;
}
如果您使用推荐的 google.protobuf.FieldMask
,则可以使用 FieldMaskUtil
(Java/C++) 库自动过滤 proto。
读取掩码在客户端设置了明确的期望,让他们控制他们想要返回多少数据,并允许后端仅获取客户端需要的数据。
可接受的替代方案是始终填充每个字段;也就是说,将请求视为存在一个隐式读取掩码,所有字段都设置为 true。 随着 proto 的增长,这可能会变得代价高昂。
最糟糕的故障模式是有一个隐式(未声明)读取掩码,该掩码根据填充消息的方法而变化。 这种反模式会导致从响应 protos 构建本地缓存的客户端出现明显的数据丢失。
当客户端对同一对象进行写入然后马上读取时,他们期望得到他们写入的内容——即使这种期望对于底层存储系统来说不合理。
您的服务器将读取本地值,如果本地 version_info 小于预期的 version_info,它将从远程副本读取以查找最新值。 通常,version_info 是一个 proto encoded as a string,其中包括修改到达数据中心和提交的时间戳。
即使是由一致存储支持的系统也经常希望使用令牌来触发更昂贵的读取一致性路径,而不是在每次读取时产生成本。
一个示例故障模式是服务的请求选项,其中每个 RPC 返回相同的数据类型,但具有单独的请求选项来指定最大注释、嵌入支持的类型列表等内容。
采取这种临时方法的代价是增加了客户端的复杂性,因为客户端需要弄清楚如何填写每个请求,并且增加了服务器的复杂性,将 N 个请求选项转换为一个通用的内部选项。 不少实际错误都可以追溯到这个例子。
相反,创建一个单独的消息来保存请求选项,并将其包含在每个顶级请求消息中。 这是一个更好的实践示例:
message FooRequestOptions {
// Field-level read mask of which fields to return. Only fields that
// were requested will be returned in the response. Clients should only
// ask for fields they need to help the backend optimize requests.
optional FooReadMask read_mask;
// Up to this many comments will be returned on each Foo in the response.
// Comments that are marked as spam don't count towards the maximum
// comments. By default, no comments are returned.
optional int max_comments_to_return;
// Foos that include embeds that are not on this supported types list will
// have the embeds down-converted to an embed specified in this list. If no
// supported types list is specified, no embeds will be returned. If an embed
// can't be down-converted to one of the supplied supported types, no embed
// will be returned. Clients are strongly encouraged to always include at
// least the THING_V2 embed type from EmbedTypes.proto.
repeated EmbedType embed_supported_types_list;
}
message GetFooRequest {
// What Foo to read. If the viewer doesn't have access to the Foo or the
// Foo has been deleted, the response will be empty but will succeed.
optional string foo_id;
// Clients are required to include this field. Server returns
// INVALID_ARGUMENT if FooRequestOptions is left empty.
optional FooRequestOptions params;
}
message ListFooRequest {
// Which Foos to return. Searches have 100% recall, but more clauses
// impact performance.
optional FooQuery query;
// Clients are required to include this field. The server returns
// INVALID_ARGUMENT if FooRequestOptions is left empty.
optional FooRequestOptions params;
}
尽可能使修改具有原子性。 更重要的是,使修具有幂等性。 部分故障的完全重试不应导致数据 破坏/重复。
有时,出于性能原因,您需要一个封装多个操作的 RPC。 部分故障时该怎么办? 如果一些成功,一些失败,最好让客户知道。
考虑将 RPC 设置为失败,并在 RPC 状态原型中返回成功和失败的详细信息。
一般来说,您希望不知道部分故障的客户仍然能够正常运行,而知道的客户可以得到有价值的额外信息。
通过让客户端编写所需的内容,在一次往返中查询许多严格指定的数据位的能力允许更广泛的 UX 选项而无需更改服务器。
这与前端和中间层服务器最为相关。
许多服务都公开了自己的批处理 API。
如果 Web 或移动客户端需要进行两个查询,并且它们之间存在数据依赖关系,则当前的最佳做法是创建一个新的 RPC 来保护客户端免受往返的影响。
在移动设备的情况下,通过将两种服务方法捆绑在一起形成一个新的方法,几乎总是值得为您的客户端节省额外往返的成本。 对于服务器到服务器的调用,情况可能不那么明显;这取决于您的服务对性能的敏感程度以及新方法引入了多少认知开销。
常见的演变是单个重复字段需要变成多个相关的重复字段。 如果您一开始使用的是 repeated 字段是基础类型,您的选择将受到限制——您可以创建并行重复字段,或者使用保存值的新消息定义新的重复字段并将客户端迁移到该字段。
如果您一开始使用的是 repeated message,后续的演变将变得容易。
// Describes a type of enhancement applied to a photo
enum EnhancementType {
ENHANCEMENT_TYPE_UNSPECIFIED;
RED_EYE_REDUCTION;
SKIN_SOFTENING;
}
message PhotoEnhancement {
optional EnhancementType type;
}
message PhotoEnhancementReply {
// Good: PhotoEnhancement can grow to describe enhancements that require
// more fields than just an enum.
repeated PhotoEnhancement enhancements;
// Bad: If we ever want to return parameters associated with the
// enhancement, we'd have to introduce a parallel array (terrible) or
// deprecate this field and introduce a repeated message.
repeated EnhancementType enhancement_types;
}
想象一下以下功能请求:“我们需要知道哪些增强功能是由用户执行的,哪些增强功能是由系统自动应用的。”
如果 PhotoEnhancementReply
中的增强字段是标量或枚举,则支持起来会困难得多。
这同样适用于 map 类型。 如果 map 的值已经是消息,则向其添加其他字段要容易得多,而不必从 map<string, string> 迁移到 map<string, MyProto>。
一个例外:
对延迟要求高的应用程序会发现,与单个 message 数组相比,多个原始类型数组的构造和删除速度更快;如果您使用 [packed=true](省略字段标签),它们在网络上的传输速度也会更小。 分配固定数量的数组比分配 N 条消息的工作量要少。 额外好处:在 Proto3 中,packed 默认为 true;您无需明确指定它。
在 Proto3 中引入 Proto3 Maps 之前,服务有时会使用带有标量字段的临时 KVPair message 将数据以键值对的形式公开。 最终,客户端将需要更深层次的结构,并最终设计需要以某种方式解析的键或值。 请参阅 Don’t encode data in a string。
因此,使用(可扩展的)消息类型作为值是对简单设计的直接改进。
所有语言的 Maps 都已移植到 proto2,因此使用 map<scalar, message> 比为相同目的发明自己的 KVPair 更好。
如果您想表示您事先不知道其结构的任意数据,请使用 google.protobuf.Any。
在服务的上层,客户端可能有重试逻辑。 如果重试是可变的,用户可能会感到惊讶。 重复的评论、构建请求、编辑等对任何人都没有好处。
避免重复写入的一个简单方法是允许客户端指定一个客户端创建的请求 ID,您的服务器会根据该 ID 进行重复数据删除(例如,内容哈希或 UUID)。
服务名称(即 .proto
文件中 service
关键字后面的部分)用于很多地方,而不仅仅是生成服务类名。
这使得这个名字比人们想象的更重要。
棘手的是,这些工具隐含地假设您的服务名称在整个网络中是唯一的。 更糟糕的是,它们使用的服务名称是非限定服务名称(例如,MyService),而不是限定服务名称(例如,my_package.MyService)。
因此,采取措施防止服务名称命名冲突是有意义的,即使它是在特定包内定义的。
例如,名为 Watcher
的服务可能会导致问题;像 MyProjectWatcher
这样的名字会更好。
默认情况下,RPC 没有超时。 由于请求可能会占用仅在完成后才释放的后端资源,因此设置允许所有表现良好的请求完成的默认截止时间是一种很好的防御做法。 过去不强制执行截止时间会给主要服务造成严重问题。 RPC 客户端仍应为传出的 RPC 设置截止时间,并且在使用标准框架时通常会默认这样做。 截止时间可能会被附加到请求的更短截止时间覆盖,并且通常会被覆盖。
设置截止时间选项可以清楚地将 RPC 截止时间传达给您的客户端,并且标准框架会尊重和执行该截止时间:
rpc Foo(FooRequest) returns (FooResponse) {
option deadline = x; // there is no globally good default
}
选择截止期限值将特别影响系统在负载下的表现。 对于现有服务,在强制执行新截止期限之前评估现有客户端行为至关重要,以免破坏客户端(咨询 SRE)。 在某些情况下,事后可能无法强制执行更短的截止期限。
请求和响应大小应该有界。 我们建议将范围限制在 8 MiB 左右,而 2 GiB 是许多原型实现无法达到的硬性限制。 许多存储系统对消息大小都有限制。
此外,无界消息
以下是绑定 API 中所有消息的几种方法:
如果您正在处理 UI,另请参阅 创建返回或操作小块数据的方法
谨慎传播状态代码 RPC 服务应在 RPC 边界小心查询错误,并向其调用者返回有意义的状态错误。
让我们检查一个玩具示例来说明这一点:
考虑一个调用 ProductService.GetProducts
的客户端,它不带任何参数。
作为 GetProducts
的一部分,ProductService
可能会获取所有产品,并为每个产品调用 LocaleService.LocaliseNutritionFacts
。
digraph toy_example {
node [style=filled]
client [label="Client"];
product [label="ProductService"];
locale [label="LocaleService"];
client -> product [label="GetProducts"]
product -> locale [label="LocaliseNutritionFacts"]
}
如果 ProductService
实现不正确,它可能会向 LocaleService
发送错误的参数,从而导致 INVALID_ARGUMENT
。
如果 ProductService
粗心地向其调用者返回错误,客户端将收到 INVALID_ARGUMENT
,因为状态代码会跨 RPC 边界传播。
但是,客户端没有向 ProductService.GetProducts
传递任何参数。
因此,有错误比无错误更糟糕:它会造成很大的混乱!
相反,ProductService
应该询问它在 RPC 边界收到的错误;即它实现的 ProductService
RPC 处理程序。
它应该向用户返回有意义的错误:如果它从调用者收到无效参数,它应该返回 INVALID_ARGUMENT
。
如果下游收到无效参数,它应该在将错误返回给调用者之前将 INVALID_ARGUMENT
转换为 INTERNAL
。
粗心地传播状态错误会导致混乱,调试成本会很高。 更糟糕的是,它可能会导致不可见的中断,其中每个服务都会转发客户端错误而不会导致任何警报发生。
一般规则是:在 RPC 边界,注意查询错误,并使用适当的状态代码向调用者返回有意义的状态错误。 为了传达含义,每个 RPC 方法都应记录在什么情况下返回什么错误代码。 每个方法的实现都应符合API 文档的约定。
为每个 RPC 方法创建唯一的请求和响应 proto。 稍后发现需要分散顶级请求或响应可能会很昂贵。 这包括“空”响应;创建一个唯一的空响应 proto,而不是重复使用众所周知的空消息类型。
要重复使用消息,请创建共享的“域”消息类型以包含在多个请求和响应 proto 中。 根据这些类型而不是请求和响应类型编写应用程序逻辑。
这使您能够灵活地独立发展方法请求/响应类型,但共享逻辑子单元的代码。
当 repeated 字段为空时,客户端无法判断该字段是否未由服务器填充,或者该字段的后备数据是否确实为空。 换句话说,重复字段没有 hasFoo 方法。
将 repeated 字段包装在 message 中是获取 hasFoo 方法的一种简单方法。
message FooList {
repeated Foo foos;
}
更全面的解决方法是使用字段读取掩码。 如果请求了该字段,则空列表表示没有数据。 如果未请求该字段,则客户端应在响应中忽略该字段。
更新 repeated 字段最糟糕的方式是强制客户端提供替换列表。 强制客户端提供整个数组的危险是多方面的。 不保留未知字段的客户端会导致数据丢失。并发写入会导致数据丢失。 即使这些问题都不存在,您的客户端也需要仔细阅读您的文档,以了解服务器端如何解释该字段。 空字段是否意味着服务器不会更新它,或者服务器将清除它?
尽量避免顺序依赖。这是额外的一层脆弱性。 一种特别糟糕的顺序依赖类型是并行数组。 并行数组使客户端更难解释结果,并使在您自己的服务中传递两个相关字段变得不自然。
message BatchEquationSolverResponse {
// Bad: Solved values are returned in the order of the equations given in
// the request.
repeated double solved_values;
// (Usually) Bad: Parallel array for solved_values.
repeated double solved_complex_values;
}
// Good: A separate message that can grow to include more fields and be
// shared among other methods. No order dependence between request and
// response, no order dependence between multiple repeated fields.
message BatchEquationSolverResponse {
// Deprecated, this will continue to be populated in responses until Q2
// 2014, after which clients must move to using the solutions field below.
repeated double solved_values [deprecated = true];
// Good: Each equation in the request has a unique identifier that's
// included in the EquationSolution below so that the solutions can be
// correlated with the equations themselves. Equations are solved in
// parallel and as the solutions are made they are added to this array.
repeated EquationSolution solutions;
}
Android 和 iOS 运行时都支持反射。 为此,字段和消息的未过滤名称会作为字符串嵌入到应用程序二进制文件 (APK、IPA) 中。
message Foo {
// This will leak existence of Google Teleport project on Android and iOS
optional FeatureStatus google_teleport_enabled;
}
几种缓解策略:
ProGuard
混淆。截至 2014 年第三季度。
iOS 没有混淆选项:一旦您在桌面上拥有 IPA,通过字符串对其进行管道传输将显示所包含的 proot 的字段名称。
iOS Chrome 拆解永远不要以此为借口使用编号混淆代码字段含义。要么堵住泄漏,要么获得支持以规避风险。
在某些情况下,您可以牺牲类型安全性或清晰度来换取性能优势。 例如,具有数百个字段(尤其是消息类型字段)的 proto 的解析速度将比具有较少字段的原型慢。 仅从内存管理的角度来看,非常深层嵌套的消息反序列化速度可能会很慢。 团队已经使用了一些技术来加速反序列化: