编写一个go gRPC的服务

时间:2022-05-05
本文章向大家介绍编写一个go gRPC的服务,主要内容包括定义服务、服务器端流式 RPC、客户端流式 RPC、双向流式 RPC、消息类型、生成服务器和客户端代码、创建服务器、启动服务器、创建客户端、调用服务器方法、运行例子、基本概念、基础应用、原理机制和需要注意的事项等,并结合实例形式分析了其使用技巧,希望通过本文能帮助到大家理解应用这部分内容。

前置条件:

获取 gRPC-go 源码

$ go get google.golang.org/grpc

简单例子的源码位置:

$ cd $GOPATH/src/google.golang.org/grpc/examples/helloworld

复杂些例子的源码位置:

$ cd $GOPATH/src/google.golang.org/grpc/examples/route_guide

写一个gRPC的服务,一般分下面几步:

  • 在一个 .proto 文件内定义服务。
  • 用 protocol buffer 编译器生成服务器和客户端代码。
  • 使用 gRPC 的 Go API 为你的服务实现一个简单的客户端和服务器。

定义服务

在一个 .proto 文件中定义服务

简单的例子服务定义在: examples/helloworld/helloworld/helloworld.proto

route_guide 的定义在 : examples/route_guide/routeguide/route_guide.proto

要定义一个服务,你必须在你的 .proto 文件中指定 service:

然后在你的服务中定义 rpc 方法,指定请求的和响应类型。

gRPC 允许你定义4种类型的 service 方法,这些都在 RouteGuide 服务中使用到了:

简单RPC

一个 简单 RPC , 客户端发送带参请求到服务器并等待响应返回,就像平常的函数调用一样。

服务器端流式 RPC

一个 服务器端流式 RPC , 客户端发送请求到服务器,拿到一个流去读取返回的消息序列。 客户端读取返回的流,直到里面没有任何消息。从例子中可以看出,通过在 响应 类型前插入 stream 关键字,可以指定一个服务器端的流方法。

客户端流式 RPC

一个 客户端流式 RPC , 客户端写入一个消息序列并将其发送到服务器,同样也是使用流。一旦客户端完成写入消息,它等待服务器完成读取返回它的响应。通过在 请求 类型前指定 stream 关键字来指定一个客户端的流方法。

双向流式 RPC

一个 双向流式 RPC 是双方使用读写流去发送一个消息序列。两个流独立操作,因此客户端和服务器可以以任意喜欢的顺序读写:比如, 服务器可以在写入响应前等待接收所有的客户端消息,或者可以交替的读取和写入消息,或者其他读写的组合。 每个流中的消息顺序被预留。你可以通过在请求和响应前加 stream 关键字去制定方法的类型。

消息类型

上面看起来像函数参数、返回值得,由于要涉及到跨服务器调用,这些其实传递的是消息。我们的 .proto 文件也包含了所有请求的 protocol buffer 消息类型定义以及在服务方法中使用的响应类型——比如,下面的Point消息类型:

生成服务器和客户端代码

我们需要通过 protocol buffer 的编译器 protoc 以及一个特殊的 gRPC Go 插件来完成用 protocol buffer 编译器生成服务器和客户端代码。

简单期间,有个 bash 脚本可以帮我们生成合适的代码 codegen.sh (https://github.com/grpc/grpc-go/blob/master/codegen.sh)

运行 codegen.sh route_guide.proto 就可以在当前目录下产生 route_guide.pb.go 文件。

在这个产生的文件中, 既有 routeGuideClient 客户端代码部分, 也有 routeGuideRouteChatServer 服务器段代码部分。

创建服务器

这部分的源码在: grpc-go/examples/route_guide/server/server.go

让 RouteGuide 服务工作有两个部分:

  • 实现我们服务定义的生成的服务接口:做我们的服务的实际的“工作”。
  • 运行一个 gRPC 服务器,监听来自客户端的请求并返回服务的响应。

实现RouteGuide

在源码中,我们可以看到实现了接口RouteGuideServer的routeGuideServer数据结构。 这个接口是在route_guide.pb.go中自动产生的。

简单RPC

GetFeature,它从客户端拿到一个 Point 对象,然后从返回包含从数据库拿到的feature信息的 Feature.

该方法传入了 RPC 的上下文对象,以及客户端的 Point 参数。它返回了Feature 响应信息和error信息。

在方法中我们遍历所有服务器端保存的信息,找到位置信息匹配的,然后将其和一个nil错误一起返回给客户端。

服务器端流式 RPC

ListFeatures 是一个服务器端的流式 RPC,我们将多个 Feature 发回给客户端。

这里的请求参数是一个 Rectangle,客户端期望返回多个 Feature,这次我们使用了一个请求对象和一个特殊的RouteGuide_ListFeaturesServer来写入我们的响应,而不是得到方法参数中的入参和返回值。 在这个方法中,我们填充了尽可能多的 Feature 对象去返回,用steam的 Send() 方法把它们写入 RouteGuide_ListFeaturesServer。

最后,我们返回了一个 nil 错误告诉 gRPC 响应的写入已经完成。

如果在调用过程中发生任何错误,我们会返回一个非 nil 的错误;

客户端流式 RPC

客户端流方法 RecordRoute,我们通过它可以从客户端拿到一个 Point 的流,其中包括我们需要的Point信息。

这次这个方法用了一个 RouteGuide_RecordRouteServer 流,服务器可以用它来同时读 和 写消息——它可以用自己的 Recv() 方法接收客户端消息并且用 SendAndClose() 方法返回它的单个响应。

在方法体中,我们使用 RouteGuide_RecordRouteServer 的 Recv() 方法去反复读取客户端的请求到一个请求对象(在这个场景下是 Point),直到没有更多的消息。

服务器需要在每次调用后检查 Read() 返回的错误。如果返回值为 nil,流依然完好,可以继续读取;

如果返回值为 io.EOF,消息流结束,服务器可以返回它的 RouteSummary。

如果它还有其它值,我们原样返回错误,gRPC 层会把它转换为 RPC 状态。

双向流式 RPC

双向流式 RPC RouteChat()。

这里读写的语法和客户端流方法相似,除了服务器会使用流的 Send() 方法而不是 SendAndClose(),因为它需要写多个响应。虽然客户端和服务器端总是会拿到对方写入时顺序的消息,它们可以以任意顺序读写——流的操作是完全独立的。

启动服务器

一旦我们实现了所有的方法,我们还需要启动一个gRPC服务器,这样客户端才可以使用服务。

为了构建和启动服务器,我们需要:

  • 使用 lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port)) 指定我们期望客户端请求的监听端口。
  • 使用grpc.NewServer()创建 gRPC 服务器的一个实例。
  • 在 gRPC 服务器注册我们的服务实现。
  • 用服务器 Serve() 方法以及我们的端口信息区实现阻塞等待,直到进程被杀死或者 Stop() 被调用。

创建客户端

建立跟服务器的连接

为了调用服务方法,我们首先创建一个 gRPC conn。我们通过给 grpc.Dial() 传入服务器地址和端口号做到这点,如下:

你可以使用 DialOptions 在 grpc.Dial 中设置授权认证(如, TLS,GCE认证,JWT认证),如果服务有这样的要求的话 —— 但是对于 RouteGuide 服务,我们不用这么做。

一旦 gRPC conn 建立起来,我们需要一个client去执行 RPC。我们通过 .proto 生成的 pb 包提供的 NewRouteGuideClient 方法来完成。

调用服务器方法

简单RPC

调用简单 RPC GetFeature 几乎是和调用一个本地方法一样直观。

服务器端流式 RPC

我们给方法传入一个上下文和请求。然而,我们得到返回的是一个 RouteGuide_ListFeaturesClient 实例,而不是一个应答对象。客户端可以使用 RouteGuide_ListFeaturesClient 流去读取服务器的响应。

我们使用 RouteGuide_ListFeaturesClientRecv() 方法去反复读取服务器的响应到一个响应 protocol buffer 对象(在这个场景下是Feature)直到消息读取完毕:每次调用完成时,客户端都要检查从 Recv() 返回的错误 err。如果返回为 nil,流依然完好并且可以继续读取;如果返回为 io.EOF,则说明消息流已经结束;否则就一定是一个通过 err传过来的 RPC 错误。

客户端流式 RPC

RouteGuide_RecordRouteClient 有一个 Send() 方法,我们可以用它来给服务器发送请求。一旦我们完成使用 Send() 方法将客户端请求写入流,就需要调用流的 CloseAndRecv()方法,让 gRPC 知道我们已经完成了写入同时期待返回应答。我们从 CloseAndRecv() 返回的 err 中获得 RPC 的状态。如果状态为nil,那么CloseAndRecv()的第一个返回值将会是合法的服务器应答。

双向流式 RPC

我们只给函数传入一个上下文对象,拿到可以用来读写的流。

运行例子

服务器端:

$ go run server/server.go

客户端:

$ go run client/client.go