一、gRPC是什么?
gRPC的官方文档:https://grpc.io/docs/
gRPC可以使用协议缓冲区作为其接口定义语言(IDL)和底层消息交换格式,是一个高性能、开源和通用的RPC框架,面向服务端和移动端,基于HTTP/2设计。它使客户端和服务器应用程序能够透明地通信,并使构建连接系统变得更加容易。
简介
概述
在gRPC中,客户端应用程序可以直接调用不同机器上的服务端应用程序上的方法,就想调用本地对象一样,可以更轻松地创建分布式应用程序和服务。与许多RPC系统一样,gRPC基于定义服务的思想,指定可以通过参数和返回类型远程调用的方法。在服务器端,服务器实现了这个接口并运行一个gRPC服务器来处理客户端调用。在客户端,客户端有一个存根(在某些语言中简称为客户端),它提供与服务器相同的方法。
gPRC客户端和服务器可以在各种环境中运行和相互通信 - 从 Google 内部的服务器到您自己的桌面 - 并且可以用任何 gRPC 支持的语言编写。因此,例如,您可以轻松地使用 Java、Go、Python 或 Ruby 中的客户端创建一个 gRPC 服务器。此外,最新的 Google API 将具有其接口的 gRPC 版本,让您可以轻松地将 Google 功能构建到您的应用程序中。
Protocol Buffers 协议缓冲区
gRPC使用Protocol Buffers(基于Google开源的结构数据序列化工具)。使用Protocol Buffers的第一步就是要在proto文件中序列化数据结构:这是一个带有.proto扩展名的普通文本文件。协议缓冲区数据被构造为message,其中每条消息都是一个小的信息逻辑记录,包含一系列称为字段的name-value
message Person{ string name = 1; int32 id = 2; bool has_ponycopter = 3; }
一旦您指定了数据结构,您就可以使用协议缓冲区编译器protoc
根据您的 proto 定义以您的首选语言(例如:go、java、python、csharp等)生成数据访问类。这些为每个字段提供了简单的访问器,如name()
和set_name()
,以及将整个结构序列化/解析为原始字节/从原始字节序列化/解析整个结构的方法。
在普通的 proto 文件中定义 gRPC 服务,并将 RPC 方法参数和返回类型指定为协议缓冲区消息:
// The greeter 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; }
Protocol Buffer 比 XML、JSON快很多,因为是基于二进制流,比字符串更省带宽,传输速度快。
Protocol Buffers 版本
一般来说,我们都是使用proto3,这样可以使用所有gRPC支持的语言,并且避免与proto2客户端通信的兼容性问题。
syntax = "proto3";
核心概念
服务定义
与许多 RPC 系统一样,gRPC 基于定义服务的思想,指定可以通过参数和返回类型远程调用的方法。默认情况下,gRPC 使用协议缓冲区作为接口定义语言 (IDL),用于描述服务接口和有效载荷消息的结构。
gRPC允许定义四种服务方法:
-
一元RPC,客户端向服务端发送单个请求并返回单个响应,就像普通的函数调用一样。
rpc SayHello(HelloRequest) returns (HelloResponse);
-
服务端stream RPC,客户端向服务器发送请求并获取stream以读取一系列消息。客户端从返回的stream中读取,直到没有更多消息。gRPC能保证单个RPC调用中的消息排序。
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse);
-
客户端stream RPC,客户端写入一系列消息并将它们发送到服务器,再次使用提供的stream。一旦客户端完成写入消息,它等待服务器读取它们并返回其响应。gRPC能保证单个RPC调用中的消息排序。
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse);
-
双向stream PRC,其中双方使用读写stream发送一系列消息。这两个stream独立运行,因此客户端和服务端可以按照它们喜欢的任何顺序进行读写。
rpc BidiHello(stream HelloRequest) returns (stream HelloResponse);
使用API
从.proto
文件中的服务定义开始,gRPC 提供了协议缓冲区编译器插件,用于生成客户端和服务器端代码。gRPC 用户通常在客户端调用这些 API,并在服务器端实现相应的 API。
-
在服务器端,服务器实现服务声明的方法并运行 gRPC 服务器来处理客户端调用。gRPC 基础设施解码传入请求、执行服务方法并编码服务响应。
-
在客户端,客户端有一个称为stub(对于某些语言,首选术语是client)的本地对象,它实现与服务相同的方法。然后客户端可以在本地对象上调用这些方法,将调用的参数包装在适当的协议缓冲区消息类型中 - gRPC 负责将请求发送到服务器并返回服务器的协议缓冲区响应。
同步与异步
在服务器响应到达之前阻塞的同步 RPC 调用最接近于 RPC 所追求的过程调用的抽象。另一方面,网络本质上是异步的,在许多情况下,能够在不阻塞当前线程的情况下启动 RPC 很有用。
大多数语言中的 gRPC 编程 API 都有同步和异步两种风格。
RPC生命周期
了解当 gRPC 客户端调用 gRPC 服务器方法时会发生什么
一元RPC
首先考虑客户端发送单个请求并返回单个响应的最简单的 RPC 类型。
-
一旦客户端调用了存根方法,服务器就会收到通知:RPC 已经被调用,其中包含客户端的元数据 、方法名称和指定的截止日期(如果适用)。
-
然后服务器可以立即发回它自己的初始元数据(必须在任何响应之前发送),或者等待客户端的请求消息。首先发生的是特定于应用程序的。
-
一旦服务器收到客户端的请求消息,它就会执行创建和填充响应所需的任何工作。然后将响应(如果成功)连同状态详细信息(状态代码和可选状态消息)和可选的尾随元数据一起返回给客户端。
-
如果响应状态为OK,则客户端得到响应,在客户端完成调用。
服务器流式RPC
服务器流式 RPC 类似于一元 RPC,不同之处在于服务器返回消息流以响应客户端的请求。发送完所有消息后,服务器的状态详细信息(状态代码和可选状态消息)和可选的尾随元数据将发送到客户端。这样就完成了服务器端的处理。一旦客户端拥有服务器的所有消息,它就完成了。
客户端流式 RPC
客户端流式 RPC 类似于一元 RPC,不同之处在于客户端向服务器发送消息流而不是单个消息。服务器用一条消息(连同它的状态详细信息和可选的尾随元数据)进行响应,通常但不一定是在它收到所有客户端的消息之后。
双向流式RPC
在双向流式 RPC 中,调用由调用方法的客户端和接收客户端元数据、方法名称和截止日期的服务器发起。服务器可以选择发回其初始元数据或等待客户端开始流式传输消息。
客户端和服务器端流处理是特定于应用程序的。由于两个流是独立的,客户端和服务器可以按任意顺序读写消息。例如,服务器可以等到收到所有客户端的消息后再写入消息,或者服务器和客户端可以玩“乒乓”——服务器收到请求,然后发回响应,然后客户端发送基于响应的另一个请求,依此类推。
二、总结
gRPC的特性
-
使用Protocol Buffers结构数据序列化工具
-
可以跨语言使用
-
安装简单、扩展方便(每秒可达到百万个RPC)
-
基于HTTP/2协议
gRPC使用流程
-
定义标准的proto文件
-
通过proto工具生成标准代码
-
服务端使用生成的代码提供服务
-
客户端使用生成的代码调用服务
为什么要使用gRPC?
主要使用场景:
-
低延迟、高度可扩展的分布式系统
-
开发与云服务器通信的移动客户端
-
设计一个需要准确、高效且独立于语言的新协议
-
分层设计以启用扩展,例如:身份验证、负载平衡、日志记录和监控等。
三、实践
关于protoc和protoc-gen-go的安装见上一篇文章。
新建一个go项目,项目目录如下:
goGrpcDemo │ client.go │ go.mod │ go.sum │ server.go │ ├─.idea │ .gitignore │ encodings.xml │ goGrpcDemo.iml │ misc.xml │ modules.xml │ runConfigurations.xml │ workspace.xml │ ├─cmd │ gen-golang.sh │ └─proto hello.pb.go hello.proto
proto文件
hello.proto代码如下:
syntax = "proto3"; package proto; option go_package = "../proto"; service HelloWorld { rpc SayHelloWorld(HelloWorldRequest) returns (HelloWorldResponse) {} rpc SayGoodNight(HelloWorldRequest) returns (HelloWorldResponse) {} } message HelloWorldRequest { string name = 1; } message HelloWorldResponse { string message = 1; }
在go1.15+以上,是需要go_package的,否则生成会报错;
而hello.pb.go是通过hello.proto使用命令生成的,命令我写在了gen-golang.sh中:
#!/usr/bin/env bash protoDir="../proto" outDir="../proto" protoc -I ${protoDir}/ ${protoDir}/*proto --go_out=plugins=grpc:${outDir}
protoc工具参数介绍:
-
-I:指定要引入proto文件的路径,可以指定多个 -I 参数,编译时按顺序查找,不指定默认当前目录
-
--go_out:指定go语言的访问类
-
plugins:指定依赖的插件
由于是在window上,我们可以使用git bash客户端运行.sh文件,如下:
定义服务端
server.go代码如下:
package main import ( "context" "fmt" "goGrpcDemo/proto" "google.golang.org/grpc" "google.golang.org/grpc/reflection" "log" "net" ) const port = ":50000" type server struct { proto.UnimplementedHelloWorldServer } // SayHelloWorld 服务端必须实现的接口 func (s *server) SayHelloWorld(ctx context.Context, request *proto.HelloWorldRequest) (*proto.HelloWorldResponse, error) { fmt.Printf("%s 说:Hello World", request.Name) return &proto.HelloWorldResponse{Message: "success"}, nil } func (s *server) SayGoodNight(ctx context.Context, request *proto.HelloWorldRequest) (*proto.HelloWorldResponse, error) { fmt.Printf("%s 说:GoodNight", request.Name) return &proto.HelloWorldResponse{Message: "success"}, nil } func main() { lis, err := net.Listen("tcp", port) if err != nil { log.Fatalf("failed to listen:%v", err) } s := grpc.NewServer() reflection.Register(s) // 为了后续使用grpcurl测试 proto.RegisterHelloWorldServer(s, &server{}) if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve:%v", err) } }
定义客户端
client.go代码如下:
package main import ( "context" "fmt" "goGrpcDemo/proto" "google.golang.org/grpc" "log" "time" ) const address = "localhost:50000" func main() { conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock()) if err != nil { log.Fatalf("not connect:%v", err) } defer conn.Close() // 客户端 client := proto.NewHelloWorldClient(conn) ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() // 调用SayHelloWorld方法 r, err := client.SayHelloWorld(ctx, &proto.HelloWorldRequest{ Name: "cxt", }) if err != nil { log.Fatalf("error:%v", err) } fmt.Printf("接收服务端返回消息: %s", r.Message) }
先运行服务端代码(server.go)再通过客户端像服务端发起请求,结果如下:
四、grpcurl命令工具使用
安装
使用go get命令进行安装
go get -u github.com/fullstorydev/grpcurl # go get 获取包成功后,进入github.com/fullstorydev/grpcurl/cmd/grpcurl目录下,执行 go install # 这样在gopath下的bin目录就会生成grpcurl.exe
使用
由于grpcurl是基于反射的,可以看到我们在server.go中加入了这样一行代码
reflection.Register(s)
常用命令如下:
# 1、查询服务列表 grpcurl -plaintext 127.0.0.1:50000 list # 2、查询服务提供的方法 grpcurl -plaintext 127.0.0.1:50000 list proto.HelloWorld # 3、服务提供的方法更详细的描述 grpcurl -plaintext 127.0.0.1:50000 describe proto.HelloWorld # 4、获取服务方法的请求类型信息 grpcurl -plaintext 127.0.0.1:50000 describe proto.HelloWorldRequest # 5、调用服务的方法 grpcurl -plaintext -d '{"name":"cxt"}' 127.0.0.1:50000 proto.HelloWorld/SayHelloWorld
五、grpcui界面工具使用
安装
使用go get命令安装
go get -u github.com/fullstorydev/grpcui # go get 获取包成功后,进入github.com/fullstorydev/grpcui/cmd/grpcui,执行 go install # 这样在gopath下的bin目录就会生成grpcui.exe
使用
常用命令如下:
# 运行web界面,然后使用提示的链接在浏览器打开 grpcui -plaintext 127.0.0.1:50000
页面很简单,就跟我们常见的postman类似的操作:
撒花,就不一一截图了,自个研究下grpcui的界面。