go gRPC基础

sasuke.cn

gRPC文档学习笔记

gRPC Basics - Go

学习目标:

  • 定义服务
  • 编译生成server和client代码
  • 使用Go gRPC API编写简单的客户端和服务端来使用service

为何使用gRPC

本篇文档使用的例子是一个简单的路由应用,客户端可以通过该服务获取路由信息,总结路由信息,交换路由信息。
一次编写proto文件就可以生成服务端和客户端代码,而且凡是支持gRPC的语言都可以互相通信。

Example

cd $GOPATH/src/google.golang.org/grpc/examples/route_guide

定义服务

使用service声明一个服务:

service RouteGuide {
  ...
}

然后再service的方法体中定义rpc methods,声明请求类型和返回体类型,gRPC允许定义四种类型的服务方法,在该例子中都出现了。

  • A simple RPC 客户端发送请求,等待服务端返回响应,看似一个普通函数的调用。
rpc GetFeature(Point) returns (Feature) {}
  • A server-side streaming RPC 服务端流式RPC,客户端发送请求,服务器返回一个响应流,读取消息序列,直到消息读取完毕。
rpc ListFeatures(Rectangle) returns (stream Feature) {}
  • A client-side streaming RPC 客户端流式RPC,客户端发送消息序列。客户端完成信息写入后,会等待服务端处理这个消息序列。
rpc RecordRoute(stream Point) returns (RouteSummary) {}
  • A bidirectional streaming RPC 双向流式RPC,双方通信都是发送消息序列。服务端可以等待客户端所有消息发送完毕在处理返回,也可以收到消息就处理返回。
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}

定义类型

message Point {
  int32 latitude = 1;
  int32 longitude = 2;
}

编译

protoc -I routeguide/ routeguide/route_guide.proto --go_out=plugins=grpc:routeguide

服务端

  • 实现服务接口。
  • 启动gRPC服务,监听客户端请求。

实现RouteGuide

服务端routeGuideServer结构体实现了生成的RouterGuideServer接口。

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 {
  // ...
}
Simple RPC
func (s *routeGuideServer) GetFeature(ctx context.Context, point *pb.Point) (*pb.Feature, error) {
  for _. feature := range s.saveFeatures {
    if proto.Equal(feature.Location, point) {
      return feature, nil
    }
  }

  //  No feature was found, return an unnamed feature
  return &pb.Feature{"", point}, nil
}

该方法接收context对象和客户端point请求作为参数,返回Feature对象和错误信息作为响应。在方法体中填充Feature信息,返回给客户端。

Server-side streaming RPC
func (s *routeGuideServer) ListFeatures(rect *pb.Rectangle, stream pb.RouteGuide_ListFeaturesServer) error {
  for _, feature := range s.saveFeatures {
    if inRange(feature.Location, rect) {
      if err := stream.Send(feature); err != nil {
        return err
      }
    }
  }

  return nil
}

ListFeature方法是服务端流式RPC方法,所以需要向客户端返回多个信息。
与简单RPC相比,服务端流式RPC接收的参数是请求对象和RouteGuide_ListFeaturesServer对象,后者用于写入responses。
在这个方法中,发送了多个Feature对象,最后如果没有错误发生,则返回一个nil error。

客户端流式RPC
func (s *routeGuideServer) RecordRoute(stream pb.RouteGuide_RecordRouteSrever) 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 += clacDistance(lastPoint, point)
    }
    lastPoint = point
  }
}

客户端流式RPC相对复杂一点,服务端获取一连串由客户端发送来的Point,处理后返回一个RouteSummary信息表示这些点描绘的路径。
这个方法不需要接收请求参数,而是直接获取一个RouteGuide_RecordRouteServer流对象,服务端可以使用这个流对象读写信息,使用
Recv()方法可以接收客户端发送的多个消息,使用SendAndClose()方法可以返回一个单一的响应,并且关闭与客户端的连接。
服务端使用Recv()方法循环读取客户端发送的请求对象,直到客户端不再发送数据,服务端在读取每一个请求对象时都需要判断该方法返回的
error信息,如果没有发生错误,则表示stream对象仍然是好的,如果错误信息是io.EOF,则表示客户端停止发送数据,服务端可以处理
收集的这些数据,返回一个RouteSummary对象。对于其它形式的error,只需要直接返回错误信息即可,这些错误信息会被转换为对应的RPC
状态。

Bidirectional streaming RPC
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
      }
    }
  }
}

stream同上一个方法一样,可以被用来读写信息,不同的是客户端在发送数据流的同时,服务端也可以返回数据流,服务端使用
Send()方法来发送流数据,客户端和服务端都可以按顺序获取对方发送的数据。流对双方发送的信息分开处理。

启动服务

当所有的RPC接口都被实现之后,开始启动一个gRPC服务,用于监听客户端连接,处理客户端发送来的RPC调用请求。

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)
  • 启动tpc监听,指定监听端口。
  • 创建gRPC服务端实例。
  • 注册服务实现。
  • 调用Serve()方法,启动RPC服务监听,杀死进程或调用Stop()方法来停止服务。

创建客户端

Creating a stub

调用RPC服务之前需要先使用grpc.Dial()方法和服务端建立RPC连接。

conn, err := grpc.Dial(*serverAddr)
if err != nil {
  // ...
}
defer conn.Close()

可以使用DialOptions设置认证信息。
建立连接之后,需要一个客户端去调用RPC,通过调用protoc编译后生成的NewRouteGuideClient方法来获取一个client。

client := pb.NewRouteGuideClient(conn)
Calling service methods

现在来看看如何调用service提供的RPC方法,gRPC-Go提供的RPC方法都是阻塞同步方式的,客户端必须等待服务端返回响应才做下一步处理。

  1. 简单RPC调用

简单到如同调用一个本地方法。

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

调用参数包含一个context.Context对象,服务端可以根据这一参数做更多事情。

  1. 服务端流式RPC
rect := &pb.Rectangle{
  // ...
} // initialize a pb.Rectangle
stream, err := client.ListFeatures(context.Backgound(), 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,返回的不是一个消息对象,而是一个流,通过这个流可以读取到服务端发送的一系列消息对象。

  1. 客户端流式RPC
// Create a random number of random 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())
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)

与服务端流式发送想对应的,客户端流式调用RPC最后在后的服务端响应时,需要调用CloseAndRecv()方法。

  1. 双向流式RPC
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.Longtitude)
  }
}()
for _, note := range notes {
  if err := stream.Send(note); err != nil {
    log.Fatalf("Failed to send to note: %v" ,err)
  }
}
stream.CloseSend()
<-waitc

Try it out

$ go run server/server.go
$ go run client/client.go

你可能感兴趣的:(go gRPC基础)