本教程提供了Go使用gRPC的基础教程git
在教程中你将会学到如何:github
.proto
文件中定义一个服务。继续以前,请确保你已经对gRPC概念有所了解,而且熟悉protocol buffer。须要注意的是教程中的示例使用的是proto3
版本的protocol buffer:你能够在Protobuf语言指南与Protobuf生成Go代码指南中了解到更多相关知识。golang
咱们的示例是一个简单的路线图应用,客户端能够获取路线特征信息、建立他们的路线摘要,还能够与服务器或者其余客户端交换好比交通状态更新这样的路线信息。数据库
借助gRPC,咱们能够在.proto
文件中定义咱们的服务,并以gRPC支持的任何语言来实现客户端和服务器,客户端和服务器又能够在从服务器到你本身的平板电脑的各类环境中运行-gRPC还会为你解决全部不一样语言和环境之间通讯的复杂性。咱们还得到了使用protocol buffer的全部优势,包括有效的序列化(速度和体积两方面都比JSON更有效率),简单的IDL(接口定义语言)和轻松的接口更新。服务器
首先须要安装gRPC golang版本的软件包,同时官方软件包的examples
目录里就包含了教程中示例路线图应用的代码。markdown
$ go get google.golang.org/grpc
复制代码
而后切换到``grpc-go/examples/route_guide:
目录:app
$ cd $GOPATH/src/google.golang.org/grpc/examples/route_guide 复制代码
安装编译器最简单的方式是去https://github.com/protocolbuffers/protobuf/releases 下载预编译好的protoc二进制文件,仓库中能够找到每一个平台对应的编译器二进制文件。这里咱们以Mac Os
为例,从https://github.com/protocolbuffers/protobuf/releases/download/v3.6.0/protoc-3.6.0-osx-x86_64.zip 下载并解压文件。dom
更新PATH
系统变量,或者确保protoc
放在了PATH
包含的目录中了。tcp
$ go get -u github.com/golang/protobuf/protoc-gen-go
复制代码
编译器插件protoc-gen-go
将安装在$GOBIN
中,默认位于$GOPATH/bin
。编译器protoc
必须在$PATH
中能找到它:ide
$ export PATH=$PATH:$GOPATH/bin 复制代码
首先第一步是使用protocol buffer定义gRPC服务还有方法的请求和响应类型,你能够在下载的示例代码examples/route_guide/routeguide/route_guide.proto
中看到完整的.proto
文件。
要定义服务,你须要在.proto
文件中指定一个具名的service
service RouteGuide { ... } 复制代码
而后在服务定义中再来定义rpc
方法,指定他们的请求和响应类型。gRPC容许定义四种类型的服务方法,这四种服务方法都会应用到咱们的RouteGuide
服务中。
// 得到给定位置的特征 rpc GetFeature(Point) returns (Feature) {} 复制代码
//得到给定Rectangle中可用的特征。结果是 //流式传输而不是当即返回 //由于矩形可能会覆盖较大的区域并包含大量特征。 rpc ListFeatures(Rectangle) returns (stream Feature) {} 复制代码
// 接收路线上被穿过的一系列点位, 当行程结束时 // 服务端会返回一个RouteSummary类型的消息. rpc RecordRoute(stream Point) returns (RouteSummary) {} 复制代码
//接收路线行进中发送过来的一系列RouteNotes类型的消息,同时也接收其余RouteNotes(例如:来自其余用户) rpc RouteChat(stream RouteNote) returns (stream RouteNote) {} 复制代码
咱们的.proto
文件中也须要全部请求和响应类型的protocol buffer消息类型定义。好比说下面的Point
消息类型:
// Points被表示为E7表示形式中的经度-纬度对。 //(度数乘以10 ** 7并四舍五入为最接近的整数)。 // 纬度应在+/- 90度范围内,而经度应在 // 范围+/- 180度(含) message Point { int32 latitude = 1; int32 longitude = 2; } 复制代码
接下来要从咱们的.proto
服务定义生成gRPC客户端和服务端的接口。咱们使用protoc
编译器和上面安装的编译器插件来完成这些工做:
在示例route_guide
的目录下运行:
protoc -I routeguide/ routeguide/route_guide.proto --go_out=plugins=grpc:routeguide
复制代码
运行命令后会在示例route_guide
目录的routeguide
目录下生成route_guide.pb.go
文件。
pb.go
文件里面包含:
RouteGuide
服务中定义的方法。RouteGuideServer
,接口类型中包含了RouteGuide
服务中定义的全部方法。首先让咱们看一下怎么建立RouteGuide
服务器。有两种方法来让咱们的RouteGuide
服务工做:
你能够在刚才安装的gPRC包的grpc-go/examples/route_guide/server/server.go找到咱们示例中RouteGuide`服务的实现代码。下面让咱们看看他是怎么工做的。
如你所见,实现代码中有一个routeGuideServer
结构体类型,它实现了protoc
编译器生成的pb.go
文件中定义的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 { ... } ... 复制代码
routeGuideServer
实现咱们全部的服务方法。首先,让咱们看一下最简单的类型GetFeature
,它只是从客户端获取一个Point
,并从其Feature
数据库中返回相应的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 buffer请求消息,它在响应信息中返回一个Feature
类型的protocol buffer消息和错误。在该方法中,咱们使用适当的信息填充Feature
,而后将其返回并返回nil
错误,以告知gRPC咱们已经完成了RPC的处理,而且能够将`Feature返回给客户端。
如今,让咱们看一下服务方法中的一个流式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_ListFeaturesServe
r对象来写入响应。
在该方法中,咱们填充了须要返回的全部Feature
对象,并使用Send()
方法将它们写入RouteGuide_ListFeaturesServer
。最后,就像在简单的RPC中同样,咱们返回nil
错误来告诉gRPC咱们已经完成了响应的写入。若是此调用中发生任何错误,咱们将返回非nil
错误; gRPC层会将其转换为适当的RPC状态,以在线上发送。
如今,让咱们看一些更复杂的事情:客户端流方法RecordRoute
,从客户端获取点流,并返回一个包含行程信息的RouteSummary
。如你所见,这一次该方法根本没有request
参数。相反,它得到一个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()
返回的错误。若是为nil
,则流仍然良好,而且能够继续读取;若是是io.EOF,则表示消息流已结束,服务器能够返回其RouteSummary。若是错误为其余值,咱们将返回错误“原样”,以便gRPC层将其转换为RPC状态。
最后让咱们看一下双向流式RPC方法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) s.mu.Lock() s.routeNotes[key] = append(s.routeNotes[key], in) // Note: this copy prevents blocking other clients while serving this one. // We don't need to do a deep copy, because elements in the slice are // insert-only and never modified. rn := make([]*pb.RouteNote, len(s.routeNotes[key])) copy(rn, s.routeNotes[key]) s.mu.Unlock() for _, note := range rn { if err := stream.Send(note); err != nil { return err } } } } 复制代码
此次,咱们获得一个RouteGuide_RouteChatServer
流,就像在客户端流示例中同样,该流可用于读取和写入消息。可是,此次,当客户端仍在向其消息流中写入消息时,咱们会向流中写入要返回的消息。
此处的读写语法与咱们的客户端流式传输方法很是类似,不一样之处在于服务器使用流的Send()
方法而不是SendAndClose()
,由于服务器会写入多个响应。尽管双方老是会按照对方的写入顺序来获取对方的消息,可是客户端和服务器均可以以任意顺序进行读取和写入-流彻底独立地运行(意思是服务器能够接受完请求后再写流,也能够接收一条请求写一条响应。一样的客户端能够写完请求了再读响应,也能够发一条请求读一条响应)
一旦实现了全部方法,咱们还须要启动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{}) ... // determine whether to use TLS grpcServer.Serve(lis) 复制代码
为了构建和启动服务器咱们须要:
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))。
grpc.NewServer()
建立一个gRPC server的实例。Serve()
进行阻塞等待,直到进程被杀死或调用Stop()
为止。在这一部分中咱们将为RouteGuide
服务建立Go客户端,你能够在grpc-go/examples/route_guide/client/client.go 看到完整的客户端代码。
要调用服务的方法,咱们首先须要建立一个gRPC通道与服务器通讯。咱们经过把服务器地址和端口号传递给grpc.Dial()
来建立通道,像下面这样:
conn, err := grpc.Dial(*serverAddr) if err != nil { ... } defer conn.Close() 复制代码
若是你请求的服务须要认证,你能够在grpc.Dial
中使用DialOptions
设置认证凭证(好比:TLS,GCE凭证,JWT凭证)--不过咱们的RouteGuide
服务不须要这些。
设置gRPC通道后,咱们须要一个客户端存根来执行RPC。咱们使用从.proto
生成的pb
包中提供的NewRouteGuideClient
方法获取客户端存根。
client := pb.NewRouteGuideClient(conn)
复制代码
生成的pb.go
文件定义了客户端接口类型RouteGuideClient
并用客户端存根的结构体类型实现了接口中的方法,因此经过上面获取到的客户端存根client
能够直接调用下面接口类型中列出的方法。
type RouteGuideClient interface { GetFeature(ctx context.Context, in *Point, opts ...grpc.CallOption) (*Feature, error) ListFeatures(ctx context.Context, in *Rectangle, opts ...grpc.CallOption) (RouteGuide_ListFeaturesClient, error) RecordRoute(ctx context.Context, opts ...grpc.CallOption) (RouteGuide_RecordRouteClient, error) RouteChat(ctx context.Context, opts ...grpc.CallOption) (RouteGuide_RouteChatClient, error) } 复制代码
每一个实现方法会再去请求gRPC服务端相对应的方法获取服务端的响应,好比:
func (c *routeGuideClient) GetFeature(ctx context.Context, in *Point, opts ...grpc.CallOption) (*Feature, error) { out := new(Feature) err := c.cc.Invoke(ctx, "/routeguide.RouteGuide/GetFeature", in, out, opts...) if err != nil { return nil, err } return out, nil } 复制代码
RouteGuideClient
接口的完整实现能够在生成的pb.go
文件里找到。
如今让咱们看看如何调用服务的方法。注意在gRPC-Go中,PRC是在阻塞/同步模式下的运行的,也就是说RPC调用会等待服务端响应,服务端将返回响应或者是错误。
调用普通RPC方法GetFeature
如同直接调用本地的方法。
feature, err := client.GetFeature(context.Background(), &pb.Point{409146138, -746188906}) if err != nil { ... } 复制代码
如你所见,咱们在以前得到的存根上调用该方法。在咱们的方法参数中,咱们建立并填充一个protocol buffer对象(在本例中为Point对象)。咱们还会传递一个context.Context
对象,该对象可以让咱们在必要时更改RPC的行为,例如超时/取消正在调用的RPC(cancel an RPC in flight)。若是调用没有返回错误,则咱们能够从第一个返回值中读取服务器的响应信息。
这里咱们会调用服务端流式方法ListFeatures
,方法返回的流中包含了地理特征信息。若是你读过上面的建立客户端的章节,这里有些东西看起来会很熟悉--流式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 buffer响应对象中(本例中的Feature
对象),直到没有更多消息为止:客户端须要在每次调用后检查从Recv()
返回的错误err
。若是为nil
,则流仍然良好,而且能够继续读取;若是是io.EOF
,则消息流已结束;不然就是必定RPC错误,该错误会经过err
传递给调用程序。
客户端流方法RecordRoute
与服务器端方法类似,不一样之处在于,咱们仅向该方法传递一个上下文并得到一个RouteGuide_RecordRouteClient
流,该流可用于写入和读取消息。
// 随机的建立一些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())// 调用服务中定义的客户端流式RPC方法 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()`的第一个返回值就是一个有效的服务器响应。
最后,让咱们看一下双向流式RPC RouteChat()
。与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()
方法外,此处的读写语法与咱们的客户端流方法很是类似。尽管双方老是会按照对方的写入顺序来获取对方的消息,可是客户端和服务器均可以以任意顺序进行读取和写入-两端的流彻底独立地运行。
要编译和运行服务器,假设你位于$ GOPATH/src/google.golang.org/grpc/examples/route_guide
文件夹中,只需:
$ go run server/server.go
复制代码
一样,运行客户端:
$ go run client/client.go
复制代码