导航

编写一个go gRPC的服务

Posted on 2016-04-13 15:46  蝈蝈俊  阅读(3923)  评论(1编辑  收藏  举报

前置条件:

获取 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:

image

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

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

简单RPC

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

image

服务器端流式 RPC

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

image

客户端流式 RPC

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

image

双向流式 RPC

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

image

消息类型

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

image

 

生成服务器和客户端代码

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

 

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

 

image

运行 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.

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

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

 

服务器端流式 RPC

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

image

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

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

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

 

客户端流式 RPC

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

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

image

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

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

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

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

 

双向流式 RPC

双向流式 RPC RouteChat()。

image

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

 

启动服务器

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

image

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

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

 

创建客户端

建立跟服务器的连接

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

image

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

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

调用服务器方法

简单RPC

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

image

服务器端流式 RPC

image

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

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

客户端流式 RPC

image

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


双向流式 RPC

image

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

运行例子

服务器端:

$ go run server/server.go

客户端:

$ go run client/client.go

 

参考资料:

中文 gRPC 基础 Go   http://doc.oschina.net/grpc?t=60133 

英文 gRPC 基础 Go  http://www.grpc.io/docs/tutorials/basic/go.html