前置条件:
获取 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_ListFeaturesClient
的 Recv()
方法去反复读取服务器的响应到一个响应 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
参考资料:
中文 gRPC 基础 Go http://doc.oschina.net/grpc?t=60133
英文 gRPC 基础 Go http://www.grpc.io/docs/tutorials/basic/go.html