0.1. RPC框架原理 0.2. 主流的RPC框架 0.3. gRPC简介 0.4. gRPC特点 0.5. 使用Protocol Buffers 0.6. protocol buffers的版本 0.1. RPC框架原理 RPC框架的目标就是让远程服务调用更加简单、透明,RPC框架负责屏蔽底层的传输方式(TCP或者UDP)、序列化方式(XML/JSON/二进制)和通信细节。服务调用者可以像调用本地接口一样调用远程的服务提供者,而不需要关心底层通信细节和调用过程。 RPC框架的调用原理图如下所示: 0.2. 主流的RPC框架 业界主流的RPC框架整体上分为三类: 支持多语言的RPC框架:Google的gRPC和Apache(Facebook)的Thrift 只支持特定语言的RPC框架:新浪微博的Motan 支持服务治理等服务化特性的分布式服务框架,其底层内核仍然是RPC框架:阿里的Dubbo 随着微服务的发展,基于语言中立性原则构建微服务,逐渐成为一种主流模式,例如: 对于后端并发处理要求高的微服务,比较适合采用Go语言构建 对于前端的Web界面,则更适合Java和JavaScript 因此,基于多语言的RPC框架来构建微服务,是一种比较好的技术选择。 例如Netflix的API服务编排层和后端的微服务之间就采用gRPC进行通信。 0.3. gRPC简介 本文档介绍gRPC和protocol buffers,gRPC可以使用protocol buffers作为其接口定义语言(Interface Definition Language,IDL)和其基础消息交换格式。 在gRPC中,客户端应用程序可以直接调用不同计算机上的应用程序中的方法,就像它是本地对象一样,可以更轻松地创建分布式应用程序和服务。与许多RPC系统一样,gRPC基于定义服务的思想,指定可以使用其参数和返回类型的远程调用方法。 在服务端,服务器实现此接口并运行gRPC服务器来处理客户端调用 在客户端,客户端有一个存根(在某些语言中称为客户端),它提供与服务器相同的方法 gRPC的调用示例如下所示: gRPC客户端和服务端可以在各种环境中相互运行和通信(从Google内部的服务器到桌面应用),并且可以使用任何gRPC支持的语言编写。因此,可以使用Go,Python或Ruby轻松创建gRPC客户端与使用Java编写的gRPC服务端通信。此外,最新的Google API将具有gRPC版本的接口,可以轻松地在编写的应用程序中构建Google提供的功能和服务。 0.4. gRPC特点 语言中立,支持多种语言; 基于IDL文件定义服务,通过protoc工具生成指定语言的数据结构、服务端接口以及客户端Stub; 通信协议基于标准的HTTP/2设计,支持双向流、消息头压缩、单TCP的多路复用、服务端推送等特性,这些特性使得gRPC在移动端设备上更加省电和节省网络流量; 序列化支持PB(ProtocolBuffer)和JSON,PB是一种语言无关的高性能序列化框架,基于HTTP/2 + PB,保障了RPC调用的高性能。 0.5. 使用Protocol Buffers 默认情况下,gRPC使用protocol buffers,这是Google成熟的开源机制,用于序列化结构化数据(尽管它可以与其他数据格式,如JSON一起使用)。 使用protocol buffers的第一步是,定义要在.proto文件中序列化的数据的结构(这是一个扩展名为.proto的普通文本文件)。 protocol buffers中的数据被构造为消息,其中每个消息是包含一系列称为字段的键值的逻辑记录。这是一个简单的例子: message Person { string name = 1; int32 id = 2; bool has_ponycopter = 3;} 一旦指定了数据结构,就可以使用protocol buffers编译器protoc从原型定义生成首选语言的数据访问类: 为每个字段提供了简单的访问器,如name()和set_name() 将整个结构序列化为原始字节的方法 例如,如果选择的语言是C++,则运行编译器,上面的例子将生成一个名为Person的类。然后,可以在应用程序中使用此类来填充,序列化和检索Person protocol buffers消息。 正如将在示例中更详细地看到的那样,可以在普通的proto文件中定义gRPC服务,并将RPC方法参数和返回类型指定为protocol buffers消息: // 服务定义 service Greeter { rpc SayHello (HelloRequest) returns (HelloReply) ;}// 请求消息 message HelloRequest { string name = 1;}// 响应消息 message HelloReply { string message = 1;} gRPC还可以使用带有特殊gRPC插件的protoc来生成proto文件中的代码。如果,使用gRPC插件,将获得生成的gRPC客户端和服务端代码,以及用于填充,序列化和检索消息类型的常规protocol buffers代码。 可以在Protocol Buffers文档中找到有关protocol buffers的更多信息,并了解如何使用所选语言的快速入门来获取和安装带有gRPC插件的protoc。 0.6. protocol buffers的版本 虽然在很早之前,protocol buffers已经可供开源用户使用,但在示例中使用了一种新的protocol buffers,称为proto3,它具有略微简化的语法,一些有用的新功能,并支持更多语言。 目前提供Java,C++,Python,Objective-C,C#,lite-runtime(Android Java),Ruby和JavaScript,都来自protocol buffers GitHub repo,以及来自golang/protobuf GitHub的Go语言生成器,还有更多语言在开发中。 可以在proto3语言指南和每种语言的参考文档中找到更多信息,参考文档还包括.proto文件格式的正式规范。 通常,虽然可以使用proto2(当前的默认protocol buffers版本),但建议将proto3与gRPC一起使用,因为它允许使用全系列的gRPC支持的语言,以及避免与proto2客户端与proto3服务端通信时的兼容性问题,反之亦然。
0.1. 概述 0.1.1. 服务定义 本文档介绍了一些关键的gRPC概念,概述了gRPC的体系结构和RPC生命周期。 假设已阅读了上文, 有关特定语言的详细信息,请参阅所选语言的快速入门,教程和参考文档(如果有)(完整的参考文档即将推出)。 0.1. 概述 0.1.1. 服务定义 与许多RPC系统一样,gRPC基于定义服务的思想,指定可以被远程调用的方法,包括方法的参数和返回类型。默认情况下,gRPC使用protocol buffers作为接口定义语言(IDL)来描述服务接口和有效消息负载的结构。如果需要,也可以使用其他替代方案。 service HelloService { rpc SayHello (HelloRequest) returns (HelloResponse);}message HelloRequest { string greeting = 1;}message HelloResponse { string reply = 1;} gRPC允许您定义四种服务方法: 简单RPC,客户端向服务端发送单个请求并返回单个响应,就像正常的函数调用一样。 rpc SayHello(HelloRequest) returns (HelloResponse); 服务端侧流数据RPC,客户端向服务端发送单个请求并获取返回流以读取消息序列,客户端从返回的流中读取,直到没有更多消息,gRPC保证单个RPC调用中的消息的顺序。 rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse); 客户端侧流数据PC,客户端多次使用提供的流写入一系列消息并将其发送到服务端。 一旦客户端写完消息,它就等待服务端读取并返回响应,gRPC保证在单个RPC调用中的消息的顺序。 rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse) ; 双向流数据RPC,双方使用读写流发送一系列消息,这两个流独立运行,因此客户端和服务端可以按照自己喜欢的顺序进行读写(例如,服务端可以在写入响应之前等待接收所有客户端消息,或者它可以交替地读取消息然后写入消息,或者其他一些读写组合)。gRPC保证每个流中的消息顺序。 rpc BidiHello(stream HelloRequest) returns (stream HelloResponse); 我们将在下面的RPC生命周期部分中更详细地介绍不同类型的RPC。 使用API接口 从.proto文件中的服务定义开始,gRPC提供了生成客户端和服务端代码的protocol buffers编译器插件。gRPC用户通常在客户端调用这些API,并在服务端实现相应的API。 在服务端实现服务定义中声明的方法,并运行gRPC服务器来处理客户端调用。gRPC框架解码传入请求,然后执行服务端方法并将服务端的响应信息编码后返回。 在客户端有一个称为存根(stub)的本地对象(对于某些语言,也称为client),它实现与服务端相同的方法,然后,客户端可以仅在本地对象上调用这些方法,并将调用方法时的参数包装在合适的protocol buffers消息类型中,gRPC框架将包装好的请求发送到服务端并接收服务端protocol buffers响应的返回消息。 同步与异步 同步RPC调用将会阻塞直到收到从服务端返回的响应,这是最接近RPC所期望的过程调用抽象。网络本质上是异步的,在许多情况下,能够在不阻塞当前线程的情况下启动RPC将非常有用。 大多数语言的gRPC编程接口都有同步和异步两种版本。可以在每种语言的教程和参考文档中找到更多信息(完整的参考文档即将推出)。 RPC生命周期 现在让我们仔细看看当gRPC客户端调用gRPC服务端方法时会发生什么。我们不会查看实现细节,您可以在我们特定语言的页面中找到有关这些内容的更多信息。 简单RPC 首先让我们看一下最简单的RPC类型,客户端发送单个请求并返回单个响应。 客户端在stub/client对象上调用方法后,将通知服务端此RPC调用已被激活,通知中将包含此次调用时客户端的元数据,方法名称和指定的截止时间(如果指定)。 服务端可以立即返回自己的初始元数据(必须在返回任何响应数据之前发送),或者等待客户端的请求消息(首先由应用程序指定)。 一旦服务端收到客户端的请求消息,它就会执行创建和填充其返回响应所需的所有工作。然后将响应与状态详细信息(状态代码和可选状态消息)以及可选的尾随元数据一起返回(如果成功)到客户端。 如果状态为OK,则客户端获取返回的响应,从而完成客户端的调用。 服务端侧流数据RPC 服务端侧流数据RPC类似于上面的简单RPC,只是服务器在获取客户端的请求消息后返回响应流。在返回所有响应之后,服务端状态的详细信息(状态代码和可选状态消息)和可选的尾随元数据都一起返回到客户端,此时服务端所有操作都完成。一旦客户端拥有所有服务器的响应,客户端的调用就完成。 客户端侧流数据RPC 客户端侧流数据RPC也类似于上面的简单RPC,只是客户端向服务器发送的是请求流而不是单个请求。服务器返回单个响应以及其状态详细信息和可选的尾随元数据,通常不一定在收到所有客户端请求后才返回响应信息。 双向流数据RPC 在双向流数据RPC中,调用由调用方法的客户端和接收客户端元数据、方法名称和截止时间的服务端启动。服务端可以选择先返回其初始元数据或等待客户端开始发送请求。 接下来会发生什么取决于应用程序,客户端和服务端可以按任意顺序进行读写,因为流是完全独立运行的。例如; 服务端可以等到它收到所有客户端的请求消息之后再写入其响应 服务端和客户端可以类似“乒乓球”的方式进行交互:服务端接收请求,然后返回响应,然后客户端发送另一个基于响应的请求,等等。 截止日期/超时时间 gRPC允许客户端指定一个愿意等待RPC完成的最长等待时间,该等待时间用于在RPC以错误DEADLINE_EXCEEDED终止之前的最长等待时间。在服务端,服务器可以通过查询来判断特定的RPC是否已超时,或者剩余多少时间来完成该RPC。 指定截止日期或超时时间的方式因编程语言而异,并非所有编程语言都有默认截止日期/超时时间: 某些编程语言的API支持设置截止日期(固定到某个时间点) 某些编程语言的API支持设置超时时间 (持续的时间长度) RPC终止 在gRPC中,客户端和服务端都会对是否调用成功进行本地的、独立的判断,并且两者得到的结论很可能是不匹配的。这意味着: 在服务端成功判断RPC完成(“我已经发送了所有响应!”),但在客户端判断RPC失败(“响应在截止日期后才到达!”) 在客户端发送所有请求之前,服务端就已经判断RPC完成 取消RPC 客户端或服务端可以随时取消RPC,取消将立即终止RPC,以便不再进行进一步的工作。取消操作并不是“撤消”,所以在取消操作之前所做的更改都将不会被回滚。 元数据 元数据是关于特定RPC操作的信息(例如,身份验证详细信息),它以键值对列表的形式存在,其中键是字符串,值通常也是字符串(但可以是二进制数据)。元数据对于gRPC来说,本身也是不透明的,它允许客户端在调用时提供相关信息给服务端,反之亦然。 对元数据的访问取决于编程语言。 通道 gRPC通道提供与特定主机和端口上的gRPC服务端的一个连接,通常在创建客户端stub/client时使用。客户端可以指定通道参数来修改gRPC的默认行为,例如打开和关闭消息压缩。通道具有状态,包括已连接和空闲。 gRPC如何处理关闭通道与编程语言有关。某些编程语言还允许查询通道状态。
0.1. 概述 0.2. 支持的身份认证机制 0.3. 认证API 0.3.1. 凭据类型 0.3.2. 使用客户端SSL/TLS 0.3.3. 使用基于Google令牌的认证 0.3.4. 扩展gRPC以支持其他认证机制 0.4. 样例 0.4.1. Golang 0.4.1.1. 基础例子,没有加密和认证 0.4.1.2. 带有SSL/TLS认证的服务 0.4.1.3. 基于Google令牌的认证 本文档概述了gRPC认证机制,包括内置支持的身份验证机制,如何插入自己的身份验证系统,以及如何在支持的语言中使用gRPC认证机制的示例。 0.1. 概述 gRPC旨在与各种身份验证机制配合使用,可以轻松安全地使用gRPC与其他系统进行通信。可以使用我们支持的机制: 带或不带基于Google令牌的身份验证的SSL/TLS 通过扩展我们提供的代码来插入自己的身份验证系统 gRPC还提供了一个简单的身份验证API,允许在创建通道或发起调用时提供所有必要的身份验证信息作为凭据。 0.2. 支持的身份认证机制 gRPC内置了以下身份验证机制: SSL/TLS:gRPC集成了SSL/TLS,并推荐使用SSL/TLS对服务进行身份验证,并且对客户端和服务端之间交换的所有数据进行加密。可选机制可供客户端提供相互身份验证的证书。 基于Google令牌的认证:gRPC提供了一种通用机制(如下所述),用于将基于凭据的元数据附加到请求和响应中。此外,某些身份验证流程提供了通过gRPC访问Google API时获取访问令牌(例如,OAuth2令牌)的支持:可以在下面的代码示例中看到它的工作原理。通常,必须使用此机制就像必须在通道上使用SSL/TLS(Google不允许没有SSL/TLS的连接),并且大多数实现gRPC的编程语现都不允许在未加密的通道上发送凭据。 警告:Google凭据只能用于连接Google服务。将Google发布的OAuth2令牌发送到非Google服务可能会导致此令牌被盗并用于冒充客户端访问Google服务。 0.3. 认证API gRPC提供基于凭据对象统一概念的简单认证API,可在创建整个gRPC通道或单个请求时使用。 0.3.1. 凭据类型 凭据有两种类型: 通道凭据(ChannelCredentials):附加到一个通道上,例如SSL凭据 调用凭据(CallCredentials):附加到一个调用上(或者是C++中的ClientContext) 还可以在综合通道凭据(CompositeChannelCredentials)中组合使用这两种凭据。 例如,可以指定通道的SSL以及在通道中进行的每个调用的调用凭据。 综合通道凭据将通道凭据与调用凭据相关联,以创建新的通道凭据。结果就是将发送与在通道上进行的每次调用相关联的调用凭据组合而成的认证数据。 例如,可以从SslCredentials和AccessTokenCredentials创建通道凭据。结果就是当应用在一个通道时将为此通道上的每个调用发送相应的访问令牌(token)。 单个调用凭据也可以使用综合调用凭据(CompositeCallCredentials)组成。在一个调用中使用综合调用凭据生成的调用凭据时,将触发发送与两个调用凭据关联的认证数据。 0.3.2. 使用客户端SSL/TLS 现在让我们看看凭据如何与我们支持的认证机制一起工作。这是最简单的身份验证方案,客户端只想验证服务端并加密所有数据。该示例使用的是C++,所有编程语言的API都类似:可以在下面的示例部分中看到如何在更多语言中启用SSL/TLS。 // Create a default SSL ChannelCredentials object. channel_creds = grpc::SslCredentials(grpc::SslCredentialsOptions()); // Create a channel using the credentials created in the previous step. channel = grpc::CreateChannel(server_name, channel_creds); // Create a stub on the channel. std::unique_ptr<Greeter::Stub> stub(Greeter::NewStub(channel)); // Make actual RPC calls on the stub. grpc::Status s = stub->sayHello(&context, *request, response); 对于高级用例,例如修改根CA或使用客户端证书,可以在传递给工厂模式的SslCredentialsOptions参数中设置相应的选项。 0.3.3. 使用基于Google令牌的认证 gRPC应用程序可以使用简单的API创建凭据,该凭据可用于在各种部署方案中与Google进行身份验证。同样,我们的示例是在C++中,但可以在我们的示例部分中找到其他语言的示例。 creds = grpc::GoogleDefaultCredentials(); // Create a channel, stub and make RPC calls (same as in the previous example) channel = grpc::CreateChannel(server_name, creds); std::unique_ptr<Greeter::Stub> stub(Greeter::NewStub(channel)); grpc::Status s = stub->sayHello(&context, *request, response); 此通道凭据对象适用于使用Service Account的应用程序或者在Google Compute Engine(GCE)中运行的应用程序。 前一种情况下,Service Account的私钥是从环境变量GOOGLE_APPLICATION_CREDENTIALS中指定的文件加载的。密钥用于生成承载令牌,该令牌附加到相应通道的每个传出RPC上。 对于在GCE中运行的应用程序,可以在VM启动期间配置默认的Service Account和对应的OAuth2作用域。在运行时,此凭据处理与身份验证系统的通信来获得OAuth2访问令牌,并将获得的访问令牌附加到相应通道的每个传出RPC上。 0.3.4. 扩展gRPC以支持其他认证机制 凭据插件API允许开发人员插入自己的凭据类型,包括: MetadataCredentialsPlugin抽象类,它包含需要由开发人员创建的子类实现的纯虚拟GetMetadata方法。 MetadataCredentialsFromPlugin函数,它从MetadataCredentialsPlugin创建调用凭据。 下面是一个简单凭的据插件的示例,该插件在自定义标头中设置认证票据。 class MyCustomAuthenticator : public grpc::MetadataCredentialsPlugin { public: MyCustomAuthenticator(const grpc::string& ticket) : ticket_(ticket) {} grpc::Status GetMetadata( grpc::string_ref service_url, grpc::string_ref method_name, const grpc::AuthContext& channel_auth_context, std::multimap<grpc::string, grpc::string>* metadata) override { metadata->insert(std::make_pair("x-custom-auth-ticket", ticket_)); return grpc::Status::OK; } private: grpc::string ticket_; }; call_creds = grpc::MetadataCredentialsFromPlugin( std::unique_ptr<grpc::MetadataCredentialsPlugin>( new MyCustomAuthenticator("super-secret-ticket"))); 通过在核心级别插入gRPC凭证可以实现更深入的集成。gRPC内部还允许使用其他加密机制切换SSL/TLS。 0.4. 样例 所有gRPC支持的编程语言都可以提供这样的身份认证机制。以下部分演示了如何在每种编程语言中使用上述身份验证和授权功能:即将推出更多语言。 0.4.1. Golang 0.4.1.1. 基础例子,没有加密和认证 // 客户端 conn, _ := grpc.Dial("localhost:50051", grpc.WithInsecure()) // error handling omitted client := pb.NewGreeterClient(conn) // ... // 服务端 s := grpc.NewServer() lis, _ := net.Listen("tcp", "localhost:50051") // error handling omitted s.Serve(lis) 0.4.1.2. 带有SSL/TLS认证的服务 // 客户端 creds, _ := credentials.NewClientTLSFromFile(certFile, "") conn, _ := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(creds)) // error handling omitted client := pb.NewGreeterClient(conn) // ... // 服务端 creds, _ := credentials.NewServerTLSFromFile(certFile, keyFile) s := grpc.NewServer(grpc.Creds(creds)) lis, _ := net.Listen("tcp", "localhost:50051") // error handling omitted s.Serve(lis) 0.4.1.3. 基于Google令牌的认证 pool, _ := x509.SystemCertPool() // error handling omitted creds := credentials.NewClientTLSFromCert(pool, "") perRPC, _ := oauth.NewServiceAccountFromFile("service-account.json", scope) conn, _ := grpc.Dial( "greeter.googleapis.com", grpc.WithTransportCredentials(creds), grpc.WithPerRPCCredentials(perRPC), ) // error handling omitted client := pb.NewGreeterClient(conn) // ...
0.1. 标准错误模型 0.2. 更丰富的错误模型 0.3. 错误状态码 0.3.1. 一般错误 0.3.2. 网络故障 0.3.3. 协议错误 本文描述了gRPC如何处理错误,包括gRPC的内置错误状态码。可以在此处看到不同编程语言的示例代码。 0.1. 标准错误模型 正如在我们的概念文档和示例中所看到的,当gRPC调用成功并调用完成时,服务端会向客户端返回OK状态(取决于编程语言,OK状态可能会也可能不会直接在代码中使用)。那么,如果调用不成功会发生什么? 如果发生错误,gRPC会返回其错误状态码,并带有可选的字符串错误消息,该消息提供有关所发生情况的更多详细信息。所有支持的编程语言中的gRPC客户端都可以使用错误信息。 0.2. 更丰富的错误模型 标准错误模型是gRPC官方的错误模型,受所有gRPC客户端/服务端库的支持,并且独立于gRPC数据格式(无论是protocol buffers还是其他格式)。标准错误模型非常有限,并且不包括传达错误详情的能力。 但是,如果使用protocol buffers作为数据格式,则可能需要考虑使用Google开发和使用的更丰富的错误模型,如此处所述。此模型在服务端返回并在客户端消费额外的错误详情,这些错误详情由一个或多个protobuf消息表示。这种模型进一步指定了一组标准的错误消息类型,以满足最常见的需求(例如,无效参数、配额违规、堆栈跟踪等)。这些额外的错误详情的protobuf二进制编码在响应中作为尾随元数据提供。 这个更丰富的错误模型已经在C++,Go,Java,Python和Ruby库中得到支持,并且至少在grpc-web和Node.js的库中开了issue讨论需要实现它。如果有需求,其他编程语言库可能会在将来添加支持,如果感兴趣可以查看他们的github存储库。但请注意,用C语言编写的grpc-core库不太可能支持它,因为它有目的实现为与数据格式无关(it is purposely data format agnostic)。 如果没有使用protocol buffers,可以使用类似的方法(在响应的尾随元数据中放置错误详情),这样的话,可能需要查找或开发用于访问此数据的库支持,以便在你编写的API中实际使用它。 在决定是否使用这样的扩展错误模型时需要注意一些重要的注意事项,包括: 在错误详情有效负载的期望和需求方面,扩展错误模型的库实现可能与编程语言之间不一致 已有的代理服务器,日志记录器和其他标准HTTP请求处理器无法查看错误详情,因此无法将其用于监视或其他目的 在尾部追加接口中的额外的错误详情将会出现队头阻塞(Head-of-line blocking,HOL blocking),并且,由于更频繁的缓存未命中会降低HTTP/2报头的压缩效率 更大的错误详情有效负载可能会遇到协议限制(例如,最大标头大小限制),从而丢失原始错误 队头阻塞(Head-of-line blocking或缩写为HOL blocking)在计算机网络的范畴中是一种性能受限的现象。它的原因是一列的第一个数据包(队头)受阻而导致整列数据包受阻。 例如它有可能在缓存式输入的交换机中出现,有可能因为传输顺序错乱而出现,亦有可能在HTTP流水线中有多个请求的情况下出现。 0.3. 错误状态码 在许多情况下错误由gRPC引起,从网络故障到未经认证的连接,每一种错误都有与之相关联的特定的状态码。所有gRPC编程语言都支持以下的错误状态代码。 0.3.1. 一般错误 案例 状态码 客户端应用取消请求 GRPC_STATUS_CANCELLED 在服务端返回状态时已超过截止日期 GRPC_STATUS_DEADLINE_EXCEEDED 服务端未找到对应方法 GRPC_STATUS_UNIMPLEMENTED 服务端宕机 GRPC_STATUS_UNAVAILABLE 服务端抛出异常(或者执行了除返回状态码以终止RPC之外的其他操作) GRPC_STATUS_UNKNOWN 0.3.2. 网络故障 案例 状态码 在截止日期到期前,无数据传输或者传输了某些数据并且未检测到其他故障 GRPC_STATUS_DEADLINE_EXCEEDED 在连接中断之前已经传输了某些数据(例如,请求元数据已经被写入TCP连接中) GRPC_STATUS_UNAVAILABLE 0.3.3. 协议错误 案例 状态码 无法解压缩但支持压缩算法 GRPC_STATUS_INTERNAL 客户端使用的压缩机制不受服务端支持 GRPC_STATUS_UNIMPLEMENTED 达到流量控制资源限制 GRPC_STATUS_RESOURCE_EXHAUSTED 流量控制协议违规 GRPC_STATUS_INTERNAL 解析返回状态出错 GRPC_STATUS_UNKNOWN 未认证:凭据无法获取元数据 GRPC_STATUS_UNAUTHENTICATED 主机集在认证元数据中无效 GRPC_STATUS_UNAUTHENTICATED 解析返回的protocol buffers时出错 GRPC_STATUS_INTERNAL 解析请求protocol buffers时出错 GRPC_STATUS_INTERNAL
0.1. 概述 0.2. 性能测试设计 0.3. 编程语言测试 0.4. 应用场景测试 0.5. 测试基础架构 gRPC是支持多种编程语言的高性能开源RPC。本文档介绍了性能基准测试工具,测试所考虑的方案以及测试基础架构。 0.1. 概述 gRPC专用于设计高性能和高生产率的分布式应用程序而设计。持续的性能基准测试是gRPC开发工作流程的关键部分。针对主分支每小时运行多语言性能测试,并将这些测试数据报告给仪表板以进行数据可视化。 多语言性能仪表板@latest_release(最新可用稳定版) 多语言性能仪表板@master(最新开发版) C++详细性能仪表板@master(最新开发版) 额外的基准测试可以提供有关CPU使用情况的细粒度洞察。 C++ full-stack microbenchmarks C Core filter benchmarks C Core shared component benchmarks C Core HTTP/2 microbenchmarks 0.2. 性能测试设计 每种编程语言都实现了一个gRPC WorkerService的性能测试worker。此服务指示worker充当实际基准测试的客户端或服务器,表示为BenchmarkService。该服务有两种方法: 简单调用:一个简单的PC请求,它指定在响应中返回的字节数 流数据调用:一个流数据RPC允许重复的在请求和响应之间交互,每一次交互过程都类似于简答调用 这些worker进程由驱动程序控制,该驱动程序将场景描述(采用JSON格式)和指定每个worker进程的host:port的环境变量作为输入。 0.3. 编程语言测试 以下编程语言作为master上的客户端和服务端进行连续性能测试: C++ Java Go C# node.js Python Ruby 此外,从C core派生的所有编程语言都在每次拉取请求时进行了有限的性能测试(烟囱测试)。 除了作为性能测试的客户端和服务端运行之外,所有编程语言都与C++版本进行交叉测试: 作为针对C++服务端的客户端进行测试 作为针对C++客户端的服务端进行测试 此测试旨在为给定语言的客户端或服务端实现提供当前的性能上限,而无需测试另一方。 虽然PHP或移动环境不支持gRPC服务端(我们的性能测试需要),但可以使用另一种编程语言编写的代理WorkerService对其客户端性能进行基准测试。此代码是为PHP实现的,但尚未处于连续测试模式。 0.4. 应用场景测试 有几个重要的场景正在测试中并显示在上面的仪表板中,包括以下内容: 无竞争延迟:只有1个客户端使用流数据调用,一次发送一条消息时看到的中位数和尾部响应延迟 QPS(Query Per Second):有2个客户端和总共64个通道时的每秒处理消息的速率,每个通道使用流数据调用一次发送100个未完成的消息 可伸缩性(针对所选编程语言):每个服务端核心的每秒消息处理速率 大多数性能测试都使用安全通信和protobuf。 一些C++测试还使用不安全的通信和通用(非protobuf)API来显示峰值性能。 0.5. 测试基础架构 所有性能基准测试都通过我们的Jenkins测试基础架构作为GCE中的实例运行。除了上面描述的gRPC性能方案之外,我们还运行基线netperf TCP_RR延迟数,以便了解底层网络特征。这些数字出现在我们的仪表板上,有时会根据我们的实例在GCE中的分配位置而有所不同。 大多数测试实例都是8核系统,这些系统用于延迟和QPS测量。对于C++和Java,我们还支持在32核系统上进行QPS测试。所有QPS测试都为每台服务器使用2台相同的客户端计算机,以确保QPS测量不受客户端限制。
0.1. 介绍 0.2. Protocol 0.2.1. Outline 0.2.2. Requests 0.2.3. Responses 0.2.3.1. Example 0.2.3.2. User Agents 0.2.3.3. Idempotency and Retries 0.2.3.4. HTTP2 Transport Mapping 0.2.3.4.1. Stream Identification 0.2.3.4.2. Data Frames 0.2.3.4.3. Errors 0.2.3.4.4. Security 0.2.3.4.5. Connection Management 0.2.3.4.5.1. GOAWAY Frame 0.2.3.4.5.2. PING Frame 0.2.3.4.5.3. Connection failure 0.2.4. Appendix A - GRPC for Protobuf 0.1. 介绍 本文档用作HTTP2 framing承载的gRPC实现的详细描述。它假设您熟悉HTTP2规范。 0.2. Protocol Production rules are using <a href="http://tools.ietf.org/html/rfc5234">ABNF syntax</a>. 0.2.1. Outline The following is the general sequence of message atoms in a GRPC request & response message stream Request → Request-Headers *Length-Prefixed-Message EOS Response → (Response-Headers *Length-Prefixed-Message Trailers) / Trailers-Only 0.2.2. Requests Request → Request-Headers *Length-Prefixed-Message EOS Request-Headers are delivered as HTTP2 headers in HEADERS + CONTINUATION frames. Request-Headers → Call-Definition *Custom-Metadata Call-Definition → Method Scheme Path TE [Authority] [Timeout] Content-Type [Message-Type] [Message-Encoding] [Message-Accept-Encoding] [User-Agent] Method → ":method POST" Scheme → ":scheme " ("http" / "https") Path → ":path" "/" Service-Name "/" {method name} # But see note below. Service-Name → {IDL-specific service name} Authority → ":authority" {virtual host name of authority} TE → "te" "trailers" # Used to detect incompatible proxies Timeout → "grpc-timeout" TimeoutValue TimeoutUnit TimeoutValue → {positive integer as ASCII string of at most 8 digits} TimeoutUnit → Hour / Minute / Second / Millisecond / Microsecond / Nanosecond Hour → "H" Minute → "M" Second → "S" Millisecond → "m" Microsecond → "u" Nanosecond → "n" Content-Type → "content-type" "application/grpc" [("+proto" / "+json" / {custom})] Content-Coding → "identity" / "gzip" / "deflate" / "snappy" / {custom} Message-Encoding → "grpc-encoding" Content-Coding Message-Accept-Encoding → "grpc-accept-encoding" Content-Coding *("," Content-Coding) User-Agent → "user-agent" {structured user-agent string} Message-Type → "grpc-message-type" {type name for message schema} Custom-Metadata → Binary-Header / ASCII-Header Binary-Header → {Header-Name "-bin" } {base64 encoded value} ASCII-Header → Header-Name ASCII-Value Header-Name → 1*( %x30-39 / %x61-7A / "_" / "-" / ".") ; 0-9 a-z _ - . ASCII-Value → 1*( %x20-%x7E ) ; space and printable ASCII HTTP2 requires that reserved headers, ones starting with ":" appear before all other headers. Additionally implementations should send Timeout immediately after the reserved headers and they should send the Call-Definition headers before sending Custom-Metadata. Some gRPC implementations may allow the Path format shown above to be overridden, but this functionality is strongly discouraged. gRPC does not go out of its way to break users that are using this kind of override, but we do not actively support it, and some functionality (e.g., service config support) will not work when the path is not of the form shown above. If Timeout is omitted a server should assume an infinite timeout. Client implementations are free to send a default minimum timeout based on their deployment requirements. If Content-Type does not begin with "application/grpc", gRPC servers SHOULD respond with HTTP status of 415 (Unsupported Media Type). This will prevent other HTTP/2 clients from interpreting a gRPC error response, which uses status 200 (OK), as successful. Custom-Metadata is an arbitrary set of key-value pairs defined by the application layer. Header names starting with "grpc-" but not listed here are reserved for future GRPC use and should not be used by applications as Custom-Metadata. Note that HTTP2 does not allow arbitrary octet sequences for header values so binary header values must be encoded using Base64 as per https://tools.ietf.org/html/rfc4648#section-4. Implementations MUST accept padded and un-padded values and should emit un-padded values. Applications define binary headers by having their names end with "-bin". Runtime libraries use this suffix to detect binary headers and properly apply base64 encoding & decoding as headers are sent and received. Custom-Metadata header order is not guaranteed to be preserved except for values with duplicate header names. Duplicate header names may have their values joined with "," as the delimiter and be considered semantically equivalent. Implementations must split Binary-Headers on "," before decoding the Base64-encoded values. ASCII-Value should not have leading or trailing whitespace. If it contains leading or trailing whitespace, it may be stripped. The ASCII-Value character range defined is more strict than HTTP. Implementations must not error due to receiving an invalid ASCII-Value that's a valid field-value in HTTP, but the precise behavior is not strictly defined: they may throw the value away or accept the value. If accepted, care must be taken to make sure that the application is permitted to echo the value back as metadata. For example, if the metadata is provided to the application as a list in a request, the application should not trigger an error by providing that same list as the metadata in the response. Servers may limit the size of Request-Headers, with a default of 8 KiB suggested. Implementations are encouraged to compute total header size like HTTP/2's SETTINGS_MAX_HEADER_LIST_SIZE: the sum of all header fields, for each field the sum of the uncompressed field name and value lengths plus 32, with binary values' lengths being post-Base64. The repeated sequence of Length-Prefixed-Message items is delivered in DATA frames Length-Prefixed-Message → Compressed-Flag Message-Length Message Compressed-Flag → 0 / 1 # encoded as 1 byte unsigned integer Message-Length → {length of Message} # encoded as 4 byte unsigned integer (big endian) Message → *{binary octet} A Compressed-Flag value of 1 indicates that the binary octet sequence of Message is compressed using the mechanism declared by the Message-Encoding header. A value of 0 indicates that no encoding of Message bytes has occurred. Compression contexts are NOT maintained over message boundaries, implementations must create a new context for each message in the stream. If the Message-Encoding header is omitted then the Compressed-Flag must be 0. For requests, EOS (end-of-stream) is indicated by the presence of the END_STREAM flag on the last received DATA frame. In scenarios where the Request stream needs to be closed but no data remains to be sent implementations MUST send an empty DATA frame with this flag set. 0.2.3. Responses Response → (Response-Headers *Length-Prefixed-Message Trailers) / Trailers-Only Response-Headers → HTTP-Status [Message-Encoding] [Message-Accept-Encoding] Content-Type *Custom-Metadata Trailers-Only → HTTP-Status Content-Type Trailers Trailers → Status [Status-Message] *Custom-Metadata HTTP-Status → ":status 200" Status → "grpc-status" 1*DIGIT ; 0-9 Status-Message → "grpc-message" Percent-Encoded Percent-Encoded → 1*(Percent-Byte-Unencoded / Percent-Byte-Encoded) Percent-Byte-Unencoded → 1*( %x20-%x24 / %x26-%x7E ) ; space and VCHAR, except % Percent-Byte-Encoded → "%" 2HEXDIGIT ; 0-9 A-F Response-Headers & Trailers-Only are each delivered in a single HTTP2 HEADERS frame block. Most responses are expected to have both headers and trailers but Trailers-Only is permitted for calls that produce an immediate error. Status must be sent in Trailers even if the status code is OK. For responses end-of-stream is indicated by the presence of the END_STREAM flag on the last received HEADERS frame that carries Trailers. Implementations should expect broken deployments to send non-200 HTTP status codes in responses as well as a variety of non-GRPC content-types and to omit Status & Status-Message. Implementations must synthesize a Status & Status-Message to propagate to the application layer when this occurs. Clients may limit the size of Response-Headers, Trailers, and Trailers-Only, with a default of 8 KiB each suggested. The value portion of Status is a decimal-encoded integer as an ASCII string, without any leading zeros. The value portion of Status-Message is conceptually a Unicode string description of the error, physically encoded as UTF-8 followed by percent-encoding. Percent-encoding is specified in RFC 3986 §2.1, although the form used here has different restricted characters. When decoding invalid values, implementations MUST NOT error or throw away the message. At worst, the implementation can abort decoding the status message altogether such that the user would received the raw percent-encoded form. Alternatively, the implementation can decode valid portions while leaving broken %-encodings as-is or replacing them with a replacement character (e.g., '?' or the Unicode replacement character). 0.2.3.1. Example Sample unary-call showing HTTP2 framing sequence Request HEADERS (flags = END_HEADERS) :method = POST :scheme = http :path = /google.pubsub.v2.PublisherService/CreateTopic :authority = pubsub.googleapis.com grpc-timeout = 1S content-type = application/grpc+proto grpc-encoding = gzip authorization = Bearer y235.wef315yfh138vh31hv93hv8h3v DATA (flags = END_STREAM) <Length-Prefixed Message> Response HEADERS (flags = END_HEADERS) :status = 200 grpc-encoding = gzip content-type = application/grpc+proto DATA <Length-Prefixed Message> HEADERS (flags = END_STREAM, END_HEADERS) grpc-status = 0 # OK trace-proto-bin = jher831yy13JHy3hc 0.2.3.2. User Agents While the protocol does not require a user-agent to function it is recommended that clients provide a structured user-agent string that provides a basic description of the calling library, version & platform to facilitate issue diagnosis in heterogeneous environments. The following structure is recommended to library developers User-Agent → "grpc-" Language ?("-" Variant) "/" Version ?( " (" *(AdditionalProperty ";") ")" ) E.g. grpc-java/1.2.3 grpc-ruby/1.2.3 grpc-ruby-jruby/1.3.4 grpc-java-android/0.9.1 (gingerbread/1.2.4; nexus5; tmobile) 0.2.3.3. Idempotency and Retries Unless explicitly defined to be, gRPC Calls are not assumed to be idempotent. Specifically: Calls that cannot be proven to have started will not be retried. There is no mechanism for duplicate suppression as it is not necessary. Calls that are marked as idempotent may be sent multiple times. 0.2.3.4. HTTP2 Transport Mapping 0.2.3.4.1. Stream Identification All GRPC calls need to specify an internal ID. We will use HTTP2 stream-ids as call identifiers in this scheme. NOTE: These ids are contextual to an open HTTP2 session and will not be unique within a given process that is handling more than one HTTP2 session nor can they be used as GUIDs. 0.2.3.4.2. Data Frames DATA frame boundaries have no relation to Length-Prefixed-Message boundaries and implementations should make no assumptions about their alignment. 0.2.3.4.3. Errors When an application or runtime error occurs during an RPC a Status and Status-Message are delivered in Trailers. In some cases it is possible that the framing of the message stream has become corrupt and the RPC runtime will choose to use an RST_STREAM frame to indicate this state to its peer. RPC runtime implementations should interpret RST_STREAM as immediate full-closure of the stream and should propagate an error up to the calling application layer. The following mapping from RST_STREAM error codes to GRPC error codes is applied. HTTP2 Code GRPC Code NO_ERROR(0) INTERNAL - An explicit GRPC status of OK should have been sent but this might be used to aggressively lameduck in some scenarios. PROTOCOL_ERROR(1) INTERNAL INTERNAL_ERROR(2) INTERNAL FLOW_CONTROL_ERROR(3) INTERNAL SETTINGS_TIMEOUT(4) INTERNAL STREAM_CLOSED No mapping as there is no open stream to propagate to. Implementations should log. FRAME_SIZE_ERROR INTERNAL REFUSED_STREAM UNAVAILABLE - Indicates that no processing occurred and the request can be retried, possibly elsewhere. CANCEL(8) Mapped to call cancellation when sent by a client.Mapped to CANCELLED when sent by a server. Note that servers should only use this mechanism when they need to cancel a call but the payload byte sequence is incomplete. COMPRESSION_ERROR INTERNAL CONNECT_ERROR INTERNAL ENHANCE_YOUR_CALM RESOURCE_EXHAUSTED ...with additional error detail provided by runtime to indicate that the exhausted resource is bandwidth. INADEQUATE_SECURITY PERMISSION_DENIED … with additional detail indicating that permission was denied as protocol is not secure enough for call. 0.2.3.4.4. Security The HTTP2 specification mandates the use of TLS 1.2 or higher when TLS is used with HTTP2. It also places some additional constraints on the allowed ciphers in deployments to avoid known-problems as well as requiring SNI support. It is also expected that HTTP2 will be used in conjunction with proprietary transport security mechanisms about which the specification can make no meaningful recommendations. 0.2.3.4.5. Connection Management 0.2.3.4.5.1. GOAWAY Frame Sent by servers to clients to indicate that they will no longer accept any new streams on the associated connections. This frame includes the id of the last successfully accepted stream by the server. Clients should consider any stream initiated after the last successfully accepted stream as UNAVAILABLE and retry the call elsewhere. Clients are free to continue working with the already accepted streams until they complete or the connection is terminated. Servers should send GOAWAY before terminating a connection to reliably inform clients which work has been accepted by the server and is being executed. 0.2.3.4.5.2. PING Frame Both clients and servers can send a PING frame that the peer must respond to by precisely echoing what they received. This is used to assert that the connection is still live as well as providing a means to estimate end-to-end latency. If a server initiated PING does not receive a response within the deadline expected by the runtime all outstanding calls on the server will be closed with a CANCELLED status. An expired client initiated PING will cause all calls to be closed with an UNAVAILABLE status. Note that the frequency of PINGs is highly dependent on the network environment, implementations are free to adjust PING frequency based on network and application requirements. 0.2.3.4.5.3. Connection failure If a detectable connection failure occurs on the client all calls will be closed with an UNAVAILABLE status. For servers open calls will be closed with a CANCELLED status. 0.2.4. Appendix A - GRPC for Protobuf The service interfaces declared by protobuf are easily mapped onto GRPC by code generation extensions to protoc. The following defines the mapping to be used. Service-Name → ?( {proto package name} "." ) {service name} Message-Type → {fully qualified proto message name} Content-Type → "application/grpc+proto"
0.1. 先决条件 0.1.1. Go 版本 0.1.2. 安装gRPC 0.1.3. 安装protocol buffers v3 0.2. 下载示例 0.3. 编译示例 0.4. 尝试一下 0.5. 更新gRPC服务端 0.6. 生成gRPC代码 0.7. 更新并运行应用 0.7.1. 更新服务端 0.7.2. 更新客户端 0.7.3. 运行 本指南以简单的工作示例为您介绍Go中的gRPC。 0.1. 先决条件 0.1.1. Go 版本 gRPC要求Go版本大于等于1.6: sugoi@Sugoi-PC:~$ go version go version go1.12.7 linux/amd64 有关安装说明,请遵循以下指南:入门-Go编程语言。 0.1.2. 安装gRPC 使用下面的指令安装gRPC: go get -u google.golang.org/grpc 0.1.3. 安装protocol buffers v3 安装用于生成gRPC服务代码的protoc编译器。最简单的方法是从此处下载适用于您的平台的预编译二进制文件(protoc-<version>-<platform>.zip): 解压缩此文件 更新环境变量PATH以包含protoc二进制文件的路径 # protoc environment export PATH=$PATH:/opt/protoc/bin 接下来,为Go安装protoc插件。 go get -u github.com/golang/protobuf/protoc-gen-go protoc编译器插件protoc-gen-go将安装在$GOBIN中,默认为$GOPATH/bin,它必须在你的$PATH中,这样protocol的编译器protoc才能找到它。 # Golang environment export GOPATH=/home/sugoi/go export PATH=$PATH:/opt/go/bin:$GOPATH/bin 0.2. 下载示例 使用go get google.golang.org/grpc获取的grpc代码也包含它示例。它们可以在示例dir:$GOPATH/src/google.golang.org/grpc/examples下找到。 0.3. 编译示例 进入示例所在的目录下: cd $GOPATH/src/google.golang.org/grpc/examples/helloworld gRPC服务在.proto文件中定义,该文件用于生成相应的.pb.go文件。.pb.go文件是通过使用protocol编译器(protoc)编译.proto文件生成的。 此示例已生成helloworld.pb.go文件(通过编译helloworld.proto生成),并且可以在此目录中找到:$GOPATH/src/google.golang.org/grpc/examples/helloworld/helloworld helloworld.pb.go文件包含: 生成客户端和服务端的代码 用于填充,序列化和检索HelloRequest和HelloReply消息类型的代码 0.4. 尝试一下 使用go run代码来编译并执行服务端和客户端代码,在示例目录下执行如下命令: # 在第一个命令行终端 go run greeter_server/main.go # 在第二个命令行终端 go run greeter_client/main.go 如果一切顺利的话,将会在运行客户端的命令行终端中看到输出信息:Greeting: Hello World。 这样就成功的使用gRPC运行客户端-服务端应用程序。 0.5. 更新gRPC服务端 现在让我们看看如何使用其他方法更新应用程序中服务端的代码以供客户端调用。我们的gRPC服务是使用protocol buffers定义的;可以在gRPC简介和gRPC基础:Go中找到更多关于如何在.proto文件中定义服务的知识。现在需要知道的是,服务端和客户端的stub都有一个SayHello RPC方法,该方法从客户端获取HelloRequest参数并从服务器返回HelloReply,此方法定义如下: // 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; } 让我们更新一下代码,使Greeter服务有两种方法。确保在示例目录下($GOPATH/src/google.golang.org/grpc/examples/helloworld)。 编辑helloworld/helloworld.proto并使用新的SayHelloAgain方法更新它,新方法具有相同的请求和响应类型: // The greeting service definition. service Greeter { // Sends a greeting rpc SayHello (HelloRequest) returns (HelloReply) ; // Sends another greeting rpc SayHelloAgain (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; } 0.6. 生成gRPC代码 接下来,更新应用程序使用的gRPC代码以使用新的服务定义,还在上面的例子目录($GOPATH/src/google.golang.org/grpc/examples/helloworld) protoc -I helloworld/ helloworld/helloworld.proto --go_out=plugins=grpc:helloworld 这会使我们的更改重新生成helloworld.pb.go文件。 0.7. 更新并运行应用 现在有了新生成的服务端和客户端代码,还需要在示例应用程序的手动编写部分中实现并调用新方法。 0.7.1. 更新服务端 编辑greeter_server/main.go并向其添加以下函数: func (s *server) SayHelloAgain(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { return &pb.HelloReply{Message: "Hello again " + in.Name}, nil } 0.7.2. 更新客户端 编辑greeter_client/main.go并向其添加以下函数: r, err = c.SayHelloAgain(ctx, &pb.HelloRequest{Name: name}) if err != nil { log.Fatalf("could not greet: %v", err) } log.Printf("Greeting: %s", r.Message) 0.7.3. 运行 # 第一个命令行终端,运行服务端 go run greeter_server/main.go # 第二个命令行终端,运行客户端 go run greeter_client/main.go # 得到如下输出 Greeting: Hello world Greeting: Hello again world
0.1. 为何使用gRPC 0.2. 样例代码与设置 0.3. 定义服务 0.4. 生成客户端和服务端代码 0.5. 创建服务端 0.5.1. 实现 RouteGuide 0.5.1.1. 简单RPC 0.5.1.2. 服务端侧流数据RPC 0.5.1.3. 客户端侧流数据RPC 0.5.1.4. 双向流数据RPC 0.5.2. 启动服务端 0.6. 创建客户端 0.6.1. 创建一个stub(存根) 0.6.2. 调用服务端方法 0.6.2.1. 简单RPC 0.6.2.2. 服务端侧流数据RPC 0.6.2.3. 客户端侧流数据RPC 0.6.2.4. 双向流数据RPC 0.7. 搞起来 本教程提供了一个基本对Go程序员的指导关于如何使用gRPC。 通过练习这个例子,可以学习到如下知识: 在.proto文件中定义服务 使用protocol buffers编译器生成服务端和客户端代码 使用Go gRPC API为你的服务编写一个简单的客户端和服务端 假定已阅读概述并熟悉protocol buffers。请注意,本教程中的示例使用protocol buffers的proto3版本:可以在proto3语言指南和Go代码生成指南中找到更多信息。 0.1. 为何使用gRPC 我们的示例是一个简单的路径映射应用程序,允许客户端获取有关其路径Feature的信息,并创建其RouteSummary后与其他服务端或客户端交换路径信息,如,进行流量更新。 使用gRPC,只需要在.proto文件中定义一次服务,然后就可以使用gRPC支持的任何语言实现客户端和服务端,而这些语言又可以在谷歌内部的服务器或个人平板电脑等各种环境中运行,不同的语言和环境之间通信的复杂性都由gRPC处理。我们还获得了使用protocol buffers的所有优势,包括高效的序列化,简单的IDL和简单的接口更新。 0.2. 样例代码与设置 本例子的样例代码在GitHub的grcp-go仓库中。执行如下指令来克隆仓库中的代码: go get google.golang.org/grpc 然后,切换到样例目录$GOPATH/src/grpc-go/examples/route_guide中: cd $GOPATH/src/google.golang.org/grpc/examples/route_guide 还应该安装相关工具来生成服务端和客户端接口代码,如果还没有准备好,请按照Go快速入门指南中的设置说明进行操作。 0.3. 定义服务 第一步,使用protocol buffers定义: gRPC服务 方法请求类型 方法响应的类型 完整的.proto文件在这里。 定义一个服务需要在.proto文件中指定服务的名字: service RouteGuide { ... } 然后在定义的服务中定义rpc方法,指定方法请求和响应的类型。gRPC允许定义四种类型的服务方法,在示例服务RouterGuide中都用到了,具体如下。 简单RPC:客户端使用stub向服务端发送请求然后等待响应返回,就像普通的方法调用: // 获得给定位置的特征。 rpc GetFeature(Point) returns (Feature) {} 服务端侧流数据RPC:客户端向服务端发送单个请求并获取返回流以读取消息序列。客户端从返回的流中读取,直到没有更多消息。正如在示例中所写的那样,将stream关键字放在响应类型之前来指定服务器端流方法。 // 获得给定Rectangle中可用的特征。 // 得到的结果是流式传输而不是一次返回(例如,在响应消息中有重复字段), // 因为rectangle可能覆盖很大面积并且包含大量的特征。 rpc ListFeatures(Rectangle) returns (stream Feature) {} 客户端侧流数据RPC:客户端多次使用提供的流写入一系列消息并将它们发送到服务端。一旦客户端写完消息,它就等待服务端全部读取并返回响应的响应。通过在请求类型之前放置stream关键字来指定客户端流方法。 // 接受正在遍历的路径上的Points消息类型的流,在遍历完成时返回RouteSummary。 rpc RecordRoute(stream Point) returns (RouteSummary) {} 双向流数据RPC:双方使用读写流发送一系列消息,这两个流独立运行,因此客户端和服务端可以按照自己喜欢的顺序进行读写。(例如,服务端可以在写入响应之前等待接收所有客户端消息,或者它可以交替地读取消息然后写入响应消息,或者其他一些读写组合)。每个流中的消息顺序都能得到保证。可以通过在请求和响应之前放置stream关键字来指定此类方法。 // 接受在遍历路径时发送的RouteNotes消息类型的流, // 同时接收其他RouteNotes消息类型的消息(例如,来自其他用户)。 rpc RouteChat(stream RouteNote) returns (stream RouteNote) {} .proto文件还包含服务方法中使用的所有请求和响应类型的protocol buffers消息类型定义,例如,这里是Point消息类型: message Point { int32 latitude = 1; int32 longitude = 2; } 0.4. 生成客户端和服务端代码 第二步,使用protoc(protocol buffers的编译器)和特定的gRPC-GO插件(protoc-gen-go),根据.proto文件中定义的服务生成gRPC客户端和服务端接口。这与快速入门中的操作一样。 在route_guide的示例目录中运行如下命令: protoc -I routeguide/ routeguide/route_guide.proto --go_out=plugins=grpc:routeguide # 在目录下生成如下文件 route_guide.pb.go route_guide.pb.go文件包含: 填充,序列化和检索请求和响应消息类型的所有protocol buffers代码 实现RouteGuide服务中定义的客户端接口类型或stub(存根)和方法 实现RouteGuide服务中定义的服务端接口类型和方法 0.5. 创建服务端 首先创建RouteGuide服务端。如果只对创建gRPC客户端感兴趣,可以跳过本节直接阅读创建客户端。 要使RouteGuide服务能够正常提供它的服务,有两个部分需要完成: 实现从服务定义中生成的服务接口:它执行服务的实际“工作” 运行gRPC服务以监听来自客户端的请求并将其分派给正确的服务端实现 在grpc-go/examples/route_guide/server/server.go中可以看到RouteGuide服务端样例。 0.5.1. 实现 RouteGuide 如下所示,服务端有一个叫routeGuideServer的结构体类型它实现了自动生成的RouteGuideServer接口。 type routeGuideServer struct { ... } ... func (s *routeGuideServer) GetFeature(ctx context.Context, point *pb.Point) (*pb.Feature, error) { ... } ... func (s *routeGuideServer) ListFeatures(rect *pb.Rectangle, stream pb.RouteGuide_ListFeaturesServer) error { ... } ... func (s *routeGuideServer) RecordRoute(stream pb.RouteGuide_RecordRouteServer) error { ... } ... func (s *routeGuideServer) RouteChat(stream pb.RouteGuide_RouteChatServer) error { ... } ... 0.5.1.1. 简单RPC routeGuideServer实现了所有的服务方法,先看看最简单的类型GetFeature(),它只是从客户端获取一个Point并从某个Feature自己的数据库中返回相应的特征信息。 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{"", point}, nil } 该方法传递RPC的上下文对象和客户端Point类型的protocol buffers请求。它返回一个Feature类型的protocol buffers对象,其中包含响应信息和错误。在此方法中,我们使用适当的信息填充Feature,然后将其与nil错误一起返回来告诉gRPC已经完成了RPC的处理,然后Feature就可以返回给客户端。 0.5.1.2. 服务端侧流数据RPC 现在看一下流数据RPC。ListFeatures()方法是服务端侧流数据RPC,因此需要将多个Feature返回给客户端。 func (s *routeGuideServer) ListFeatures(rect *pb.Rectangle, stream pb.RouteGuide_ListFeaturesServer) error { for _, feature := range s.savedFeatures { if inRange(feature.Location, rect) { if err := stream.Send(feature); err != nil { return err } } } return nil } 如上所示,此方法不是在方法参数中获取简单的请求和响应对象,而是得到一个请求对象(在Feature中客户端想要查找的Rectangle)和一个特定的RouteGuide_ListFeaturesServer对象来编写我们的响应。 在该方法中,填充尽可能多的需要被返回的Feature对象,并使用其Send()方法将Feature都写入RouteGuide_ListFeaturesServer。最后,就像在简单RPC方法中那样,返回一个nil错误告诉gRPC已经完成了写响应信息的操作。如果在此调用中发生任何错误,那么将返回非零错误; gRPC层会将其转换为适当的RPC状态,以便在线路上发送。 0.5.1.3. 客户端侧流数据RPC 现在来看一些更复杂的东西:客户端侧流数据方法RecordRoute(),从客户端获取Points类型的流并返回单个包含有关传输链路信息的RouteSummary。如下所示,该方法根本没有请求参数。相反,它获取RouteGuide_RecordRouteServer流,服务端可以使用该流来读取和写入消息,服务端可以使用它的Recv()方法接收客户端消息,并使用它的SendAndClose()方法返回其单个响应。 func (s *routeGuideServer) RecordRoute(stream pb.RouteGuide_RecordRouteServer) error { var pointCount, featureCount, distance int32 var lastPoint *pb.Point startTime := time.Now() for { point, err := stream.Recv() if err == io.EOF { endTime := time.Now() return stream.SendAndClose(&pb.RouteSummary{ PointCount: pointCount, FeatureCount: featureCount, Distance: distance, ElapsedTime: int32(endTime.Sub(startTime).Seconds()), }) } if err != nil { return err } pointCount++ for _, feature := range s.savedFeatures { if proto.Equal(feature.Location, point) { featureCount++ } } if lastPoint != nil { distance += calcDistance(lastPoint, point) } lastPoint = point } } 在方法体中,使用RouteGuide_RecordRouteServer的Recv()方法重复读取客户端请求一个请求对象(在本例中为一个Point)的请求,直到没有更多消息,服务端需要检查每次调用Recv()方法后从中返回的错误err。 如果错误是nil,那么说明流仍然处于正常状态并且可以继续进行读取操作 如果错误是io.EOF,那么说明消息流已经结束,服务端可以返回它的RouteSummary 如果错误是任何其他值,那么将“按原样”返回错误,以便gRPC层将其转换为RPC状态 0.5.1.4. 双向流数据RPC 最后,看一下双向流数据PRC RouteChat()方法。 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) ... // 此处代码 寻找要发送给客户端的note for _, note := range s.routeNotes[key] { if err := stream.Send(note); err != nil { return err } } } } 得到一个RouteGuide_RouteChatServer流,就像客户端侧流数据示例那样,可用于读取和写入消息。但是,在这里通过方法的流来返回值,哪怕此时客户端仍在向其消息流写入消息。 这里的读写语法与客户端流方法中的语法非常相似,只是此处服务端使用流的Send()方法而不是SendAndClose()方法,因为它正在写入多个响应。尽管每一方都会按照写入的顺序获取对方的消息,但客户端和服务端都可以按任意顺序进行读写,因为,这些流完全独立运行。 0.5.2. 启动服务端 实现了所有方法后,还需要启动一个gRPC服务端,以便客户端可以实际使用我们的服务。以下代码段显示了如何为RouteGuide服务执行此操作: flag.Parse() lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port)) if err != nil { log.Fatalf("failed to listen: %v", err) } grpcServer := grpc.NewServer() pb.RegisterRouteGuideServer(grpcServer, &routeGuideServer{}) ... // 此处代码 确定是否启动TLS grpcServer.Serve(lis) 要构建和启动一个服务端,我们需要: 使用lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port)).指定需要监听客户端请求的端口 使用grpc.NewServer()创建一个gRPC服务端实例 使用gRPC服务端注册我们实现的服务 使用端口详细信息调用服务端的Serve()方法进行阻塞等待,直到进程被终止或调用Stop()。 0.6. 创建客户端 在本节中,将介绍如何为RouteGuide服务创建Go客户端,可以在grpc-go/examples/route_guide/client/client.go中查看完整的示例客户端代码。 0.6.1. 创建一个stub(存根) 要调用服务端的方法,首先需要创建一个gRPC通道来与服务端通信。通过将服务器地址和端口号传递给grpc.Dial()来创建它,如下所示: conn, err := grpc.Dial(*serverAddr) if err != nil { ... } defer conn.Close() 如果请求的服务需要身份认证,可以使用DialOptions在grpc.Dial()方法中设置身份验证凭据(例如,TLS,GCE凭据,JWT凭证,但是,在这里的RouteGuide服务中不需要执行此操作。 一旦gRPC通道建立起来,就需要有一个客户端stub(存根)来执行RPC调用。我们使用从.proto文件中生成的在pb包中提供的NewRouteGuideClient()方法创建一个客户端stub。 client := pb.NewRouteGuideClient(conn) 0.6.2. 调用服务端方法 现在来看看如何调用服务方法。请注意,在gRPC-Go中,RPC以阻塞/同步模式运行,这意味着RPC调用等待服务端响应,并将返回响应或错误。 0.6.2.1. 简单RPC 调用简单RPC的GetFeature()方法几乎与调用本地方法一样简单。 feature, err := client.GetFeature(context.Background(), &pb.Point{409146138, -746188906}) if err != nil { ... } 如上所示:在之前获得的stub上调用该方法,在方法参数中,创建并填充请求protocol buffers对象(在例子中为Point)。 还传递一个context.Context对象,它允许我们在必要时更改RPC的行为,例如超时/取消执行中的RPC请求。如果调用没有返回错误,那么可以从第一个返回值中读取服务端的响应信息。 log.Println(feature) 0.6.2.2. 服务端侧流数据RPC 这是调用服务端流方法ListFeatures()的地方,该方法返回地理Feature流。如果已经阅读过创建服务端,其中一些部分可能看起来非常熟悉:流数据RPC在服务端和客户端之间都以类似的方式实现。 rect := &pb.Rectangle{ ... } // initialize a pb.Rectangle stream, err := client.ListFeatures(context.Background(), rect) if err != nil { ... } for { feature, err := stream.Recv() if err == io.EOF { break } if err != nil { log.Fatalf("%v.ListFeatures(_) = _, %v", client, err) } log.Println(feature) } 与简单的RPC一样,传递上下文和请求给方法,但是在这里不会返回响应对象,而是返回RouteGuide_ListFeaturesClient的实例。这个客户端实例可以使用RouteGuide_ListFeaturesClient流来读取服务端的响应。 使用RouteGuide_ListFeaturesClient的Recv()方法重复的在服务器的响应中读取protocol buffers对象(在本例中为Feature),直到没有更多消息:这个客户端实例需要检查每次调用Recv()返回的错误err。 如果是nil,那么说明这个流仍然是正常的并且可以继续读取 如果是io.EOF,那么说明消息流已经结束 否则必须有一个RPC错误,它通过err参数传递。 0.6.2.3. 客户端侧流数据RPC 客户端流方法RecordRoute类似于服务器端方法,不过只传递给方法一个上下文,然后会获取一个RouteGuide_RecordRouteClient流的返回,这个流可以用来写和读消息。 // Create a random number of random points r := rand.New(rand.NewSource(time.Now().UnixNano())) pointCount := int(r.Int31n(100)) + 2 // Traverse at least two points var points []*pb.Point for i := 0; i < pointCount; i++ { points = append(points, randomPoint(r)) } log.Printf("Traversing %d points.", len(points)) stream, err := client.RecordRoute(context.Background()) if err != nil { log.Fatalf("%v.RecordRoute(_) = _, %v", client, err) } for _, point := range points { if err := stream.Send(point); err != nil { if err == io.EOF { break } log.Fatalf("%v.Send(%v) = %v", stream, point, err) } } reply, err := stream.CloseAndRecv() if err != nil { log.Fatalf("%v.CloseAndRecv() got error %v, want %v", stream, err, nil) } log.Printf("Route summary: %v", reply) RouteGuide_RecordRouteClient有一个Send()方法,可以用它向服务端发送请求。一旦使用Send()将客户端的请求向流中写入完成,就需要在流上调用CloseAndRecv()方法让gRPC知道我们已完成写入并期望收到响应。可以从CloseAndRecv()返回的错误err中获取RPC状态,如果状态为nil,那么CloseAndRecv()的第一个返回值将是有效的服务端响应。 0.6.2.4. 双向流数据RPC 最后是双向流数据RPCRouteChat()。与RecordRoute的情况一样,只给方法传递一个上下文对象,然后获得返回一个可用于写入和读取消息的流。但是,这次通过方法的流返回值,而服务端仍在向其消息流写入消息。 stream, err := client.RouteChat(context.Background()) waitc := make(chan struct{}) go func() { for { in, err := stream.Recv() if err == io.EOF { // read done. close(waitc) return } if err != nil { log.Fatalf("Failed to receive a note : %v", err) } log.Printf("Got message %s at point(%d, %d)", in.Message, in.Location.Latitude, in.Location.Longitude) } }() for _, note := range notes { if err := stream.Send(note); err != nil { log.Fatalf("Failed to send a note: %v", err) } } stream.CloseSend() <-waitc 这里的读写语法与客户端流方法的语法非常相似,只是在完成调用后使用流的CloseSend()方法。尽管每一方都会按照写入顺序获取对方的消息,但客户端和服务端都可以按任意顺序进行读写,这些流完全独立运行。 0.7. 搞起来 要编译和运行服务端,假设位于$GOPATH/src/google.golang.org/grpc/examples/route_guide文件夹中,只需: go run server/server.go 同样,要运行客户端: go run client/client.go
0.1. 生成服务端接口的方法 0.1.1. 简单方法 0.1.2. 服务端侧流数据方法 0.1.3. 客户端侧流数据方法 0.1.4. 双向流数据方法 0.2. 生成客户端接口的方法 0.2.1. 简单方法 0.2.2. 服务端侧流数据方法 0.2.3. 客户端侧流数据方法 0.2.4. 双向流数据方法 0.3. 包和命名空间 本指南描述了使用protoc编译.proto文件时在protoc-gen-go插件中使用grpc插件生成代码。 在服务定义章节中查看如何在.proto文件中定义一个服务。 线程安全:注意客户端RPC调用和服务端RPC处理都是线程安全的,这意味着能在并发的goroutine中运行。但是请注意,对于单个流来说,传入和传出数据是双向串行的,所以单个流不支持并发读取并发写入(但并发执行读入和写入是安全的)。 0.1. 生成服务端接口的方法 在服务端,.proto文件中的每个服务都会生成如下函数: func RegisterBarServer(s *grpc.Server, srv BarServer) 应用程序可以定义BarServer接口的具体实现,并使用此函数将其注册到grpc.Server实例(在启动服务端实例之前)。 0.1.1. 简单方法 这些方法在生成服务端接口时具有如下签名: Foo(context.Context, *MsgA) (*MsgB, error) 在这个上下文中: MsgA:是从客户端发送的protobuf消息 MsgB:是从服务端返回的protobuf消息 0.1.2. 服务端侧流数据方法 这些方法在生成服务端接口时具有如下签名: Foo(*MsgA, <ServiceName>_FooServer) error 在这个上下文中: MsgA:是从客户端发送的单个请求 <ServiceName>_FooServer:参数代表传送MsgB消息的,从服务端到客户端的流 <ServiceName>_FooServer有一个嵌入的grpc.ServerStream和下面的接口: type <ServiceName>_FooServer interface { Send(*MsgB) error grpc.ServerStream } 服务端的处理器可以通过这个参数的Send方法发送protobuf消息流到客户端。根据服务端处理器方法的返回结果来判断从服务端到客户端的流是否结束。 0.1.3. 客户端侧流数据方法 这些方法在生成服务端接口时具有如下签名: Foo(<ServiceName>_FooServer) error 在这个上下文中: <ServiceName>_FooServer: 既可以用来读取从客户端到服务端的消息流 也可以用来发送单个的服务响应消息 <ServiceName>_FooServer:有一个嵌入的grpc.ServerStream和下面的接口: type <ServiceName>_FooServer interface { SendAndClose(*MsgA) error Recv() (*MsgB, error) grpc.ServerStream } 服务端的处理器可以在这个参数上重复调用Recv()来接收从客户端发送过来的流中的全部消息。一旦到达流的末尾Recv()将返回(nil,io.EOF)。服务端单独的响应消息是通过调用<ServiceName>_FooServer参数中的SendAndClose()方法发送的。请注意,SendAndClose()方法能且仅能调用一次。 0.1.4. 双向流数据方法 这些方法在生成服务端接口时具有如下签名: Foo(<ServiceName>_FooServer) error 在这个上下文中: <ServiceName>_FooServer: 用于连接从客户端到服务端的消息流 也用于连接从服务端到客户端的消息流 <ServiceName>_FooServer有一个嵌入的grpc.ServerStream和下面的接口: type <ServiceName>_FooServer interface { Send(*MsgA) error Recv() (*MsgB, error) grpc.ServerStream } 服务端处理器可以在这个参数上重复调用Recv()方法来读取从客户端到服务端的消息流。一旦到达从客户端到服务端的流的末尾Recv()返回(nil,io.EOF)。 从服务端到客户端的响应消息流是通过重复调用<ServiceName>_FooServer参数的Send()方法发送出来的。bidi(双向)方法处理器的返回的结果表示从服务器到客户端流的结束。 0.2. 生成客户端接口的方法 对于客户端的使用来说,在.proto文件中的每个服务都会产生一下函数: func BarClient(cc *grpc.ClientConn) BarClient 它返回BarClient接口的具体实现(这个具体实现也存在于生成的.pb.go文件中)。 0.2.1. 简单方法 这些方法在生成客户端stub时具有如下签名: Foo(ctx context.Context, in *MsgA, opts ...grpc.CallOption) (*MsgB, error) 在这个上下文中: MsgA:从客户端到服务端的一个单一请求 MsgB:包含从服务端返回的响应信息 0.2.2. 服务端侧流数据方法 这些方法在生成客户端stub时具有如下签名: Foo(ctx context.Context, in *MsgA, opts ...grpc.CallOption) (<ServiceName>_FooClient, error) 在这个上下文中: <ServiceName>_FooClient:代表了从服务端到客户端的包含MsgB消息的流 这个流有一个嵌入的grpc.ClientStream和如下的接口: type <ServiceName>_FooClient interface { Recv() (*MsgB, error) grpc.ClientStream } 当客户端在stub上调用Foo方法时这个流就开始了。这个客户端可以在返回的<ServiceName>_FooClient流上重复调用Recv()方法,以便读取从服务端到客户端的响应消息流。一旦完全读取了服务端到客户端的流,Recv()方法返回(nil,io.EOF)。 0.2.3. 客户端侧流数据方法 这些方法在生成客户端stub时具有如下签名: Foo(ctx context.Context, opts ...grpc.CallOption) (<ServiceName>_FooClient, error) 在这个上下文中: <ServiceName>_FooClient:代表从客户端到服务端的包含MsgA消息的流 <ServiceName>_FooClient:有一个嵌入的grpc.ClientStream和下面的接口: type <ServiceName>_FooClient interface { Send(*MsgA) error CloseAndRecv() (*MsgA, error) grpc.ClientStream } 当客户端在stub上调用Foo方法时流开始。客户端会在返回的<ServiceName>_FooClient流上重复调用Send()方法,以便发送从客户端到服务端的消息流。这个流的CloseAndRecv()方法能且仅能调用一次,以便能够在关闭从客户端到服务端流的同时接收从服务端返回的单个响应消息。 0.2.4. 双向流数据方法 这些方法在生成客户端stub时具有如下签名: Foo(ctx context.Context, opts ...grpc.CallOption) (<ServiceName>_FooClient, error) 在这个上下文中: <ServiceName>_FooClient:代表从客户端到服务端的消息流,也代表从服务端到客户端的消息流 <ServiceName>_FooClient有一个嵌入的grpc.ClientStream和如下的接口: type <ServiceName>_FooClient interface { Send(*MsgA) error Recv() (*MsgB, error) grpc.ClientStream } 当客户端在stub上调用Foo方法时流开始。客户端可以在返回的<ServiceName>_FooClient上重复调用Send()方法,以便发送从客户端到服务端的消息流。客户端也可以在这个流上重复调用Recv()方法,以便接收从服务端到客户端的消息流中全部消息。 服务端到客户端流的结束由流的Recv()方法上的返回值(nil,io.EOF)指示。 从客户端到服务端流的结束由客户端通过在流上调用CloseSend()方法指示。 0.3. 包和命名空间 使用 --go_out=plugins=grpc: 调用 protoc 编译器时,从 proto 包到 Go 包的转换与使用protoc-gen-go插件而不使用grpc插件的工作方式相同。 因此,例如,如果foo.proto声明自己在包foo中,那么生成的foo.pb.go文件也将在Go包foo中。