Let’s start off by defining a really simple gRPC server in Go. Once we have a simple server up and running we can set about creating a gRPC client that will be able to interact with it.
We will start off by writing the logic within our main function to listen on a port for incoming TCP connections:
main.go
package main
import (
"log"
"net"
)
func main() {
lis, err := net.Listen("tcp", ":9000")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
}
Next, we’ll want to import the official gRPC package from golang.org so that we can create a new gRPC server, then register the endpoints we want to expose before serving this over our existing TCP connection that we have defined above:
main.go
package main
import (
"log"
"net"
"google.golang.org/grpc"
)
func main() {
lis, err := net.Listen("tcp", ":9000")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
grpcServer := grpc.NewServer()
if err := grpcServer.Serve(lis); err != nil {
log.Fatalf("failed to serve: %s", err)
}
}
This right here is the absolute minimum for a gRPC server written in go. However, right now it doesn’t exactly do much.
Adding Some Functionality with proto file
Implementing RouteGuide
As you can see, our server has a routeGuideServer struct type that implements the generated RouteGuideServer interface:
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
routeGuideServer implements all our service methods. Let’s look at the simplest type first, GetFeature, which just gets a Point from the client and returns the corresponding feature information from its database in a 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
}
The method is passed a context object for the RPC and the client’s Point protocol buffer request. It returns a Feature protocol buffer object with the response information and an error. In the method we populate the Feature with the appropriate information, and then return it along with an nil error to tell gRPC that we’ve finished dealing with the RPC and that the Feature can be returned to the client.
Server-side streaming RPC
Now let’s look at one of our streaming RPCs. ListFeatures is a server-side streaming RPC, so we need to send back multiple Features to our client.
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
}
As you can see, instead of getting simple request and response objects in our method parameters, this time we get a request object (the Rectangle in which our client wants to find Features) and a special RouteGuide_ListFeaturesServer object to write our responses.
In the method, we populate as many Feature objects as we need to return, writing them to the RouteGuide_ListFeaturesServer using its Send() method. Finally, as in our simple RPC, we return a nil error to tell gRPC that we’ve finished writing responses. Should any error happen in this call, we return a non-nil error; the gRPC layer will translate it into an appropriate RPC status to be sent on the wire.
Client-side streaming RPC
Now let’s look at something a little more complicated: the client-side streaming method RecordRoute, where we get a stream of Points from the client and return a single RouteSummary with information about their trip. As you can see, this time the method doesn’t have a request parameter at all. Instead, it gets a RouteGuide_RecordRouteServer stream, which the server can use to both read and write messages - it can receive client messages using its Recv() method and return its single response using its 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()),
})
}
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
}
}
In the method body we use the RouteGuide_RecordRouteServers Recv() method to repeatedly read in our client’s requests to a request object (in this case a Point) until there are no more messages: the server needs to check the error returned from Recv() after each call. If this is nil, the stream is still good and it can continue reading; if it’s io.EOF the message stream has ended and the server can return its RouteSummary. If it has any other value, we return the error “as is” so that it’ll be translated to an RPC status by the gRPC layer.
Bidirectional streaming RPC
Finally, let’s look at our bidirectional streaming 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
}
}
}
}
This time we get a RouteGuide_RouteChatServer stream that, as in our client-side streaming example, can be used to read and write messages. However, this time we return values via our method’s stream while the client is still writing messages to their message stream.
The syntax for reading and writing here is very similar to our client-streaming method, except the server uses the stream’s Send() method rather than SendAndClose() because it’s writing multiple responses. Although each side will always get the other’s messages in the order they were written, both the client and server can read and write in any order — the streams operate completely independently.
Starting the server
Once we’ve implemented all our methods, we also need to start up a gRPC server so that clients can actually use our service. The following snippet shows how we do this for our RouteGuide service:
flag.Parse()
lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%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)
To build and start a server, we:
- Specify the port we want to use to listen for client requests using
lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", *port)). - Create an instance of the gRPC server using
grpc.NewServer(). - Register our service implementation with the gRPC server.
- Call
Serve()on the server with our port details to do a blocking wait until the process is killed orStop()is called.
https://tutorialedge.net/golang/go-grpc-beginners-tutorial/