grpc是google开发的一款rpc框架,非常好用而且流行。grpc使用proto buffer来进行接口定义,也使用proto buffer作为底层的编码格式(gRPC can use protocol buffers as both its Interface Definition Language (IDL) and as its underlying message interchange format)。

hello world

首先要安装proto buffer的编译器,这里省略。为了生成go的文件,还需要安装两个go的插件,如下:

$ go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2

并且设置PATH环境变量:

$ export PATH="$PATH:$(go env GOPATH)/bin"

在项目下面创建一个proto文件夹,如图:

go 的grpc 可以指定ip 调用吗 go语言grpc_客户端


proto文件夹下面放的是相关结构体和rpc的定义,pkg文件夹下面放的是之后protoc自动生成的框架代码。

hello.proto的文件内容:

syntax = "proto3";
option go_package = "./hellopb";

// The greeting service definition.
service Greeter {
    // Sends a greeting
    rpc SayHello (HelloRequest) returns (HelloReply) {}
  }

  // The request message containing the user's name.
  message HelloRequest {
    string name = 1;
  }

  // The response message containing the greetings
  message HelloReply {
    string message = 1;
  }

option go_package 表示的是go的import path,待会生成文件之后我们会看到有什么用。
使用以下命令生成go文件:

protoc --go_out=./pkg/ --go-grpc_out=./pkg/  proto/hello.proto

go_out和go-grpc_out参数都表示生成的文件存放的位置。

生成文件的结果:

go 的grpc 可以指定ip 调用吗 go语言grpc_rpc_02

pkg下面多了一个文件夹hellopb,里面是生成好的文件,这个hellopb就来源于我们在proto文件里面定义的option go_package = “./hellopb”;

然后分别简单地实现server和client。
server端实现了具体的rpc的处理逻辑。

package main

import (
    "context"
    "flag"
    "fmt"
    pb "learngrpc/proto/pkg/hellopb"
    "log"
    "net"
    "google.golang.org/grpc"
)

var (
    port = flag.Int("port", 8080, "The server port")
)

type server struct {
    pb.UnimplementedGreeterServer
}

func (*server) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloReply, error) {
    return &pb.HelloReply{Message: "hello " + req.GetName()}, nil
}

func main() {
    flag.Parse()
    lis, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%d", *port))
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }

    s := grpc.NewServer()
    pb.RegisterGreeterServer(s, &server{})
    log.Printf("server listening at %v", lis.Addr())
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}

client:

package main
import (
    "context"
    "flag"
    "log"
    "time"

    pb "learngrpc/proto/pkg/hellopb"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
)

const (
    defaultName = "world"
)

var (
    addr = flag.String("addr", "localhost:8080", "the address to connect to")
    name = flag.String("name", defaultName, "Name to greet")
)

func main() {
    flag.Parse()
    // Set up a connection to the server.
    conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }
    defer conn.Close()
    c := pb.NewGreeterClient(conn)

    // Contact the server and print out its response.
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()
    r, err := c.SayHello(ctx, &pb.HelloRequest{Name: *name})
    if err != nil {
        log.Fatalf("could not greet: %v", err)
    }
    log.Printf("Greeting: %s", r.GetMessage())
}

启动server:

go 的grpc 可以指定ip 调用吗 go语言grpc_golang_03

启动client:

go 的grpc 可以指定ip 调用吗 go语言grpc_客户端_04

到此我们就成功完成了grpc的hello world的demo。

流式rpc (streaming rpc)

上面的hello world是一种基本的rpc用法,对于客户端来说,就像调用普通函数一样进行了rpc调用。grpc还提供了streaming RPC,可以让服务端或者客户端持续发送消息。

客户端流式 (client-side streaming RPC)

IDL:

// Accepts a stream of Points on a route being traversed, returning 
// RouteSummary when traversal is completed.

rpc RecordRoute(stream Point) returns (RouteSummary) {}

要使用客户端流式rpc,只需要在定义时在参数前面加一个 stream。这个例子中,客户端会发送多个Point的信息,服务端汇总后再把结果返回给客户端。
客户端的实现:

// 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 {
    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)

在调用client.RecordRoute()方法时,并不是直接进行rpc调用,而是返回一个RecordRouteClient。后面可以使用这个结构体调用Send()和CloseAndRecv()等方法。
服务端的实现:

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
  }
}

客户端在实现时,函数的参数也不是普通的参数,而是一个RecordRouteServer,它可以调用Recv()和SendAndClose()方法。

服务端流式 (Server-side streaming RPC)

IDl:

rpc ListFeatures(Rectangle) returns (stream Feature) {}

客户端的实现:

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)
}

服务端的实现:

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
}

双向流式 (Bidirectional streaming RPC)

IDl:

rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}

客户端:

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

服务端:

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
      }
    }
  }
}