This tutorial provides the basics of using gRPC with Go

In this tutorial you will learn how to:

  • in.protoFile to define a service.
  • Generate client and server code using the Protocol Buffer compiler.
  • Use gRPC’s Go API to write a client and server for your service.

Before proceeding, make sure you are familiar with gRPC concepts and are familiar with Protocol Buffer. Note that the examples in the tutorial use the Proto3 version of the Protocol Buffer: you can learn more about this in the Protobuf Language Guide and Protobuf Generation Go Code Guide.

Why use gRPC

Our example is a simple roadmap application where clients can obtain route characteristics, create their route summaries, and exchange route information such as traffic status updates with the server or other clients.

With gRPC, we can define our services in.proto files and implement clients and servers in any language gRPC supports, which in turn can run in a variety of environments from the server to your own tablet -gRPC also solves the complexities of communicating between all the different languages and environments for you. We also get all the benefits of using protocol Buffer, including efficient serialization (more efficient than JSON in both speed and volume), simple IDL (Interface Definition Language), and easy interface updates.

The installation

GRPC installation package

The gRPC Golang version of the package needs to be installed first, and the examples directory of the official package contains the code for the example roadmap application in the tutorial.

$ go get google.golang.org/grpc
Copy the code

Grpc-go /examples/route_guide:

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

Install tools and plug-ins

  • Install the Protocol Buffer compiler

Compiler installed the simplest way is to go to https://github.com/protocolbuffers/protobuf/releases to download precompiled good protoc binaries, each platform can be found in the warehouse that corresponds to the compiler binary files. Here we Mac Os, for example, from https://github.com/protocolbuffers/protobuf/releases/download/v3.6.0/protoc-3.6.0-osx-x86_64.zip to download and unzip the files.

Update the PATH system variable, or make sure protoc is in the directory that PATH contains.

  • Install the Protoc compiler plug-in
$ go get -u github.com/golang/protobuf/protoc-gen-go
Copy the code

The compiler plug-in Protoc-Gen-Go will be installed in $GOBIN, which defaults to $GOPATH/bin. The compiler protoc must find it in $PATH:

$ export PATH=$PATH:$GOPATH/bin
Copy the code

Define the service

The first step is to define the request and response types for the gRPC service and methods using the protocol Buffer. You can see the complete. Proto file in the examples/route_guide/ routeGuide /route_guide.proto download.

To define a service, you need to specify a named service in the.proto file

service RouteGuide {... }Copy the code

The RPC methods are then defined in the service definition, specifying their request and response types. GRPC allows you to define four types of service methods, all of which are applied to our RouteGuide service.

  • A simple RPC in which the client uses a stub to send the request to the server and then waits for the response to return, just like a normal function call.
// Get the characteristics of the given location
rpc GetFeature(Point) returns (Feature) {}
Copy the code
  • Server-side streaming RPC, where the client sends a request to the server and gets the stream to read back a series of messages. The client reads from the returned stream until there are no more messages. As our example shows, you can specify the server-side stream method by placing the STREAM keyword before the response type.
// Get the features available in the given Rectangle. As a result,
// Stream instead of returning immediately
// Because rectangles can cover a large area and contain a large number of features.
rpc ListFeatures(Rectangle) returns (stream Feature) {}
Copy the code
  • Client-side streaming RPC, in which the client writes a series of messages and sends them to the server using the stream provided by gRPC. After the client has written the message, it waits for the server to read all the messages and return its response. You can specify the client stream method by placing the STREAM keyword before the request type.
// A series of points to be crossed on the receiving route when the journey is over
// The server returns a RouteSummary message.
rpc RecordRoute(stream Point) returns (RouteSummary) {}
Copy the code
  • Bidirectional streaming RPC, in which both parties send a series of messages using a read/write stream. The two streams run independently, so the client and server can read and write in whichever order they like: for example, the server can wait to receive all the client messages before writing the response, or it can read the message before writing it, or some other combination of read and write. The order of messages in each flow is preserved. You can specify this type of method by placing the STREAM keyword before both the request and the response.
// Receive a series of RouteNotes sent along the route, as well as other RouteNotes(e.g. from other users)
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
Copy the code

Our.proto file also requires protocol buffer message type definitions for all request and response types. For example, the following Point message type:

// Points are represented as longitudinal-latitude pairs in the E7 representation.
// (multiply the degree by 10 ** 7 and round to the nearest integer).
// Latitude should be within +/ -90 degrees and longitude should be within
// Range +/- 180 ° (inclusive)
message Point {
  int32 latitude = 1;
  int32 longitude = 2;
}
Copy the code

Generate client and server code

The next step is to generate the gRPC client and server interfaces from our.Proto service definition. We use the Protoc compiler and the compiler plug-in installed above to do this:

Run it in the example route_guide directory:

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

After running this command, the route_guide.pb.go file is generated in the routeGuide directory of the route_guide directory.

The pb.go file contains:

  • All protocol buffer code used to populate, serialize, and retrieve the request and response message types we defined.
  • A client stub is used for the client to callRouteGuideMethods defined in the service.
  • An interface type that needs to be implemented by the serverRouteGuideServerIs included in the interface typeRouteGuideAll methods defined in the service.

Create the gRPC server

First let’s look at how to create the RouteGuide server. There are two ways to make our RouteGuide service work:

  • Implement the service interface we generated from the service definition: do what the service actually does.
  • Run a gRPC server that listens for client requests and sends them to the correct service implementation.

You can find the implementation code for the RouteGuide service in our example in the gPRC package grpc-go/examples/route_guide/server/server.go. Let’s see how he works.

Implement RouteGuide

As you can see, there is a routeGuideServer struct type in the implementation code, which implements the routeGuideServer interface defined in the Pb. go file generated by the Protoc compiler.

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{... }...Copy the code

Ordinary PRC

RouteGuideServer implements all of our service methods. First, let’s look at the simplest type, GetFeature, which simply gets a Point from the client and returns the corresponding Feature information from its Feature database.

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
}
Copy the code

This method passes the RPC context object and the client’s Point Protocol Buffer request message. It returns a protocol buffer of Feature type and an error in the response message. In this method, we populate the Feature with the appropriate information and then return it with a nil error to inform gRPC that we have finished processing the RPC and can return the ‘Feature to the client.

Server-side streaming RPC

Now, let’s look at a streaming RPC in the service method. ListFeatures is server-side streaming RPC, so we need to send multiple features back to the client.

func (s *routeGuideServer) ListFeatures(rect *pb.Rectangle, stream pb.RouteGuide_ListFeaturesServer) error {
	for _, feature := range s.savedFeatures {
		if inRange(feature.Location, rect) {
			iferr := stream.Send(feature); err ! =nil {
				return err
			}
		}
	}
	return nil
}
Copy the code

As you can see, instead of simple request and response objects, this time we get a request object (where the client looks for a Rectangle for the Feature) and a special RouteGuide_ListFeaturesServer object to write the response.

In this method, we populate all the Feature objects that need to be returned and write them to RouteGuide_ListFeaturesServer using the Send() method. Finally, just as in simple RPC, we return a nil error to tell the gRPC that we have finished writing the response. If any errors occur in this call, we will return a non-nil error; The gRPC layer converts it to the appropriate RPC state to send online.

Client streaming RPC

Now, let’s look at something a little more complicated: the client stream method RecordRoute retrieves the point stream from the client and returns a RouteSummary containing the route information. As you can see, this time the method has no request argument at all. Instead, it gets a RouteGuide_RecordRouteServer stream that the server can use to read and write messages – it can receive the client message using the Recv() method and return its single response using the SendAndClose() method.

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()),
			})
		}
		iferr ! =nil {
			return err
		}
		pointCount++
		for _, feature := range s.savedFeatures {
			if proto.Equal(feature.Location, point) {
				featureCount++
			}
		}
		iflastPoint ! =nil {
			distance += calcDistance(lastPoint, point)
		}
		lastPoint = point
	}
}
Copy the code

In the method body, we use the Recv() method of the RouteGuide_RecordRouteServer to continuously read the client’s request into a request object (Point in this case) until there are no more messages: the server needs to check for errors returned from Recv() after each call. If nil, the stream is still fine and can continue to read; If it is io.eof, then the message flow has ended and the server can return its RouteSummary. If the error is any other value, we will return the error “as is” so that the gRPC layer can convert it to an RPC state.

Bidirectional flow RPC

Finally let’s look at the bidirectional streaming RPC method RouteChat()

func (s *routeGuideServer) RouteChat(stream pb.RouteGuide_RouteChatServer) error {
	for {
		in, err := stream.Recv()
		if err == io.EOF {
			return nil
		}
		iferr ! =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 {
			iferr := stream.Send(note); err ! =nil {
				return err
			}
		}
	}
}
Copy the code

This time, we get a RouteGuide_RouteChatServer stream, which can be used to read and write messages, just as in the client stream example. However, this time, while the client is still writing to its message flow, we will write the message to be returned to the flow.

The read and write syntax here is very similar to our client-side streaming method, except that the server uses the Send() method of the stream instead of SendAndClose() because the server writes multiple responses. Although both sides will always get each other’s messages in the order in which they were written, both the client and the server can read and write in any order – streams run completely independently (meaning that the server can accept a request and write to the stream, or receive a request and write a response). The same client can write a request and then read the response, or send a request and read a response.

Starting the server

Once all the methods are implemented, we also need to start the gRPC server so that clients can actually use our services. The following code snippet shows how to start the RouteGuide service.

flag.Parse()
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
iferr ! =nil {
        log.Fatalf("failed to listen: %v", err)
}
grpcServer := grpc.NewServer()
pb.RegisterRouteGuideServer(grpcServer, &routeGuideServer{})
... // determine whether to use TLS
grpcServer.Serve(lis)
Copy the code

To build and start the server we need:

  • Specifies the port on which to listen for client requestsLis, err: = net. Listen (" TCP ", FMT. Sprintf (" : % d ", * port)).
  • usegrpc.NewServer()Create an instance of gRPC Server.
  • Register our service implementation with the gRPC Server.
  • Called on the server using our port detailsServe()Block and wait until the process is killed or calledStop()So far.

Creating a client

In this section we will create the Go client for the RouteGuide service. You can see the full client code at grpc-go/examples/route_guide/client/client.go.

Create the client stub

To invoke the method of the service, we first need to create a gRPC channel to communicate with the server. We create a channel by passing the server address and port number to grpc.dial (), as follows:

conn, err := grpc.Dial(*serverAddr)
iferr ! =nil{... }defer conn.Close()
Copy the code

If the service you are requesting requires authentication, you can use DialOptions in grPC. Dial to set authentication credentials (e.g., TLS, GCE, JWT credentials) — our RouteGuide service does not require these.

With the gRPC channel set up, we need a client stub to perform RPC. We use the NewRouteGuideClient method provided in the PB package generated from.proto to get the client stub.

client := pb.NewRouteGuideClient(conn)
Copy the code

The generated Pb. go file defines the client interface type RouteGuideClient and implements the methods in the interface with the structure type of the client stub, so the client can directly call the methods listed in the following interface types using the client stub obtained above.

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) }Copy the code

Each implementation method then requests the corresponding gRPC server method to obtain the server response, such as:

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...)
	iferr ! =nil {
		return nil, err
	}
	return out, nil
}
Copy the code

The full implementation of the RouteGuideClient interface can be found in the generated pb.go file.

The method that invokes the service

Now let’s look at how to invoke the methods of the service. Note that in GRPC-GO, PRC runs in blocking/synchronous mode, meaning that the RPC call waits for the server to respond, and the server will return a response or an error.

Ordinary RPC

Calling the normal RPC method GetFeature is like calling the local method directly.

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

As you can see, we call this method on the stub we obtained earlier. In our method parameters, we create and populate a protocol buffer object (in this case, the Point object). We also pass a context. context object that allows us to change the behavior of the RPC if necessary, such as timeout/cancel an RPC in flight. If the call does not return an error, we can read the server response information from the first return value.

Server-side streaming RPC

Here we call the server-side streaming method ListFeatures, which returns a stream containing geographic feature information. If you read the client creation section above, something here will look familiar – streaming RPC is implemented in similar ways on both sides.

rect := &pb.Rectangle{ ... }  // initialize a pb.Rectangle
stream, err := client.ListFeatures(context.Background(), rect)
iferr ! =nil{... }for {
    feature, err := stream.Recv()
    if err == io.EOF {
        break
    }
    iferr ! =nil {
        log.Fatalf("%v.ListFeatures(_) = _, %v", client, err)
    }
    log.Println(feature)
}
Copy the code

As with a simple RPC call, the context of a method and a request are passed. But we’re getting back an instance of RouteGuide_ListFeaturesClient instead of a response object. The client can read the response from the server using the RouteGuide_ListFeaturesClient flow.

We use the Recv() method of the RouteGuide_ListFeaturesClient to keep reading the server’s response into a protocol buffer (the Feature object in this example) until there are no more messages: The client needs to check the error err returned from Recv() after each call. If nil, the stream is still fine and can continue to read; If it is IO.EOF, the message flow has ended; Otherwise, it is a certain RPC error, which is passed to the caller through err.

Client streaming RPC

The client-side stream method RecordRoute is similar to the server-side method, except that we just pass a context to the method and get a RouteGuide_RecordRouteClient stream that can be used to write and read messages.

// Create Points randomly
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())// Call the client-side streaming RPC method defined in the service
iferr ! =nil {
	log.Fatalf("%v.RecordRoute(_) = _, %v", client, err)
}
for _, point := range points {
	iferr := stream.Send(point); err ! =nil {// Write multiple request messages to the flow
		if err == io.EOF {
			break
		}
		log.Fatalf("%v.Send(%v) = %v", stream, point, err)
	}
}
reply, err := stream.CloseAndRecv()// Fetch the server response from the stream
iferr ! =nil {
	log.Fatalf("%v.CloseAndRecv() got error %v, want %v", stream, err, nil)
}
log.Printf("Route summary: %v", reply)
Copy the code

RouteGuide_RecordRouteClient has a Send(). We can use it to send requests to the server. Once we have finished writing to the stream using Send(), we need to call CloseAndRecv() on the stream to let gRPC know that we have finished writing the request and expect a response. We can get the RPC status from the ERR returned by CloseAndRecv(). If the status is nil, the first return value of CloseAndRecv() ‘is a valid server response.

Bidirectional flow RPC

Finally, let’s look at bidirectional streaming RPC RouteChat(). As with the RecordRoute, we just pass a context object to the method and then get a stream that can be used to write and read messages. This time, however, we return the value through the method’s flow while the server is still writing the message to the message flow.

stream, err := client.RouteChat(context.Background())
waitc := make(chan struct{})
go func(a) {
	for {
		in, err := stream.Recv()
		if err == io.EOF {
			// read done.
			close(waitc)
			return
		}
		iferr ! =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 {
	iferr := stream.Send(note); err ! =nil {
		log.Fatalf("Failed to send a note: %v", err)
	}
}
stream.CloseSend()
<-waitc
Copy the code

The read and write syntax here is very similar to our client stream method, except that we use the stream’s CloseSend() method after the call is complete. Although each side always gets messages from the other in the order in which they were written, both the client and the server can read and write in any order – the streams on both sides run completely independently.

Start the application

To compile and run the server, assuming you in $GOPATH/src/google.golang.org/grpc/examples/route_guide folder, simply:

$ go run server/server.go
Copy the code

Again, run the client:

$ go run client/client.go
Copy the code