用Golang构建gRPC服务

本教程提供了Go使用gRPC的基础教程git

在教程中你将会学到如何:github

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

继续以前,请确保你已经对gRPC概念有所了解,而且熟悉protocol buffer。须要注意的是教程中的示例使用的是proto3版本的protocol buffer:你能够在Protobuf语言指南Protobuf生成Go代码指南中了解到更多相关知识。golang

为何使用gRPC

咱们的示例是一个简单的路线图应用,客户端能够获取路线特征信息、建立他们的路线摘要,还能够与服务器或者其余客户端交换好比交通状态更新这样的路线信息。数据库

借助gRPC,咱们能够在.proto文件中定义咱们的服务,并以gRPC支持的任何语言来实现客户端和服务器,客户端和服务器又能够在从服务器到你本身的平板电脑的各类环境中运行-gRPC还会为你解决全部不一样语言和环境之间通讯的复杂性。咱们还得到了使用protocol buffer的全部优势,包括有效的序列化(速度和体积两方面都比JSON更有效率),简单的IDL(接口定义语言)和轻松的接口更新。服务器

安装

安装grpc包

首先须要安装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
复制代码

安装相关工具和插件

  • 安装protocol buffer编译器

安装编译器最简单的方式是去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

  • 安装protoc编译器插件
$ 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,客户端使用存根将请求发送到服务器,而后等待响应返回,就像普通的函数调用同样。
// 得到给定位置的特征
rpc GetFeature(Point) returns (Feature) {} 复制代码
  • 服务器端流式RPC,客户端向服务器发送请求,并获取流以读取回一系列消息。客户端从返回的流中读取,直到没有更多消息为止。如咱们的示例所示,能够经过将stream关键字放在响应类型以前来指定服务器端流方法。
//得到给定Rectangle中可用的特征。结果是
//流式传输而不是当即返回
//由于矩形可能会覆盖较大的区域并包含大量特征。
rpc ListFeatures(Rectangle) returns (stream Feature) {} 复制代码
  • 客户端流式RPC,其中客户端使用gRPC提供的流写入一系列消息并将其发送到服务器。客户端写完消息后,它将等待服务器读取全部消息并返回其响应。经过将stream关键字放在请求类型以前,能够指定客户端流方法。
// 接收路线上被穿过的一系列点位, 当行程结束时
// 服务端会返回一个RouteSummary类型的消息.
rpc RecordRoute(stream Point) returns (RouteSummary) {} 复制代码
  • 双向流式RPC,双方都使用读写流发送一系列消息。这两个流是独立运行的,所以客户端和服务器能够按照本身喜欢的顺序进行读写:例如,服务器能够在写响应以前等待接收全部客户端消息,或者能够先读取消息再写入消息,或其余一些读写组合。每一个流中的消息顺序都会保留。您能够经过在请求和响应以前都放置stream关键字来指定这种类型的方法。
//接收路线行进中发送过来的一系列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文件里面包含:

  • 用于填充、序列化和检索咱们定义的请求和响应消息类型的全部protocol buffer代码。
  • 一个客户端存根用来让客户端调用RouteGuide服务中定义的方法。
  • 一个须要服务端实现的接口类型RouteGuideServer,接口类型中包含了RouteGuide服务中定义的全部方法。

建立gRPC服务端

首先让咱们看一下怎么建立RouteGuide服务器。有两种方法来让咱们的RouteGuide服务工做:

  • 实现咱们从服务定义生成的服务接口:作服务实际要作的事情。
  • 运行一个gRPC服务器监听客户端的请求而后把请求派发给正确的服务实现。

你能够在刚才安装的gPRC包的grpc-go/examples/route_guide/server/server.go找到咱们示例中RouteGuide`服务的实现代码。下面让咱们看看他是怎么工做的。

实现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 {
        ...
}
...
复制代码

普通PRC

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

如今,让咱们看一下服务方法中的一个流式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
}
复制代码

如你所见,此次咱们没有得到简单的请求和响应对象,而是得到了一个请求对象(客户端要在其中查找FeatureRectangle)和一个特殊的RouteGuide_ListFeaturesServer对象来写入响应。

在该方法中,咱们填充了须要返回的全部Feature对象,并使用Send()方法将它们写入RouteGuide_ListFeaturesServer。最后,就像在简单的RPC中同样,咱们返回nil错误来告诉gRPC咱们已经完成了响应的写入。若是此调用中发生任何错误,咱们将返回非nil错误; gRPC层会将其转换为适当的RPC状态,以在线上发送。

客户端流式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_RecordRouteServerRecv()方法不停地读取客户端的请求到一个请求对象中(在本例中为Point),直到没有更多消息为止:服务器须要要在每次调用后检查从Recv()返回的错误。若是为nil,则流仍然良好,而且能够继续读取;若是是io.EOF,则表示消息流已结束,服务器能够返回其RouteSummary。若是错误为其余值,咱们将返回错误“原样”,以便gRPC层将其转换为RPC状态。

双向流式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的实例。
  • 使用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

调用普通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)。若是调用没有返回错误,则咱们能够从第一个返回值中读取服务器的响应信息。

服务端流式RPC

这里咱们会调用服务端流式方法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_ListFeaturesClientRecv()方法不停地将服务器的响应读入到一个protocol buffer响应对象中(本例中的Feature对象),直到没有更多消息为止:客户端须要在每次调用后检查从Recv()返回的错误err。若是为nil,则流仍然良好,而且能够继续读取;若是是io.EOF,则消息流已结束;不然就是必定RPC错误,该错误会经过err传递给调用程序。

客户端流式RPC

客户端流方法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

最后,让咱们看一下双向流式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
复制代码
相关文章
相关标签/搜索