01

为什么使用 gRPC?

借助 gRPC,我们可以在 .proto 文件中一次定义我们的服务,并以 gRPC 支持的任何语言生成客户端和服务器代码,无论是在大型数据中心内的服务器,还是在个人的电脑的环境中,这些客户端和服务器代码都可以运行 –  gRPC 可以为您处理不同语言和环境之间的通信。我们还获得了使用 protocol buffers 的所有优点,包括有效的序列化,简单的 IDL 和容易的接口更新。

我们的示例是一个简单的路由映射应用程序,它使客户端可以获取有关其路由功能的信息,创建其路由的摘要以及与服务器和其他客户端交换路由信息(例如流量更新)。

02

准备工作

  • 安装 Go 最新正式发行版本。

  • 安装 protocol buffers 编译器 protoc。请参考「Protobuf - 更小、更快、更简单的交互式数据语言」- Part 05。

  • 安装编译器 protoc 的 Go 插件。请参考「gRPC 初探与简单使用」- Part 04。

  • git clone 示例代码,并进入该目录。

    $ git clone https://github.com/grpc/grpc-go$ cd grpc-go/examples/route_guide

03

定义服务并生成客户端和服务器代码

我们的第一步是使用 protocol buffers 定义 gRPC 服务以及方法请求和响应类型。

有关完整的

.proto 文件,请参阅 Part 2 git clone 的代码 routeguide/route_guide.proto。

要定义服务,请在 .proto 文件中指定一个命名服务:

service RouteGuide {   ...}

然后,在服务定义中定义 rpc 方法,并指定它们的请求和响应类型。gRPC 允许您定义四种服务方法,所有这些方法都在 RouteGuide 服务中使用:

  1. 一个简单的 RPC,客户端使用存根将请求发送到服务器,然后等待响应返回,就像正常的函数调用一样。

    // Obtains the feature at a given position.rpc GetFeature(Point) returns (Feature) {}
  2. 服务器端流式 RPC,客户端在其中向服务器发送请求,并获取流以读取回一系列消息。客户端从返回的流中读取,直到没有更多消息为止。如我们的示例所示,您可以通过在响应类型之前放置 stream 关键字来指定服务器端流方法。

    // Obtains the Features available within the given Rectangle.  Results are// streamed rather than returned at once (e.g. in a response message with a// repeated field), as the rectangle may cover a large area and contain a// huge number of features.rpc ListFeatures(Rectangle) returns (stream Feature) {}
  3. 客户端流式 RPC,客户端在其中编写消息序列,然后再次使用提供的流将其发送到服务器。客户端写完消息后,它将等待服务器读取所有消息并返回其响应。通过将 stream 关键字放在请求类型之前,可以指定客户端流方法。

    // Accepts a stream of Points on a route being traversed, returning a// RouteSummary when traversal is completed.rpc RecordRoute(stream Point) returns (RouteSummary) {}
  4. 双向流式 RPC,双方都使用读写流发送一系列消息。这两个流是独立运行的,因此客户端和服务器可以按照自己喜欢的顺序进行读写:例如,服务器可以在写响应之前等待接收所有客户端消息,或者可以先读取一条消息再写入一条消息,或读写的其他组合。每个流中的消息顺序都会保留。您可以通过在请求和响应之前都放置 stream 关键字来指定这种类型的方法。

    // Accepts a stream of RouteNotes sent while a route is being traversed,// while receiving other RouteNotes (e.g. from other users).rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}

我们的 .proto 文件还包含用于服务方法中所有请求和响应类型的 protocol buffers message 类型定义-例如,这是 Point message 类型:

// Points are represented as latitude-longitude pairs in the E7 representation// (degrees multiplied by 10**7 and rounded to the nearest integer).// Latitudes should be in the range +/- 90 degrees and longitude should be in// the range +/- 180 degrees (inclusive).message Point {  int32 latitude = 1;  int32 longitude = 2;}

接下来,我们需要根据 .proto 服务定义生成 gRPC 客户端和服务器接口。我们使用带有特殊 gRPC Go 插件的 protocol buffers 编译器 protoc 来执行此操作。

在 examples/route_guide 目录中,运行以下命令:

$ protoc --go_out=. --go_opt=paths=source_relative \    --go-grpc_out=. --go-grpc_opt=paths=source_relative \    routeguide/route_guide.proto

运行此命令将在 routeguide 目录中生成以下文件:

  • route_guide.pb.go,其中包含用于填充,序列化和检索请求和响应消息类型的所有 protocol buffers 代码。

  • route_guide_grpc.pb.go,其中包含以下内容:

    • 客户端使用 RouteGuide 服务中定义的方法调用的接口类型(或存根)。

    • 服务器要实现的接口类型,也具有 RouteGuide 服务中定义的方法。

04

创建服务器

首先,让我们看一下如何创建 RouteGuide 服务器。

使我们的 RouteGuide 服务完成其工作包括两个部分:

  • 实施根据我们的服务定义生成的服务接口:完成我们服务的实际“工作”。

  • 运行 gRPC 服务器以监听来自客户端的请求,并将其分派到正确的服务实现。

您可以在 server/server.go 中找到我们的示例 RouteGuide 服务器。让我们仔细看看它是如何工作的。

实现 RouteGuide

如您所见,我们的服务器具有一个 routeGuideServer 结构体类型,该结构体类型实现了生成的 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 {        ...}...

简单的 RPC

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{Location: point}, nil}

该方法传递了 RPC 和客户端的 Point protocol buffer 请求的上下文对象。它返回 Feature protocol buffer 对象以及响应信息和错误。在该方法中,我们使用适当的信息填充功能,然后将其返回并返回 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}

如您所见,这次我们没有获得简单的请求和响应对象,而是获得了一个请求对象(客户端要在其中找到 Feature 的 Rectangle)

和一个特殊的 RouteGuide_ListFeaturesServer 对象来编写响应。

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

客户端流式 RPC

现在,让我们看一些更复杂的事情:客户端流方法 RecordRoute,从客户端获取 Point 流,并返回一个包含行程信息的 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)的请求,直到没有更多消息为止:服务器需要检查从 Read() 返回的错误。每个 call。如果为 nil,则流仍然良好,并且可以继续读取;否则为 0。如果是 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)                ... // look for notes to be sent to client    for _, note := range s.routeNotes[key] {      if err := stream.Send(note); err != nil {        return err      }    }  }}

这次,我们获得一个 RouteGuide_RouteChatServer 流,就像在客户端流示例中一样,该流可用于读取和写入消息。但是,这次,当客户端仍在向其消息流中写入消息时,我们通过方法的流返回值。

此处的读写语法与我们的客户端流式传输方法非常相似,不同之处在于服务器使用流的 Send() 方法而不是 SendAndClose() ,因为服务器正在写多个响应。尽管双方总是会按照对方的写入顺序来获取对方的消息,但是客户端和服务器都可以以任意顺序进行读取和写入-流完全独立地运行。

启动服务器

一旦实现了所有方法,我们还需要启动 gRPC 服务器,以便客户端可以实际使用我们的服务。以下代码段显示了如何为 RouteGuide 服务执行此操作:

flag.Parse()lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", *port))if err != nil {  log.Fatalf("failed to listen: %v", err)}var opts []grpc.ServerOption...grpcServer := grpc.NewServer(opts...)pb.RegisterRouteGuideServer(grpcServer, newServer())grpcServer.Serve(lis)

构建和启动服务:

  1. 使用以下命令指定我们要用于监听客户端请求的端口:

    lis,err:= net.Listen(...)。

  2. 使用 grpc.NewServer(...) 创建 gRPC 服务器的实例。

  3. 在 gRPC 服务器上注册我们的服务实现。

  4. 使用我们的端口详细信息在服务器上调用 Serve() 进行阻塞等待,直到进程被杀死或调用 Stop() 为止。

05

创建客户端

在本部分中,我们将研究为 RouteGuide 服务创建 Go 客户端。

您可以在 grpc-go/examples/route_guide/client/client.go 中看到我们完整的示例客户端代码。

创建客户端存根

要调用服务方法,我们首先需要创建一个 gRPC 通道来与服务器通信。我们通过将服务器地址和端口号传递给 grpc.Dial() 来创建它,如下所示:

var opts []grpc.DialOption...conn, err := grpc.Dial(*serverAddr, opts...)if err != nil {  ...}defer conn.Close()

当服务需要它们时,可以使用 DialOptions 在 grpc.Dial 中设置身份验证凭据(例如TLS,GCE凭据或JWT凭据)。RouteGuide 服务不需要任何凭据。

设置 gRPC 通道后,我们需要一个客户端存根来执行 RPC。我们使用从示例 .proto 文件生成的 pb 包提供的 NewRouteGuideClient 方法获取它。

client := pb.NewRouteGuideClient(conn)

调用服务方法

现在,让我们看看我们如何调用我们的服务方法。请注意,在 gRPC-Go 中,RPC 在阻塞/同步模式下运行,这意味着 RPC 调用等待服务器响应,并且将返回响应或错误。

简单的 RPC

调用简单的 RPC GetFeature 几乎与调用本地方法一样简单。

feature, err := client.GetFeature(context.Background(), &pb.Point{409146138, -746188906})if err != nil {  ...}

如您所见,我们在先前获得的存根上调用该方法。在我们的方法参数中,我们创建并填充一个请求 protocol buffer 对象(在本例中为 Point)。我们还会传递一个 context.Context 对象,该对象可让我们在必要时更改 RPC 的行为,例如 time-out/cancel 运行中的 RPC。如果调用没有返回错误,那么我们可以从服务器的第一个返回值中读取响应信息。

log.Println(feature)

服务器端流式 RPC

我们在这里调用服务器端流方法 ListFeatures,该方法返回地理要素流。如果您已经阅读了创建服务器的内容,那么其中的一些内容可能看起来非常熟悉-流式 RPC 在两侧都以类似的方式实现。

rect := &pb.Rectangle{ ... }  // initialize a pb.Rectanglestream, 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 传递。

客户端流式 RPC

客户端流方法 RecordRoute 与服务器端方法相似,不同之处在于,我们仅向该方法传递上下文,并获取回

RouteGuide_RecordRouteClientClient 流,我们可以使用该流来写入和读取消息。

// Create a random number of random pointsr := rand.New(rand.NewSource(time.Now().UnixNano()))pointCount := int(r.Int31n(100)) + 2 // Traverse at least two pointsvar points []*pb.Pointfor i := 0; i < pointCount; i++ {  points = append(points, randomPoint(r))}log.Printf("Traversing %d points.", len(points))stream, err := client.RecordRoute(context.Background())if err != nil {  log.Fatalf("%v.RecordRoute(_) = _, %v", client, err)}for _, point := range points {  if err := stream.Send(point); err != nil {    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() 返回的错误中获取 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()

除了在完成调用后使用流的 CloseSend() 方法外,此处的读写语法与我们的客户端流方法非常相似。尽管双方总是会按照对方的写入顺序来获取对方的消息,但是客户端和服务器都可以以任意顺序进行读取和写入-流完全独立地运行。

06

运行程序

从 examples/route_guide 目录执行以下命令:

  1. 运行服务器:

    $ go run server/server.go
  2. 从另一个终端,运行客户端:

    $ go run client/client.go

输出内容:

Getting feature for point (409146138, -746188906)name:"Berkshire Valley Management Area Trail, Jefferson, NJ, USA" location:<409146138 longitude:-746188906>409146138>Getting feature for point (0, 0)location:<>Looking for features within lo:<400000000 longitude:-750000000> hi:<420000000 longitude:-730000000>420000000>400000000>name:"Patriots Path, Mendham, NJ 07945, USA" location:<407838351 longitude:-746143763>407838351>...name:"3 Hasta Way, Newton, NJ 07860, USA" location:<410248224 longitude:-747127767>410248224>Traversing 56 points.Route summary: point_count:56 distance:497013163Got message First message at point(0, 1)Got message Second message at point(0, 2)Got message Third message at point(0, 3)Got message First message at point(0, 1)Got message Fourth message at point(0, 1)Got message Second message at point(0, 2)Got message Fifth message at point(0, 2)Got message Third message at point(0, 3)Got message Sixth message at point(0, 3)

注意:

我们已从本页显示的客户端和服务器跟踪输出中省略了时间戳。

07

总结

本文开篇先介绍了为什么要使用 gRPC,接着简述了使用 gRPC 需要做的准备工作,然后通过 gRPC 官方 Go 示例代码介绍了如何在 .proto 文件中定义服务,如何使用 protoc 编译器生成客户端和服务器代码,如何根据 protoc 编译器生成的客户端和服务器代码创建服务器和客户端的 4 种形式。


参考资料:

https://grpc.io/docs/languages/go/basics/

go语言入门经典_Go 语言中的 gRPC 基础入门相关推荐

  1. 入门C语言模板,C语言入门经典-C语言编程

    C语言入门经典-C语言编程Tag内容描述: 1.第01章C语言编程,C语言是一种功能强大.简洁的计算机语言,通过它可以编写程序,指挥计算机完成指定的任务.我们可以利用C语言创建程序(即一组指令),并让 ...

  2. c语言编程培训ppt,C语言入门经典-C语言编程.ppt

    <C语言入门经典-C语言编程.ppt>由会员分享,可在线阅读,更多相关<C语言入门经典-C语言编程.ppt(27页珍藏版)>请在人人文库网上搜索. 1.第01章 C语言编程,C ...

  3. c语言入门经典doc,C语言入门经典C语言编程.ppt

    C语言入门经典C语言编程.ppt 第01章 C语言编程,C语言是一种功能强大.简洁的计算机语言,通过它可以编写程序,指挥计算机完成指定的任务.我们可以利用C语言创建程序即一组指令,并让计算机依指令行事 ...

  4. 编程入门c语言ppt,C语言入门经典-C语言编程.ppt

    <C语言入门经典-C语言编程.ppt>由会员分享,可在线阅读,更多相关<C语言入门经典-C语言编程.ppt(27页珍藏版)>请在装配图网上搜索. 1.第01章 C语言编程,C语 ...

  5. go语言视频教程_ go语言入门视频教程_go语言实战视频教程

    许多人可能知道go语言的优点在哪里,但他们不知道go语言适合在哪里使用.通过学习go语言视频教程,可以清楚的知道go语言主要用于服务器端开发,其定位是用来开发"大型软件".学习go ...

  6. 【Python数据科学快速入门系列 | 06】Matplotlib数据可视化基础入门(一)

    这是机器未来的第52篇文章 原文首发地址:https://robotsfutures.blog.csdn.net/article/details/126899226 <Python数据科学快速入 ...

  7. java基础入门课后习题答案_《Java基础入门》课后习题及答案

    <Java基础入门>课后习题及答案Java基础入门,课后习题,答案 博学谷--让IT教学更简单,让IT学习更有效 第6章JavaAPI 一.填空题 1.在Java中定义了两个类来封装对字符 ...

  8. azure 入门_Azure Cosmos DB中的子文档入门

    azure 入门 As we've worked with Azure Cosmos DB, we've seen that we can store data within fields and t ...

  9. 零基础入门学python 第二版-《零基础入门学习Python》第二版和第一版的区别在哪里呢?...

    第一版 时光荏苒,一晃间,距离<零基础入门学习 Python>出版(2016年11月)已经过去两年多了,在这段时间里, Python 逐步走入了大家的视野,这门语言因其简洁的语法风格,在云 ...

最新文章

  1. LeetCode上最难的链表算法题,没有之一
  2. umi搭建react+antd项目(二)路由
  3. Android4.0-4.4 加入支持状态栏显示耳机图标方法(支持带不带MIC的两种耳机自己主动识别)...
  4. 石川es6课程---9、面向对象-基础
  5. 【Python数据分析】四级成绩分布 -matplotlib,xlrd 应用
  6. 构建高可用ZooKeeper集群
  7. 摘自《Java工程师成神之路》2018修订版,自我勉励
  8. [剑指offer]面试题48:不能被继承的类
  9. 2.9.JavaScript--内置对象
  10. linux内核端口绑定,linux 多网卡bonding 绑定 端口聚合
  11. mysql记录当前表数据的数据条数据类型_MySQL学习记录:数据类型与操作数据表...
  12. python和c 的区别-Python与C语言有什么区别?
  13. 如何理解halcon 算子get_grayval 、set_grayval 逐行读取和逐行写入
  14. (解决办法)Windows Server 2003安装sp1时说产品密钥无效
  15. java图片循环_java – 如何实现无限图像循环?
  16. 2007年各大论坛最新经典WindowsXP操作系统
  17. forrtl: error (200): program aborting due to control-C event
  18. 2015年仿团800首页视频教程
  19. 集团税务共享化、数字化系统如何选型(一)
  20. CPU虚拟化技术及QEMU/KVM虚拟机安装实践

热门文章

  1. Windows7 apache启动失败的解决方法
  2. 循环链表的插入和删除
  3. inotify+rsync实时同步服务部署
  4. 豪华版飞机大战系列(六)--附源代码
  5. 要有被打断仍能够继续学习的能力
  6. 基于CSS3飘带状3D菜单 菜单带小图标
  7. android中颜色参考
  8. 使用命令启动IIS管理器
  9. 服务机器人传感器应用
  10. 程序员生存定律--管理向左,技术向右