gRPC and Go
Why a new standard?
At the core, gRPC is just another client-server communication tool that tries to solve problems with its predecessors. Built on top of the TCP communication channel, gRPC allows a bi-directional server and client data streaming. Using protobuf allows for code gen across frameworks, and standardizes communication channels, and the Client Libraries.
Protobuf
Protobufs can be used for more than gRPC. They are a good replacement for JSON since they are a binary format and have a lesser size compared to JSON while transmitting in the network. Similar to JSON, there is in-built support for most common languages, as well as, conversion to JSON object itself.
Protobuf and gRPC
To define protobuf for gRPC, we don't have to change much about them.
For this example, I'm going to create 2 message types to be used in MessageService, and 2 endpoints. 1 for sending message and another for getting server time. Both these endpoints belong to Message Service, which will be acting as our server.
// protobuf syntax version
syntax = "proto2";
// go package name
option go_package = "../grpc";
// request message
message MessageReq {
optional string body = 1;
optional int64 timestamp = 100;
}
// response message
message MessageResp {
optional string body = 1;
optional string debuf_msg = 100;
optional int64 err_code = 101;
}
// message service definition
service MessageService {
// send message endpoint
rpc SendMessage(MessageReq) returns (MessageResp) {}
// get server clock time
rpc GetServerTime(MessageReq) returns (MessageResp) {}
}After generating this protofile with grpc plugin, we should be able to use Message Service out of the box.
To generate the proto & service files, we can use the following command
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative *.protoServices
Since gRPC is a client-server protocol, we need to define 2 separate services, one running as our client, and the other one as our server. For the purposes of this example, I'm going to run both of them on the localhost, and will be running the server on the port 8080.
Server
Next step is to implement the processors defined in our proto rpc file. The interface of these processors are already defined in the proto rpc file, and we can use them for reference. A simple implementation would look as follow
type Processor struct {
}
func (processor *Processor) SendMessage(ctx context.Context, req *pb.MessageReq) (resp *pb.MessageResp, err error) {
resp = &pb.MessageResp{}
return resp, nil
}
func (processor *Processor) GetServerTime(ctx context.Context, req *pb.MessageReq) (resp *pb.MessageResp, err error) {
fmt.Println(req)
resp = &pb.MessageResp{}
return resp, nil
}
To invoke our server, we need to run a new TCP Listener first, let's do that
// init a new tcp addr resolver on the port 8080
tcpAddr, err := net.ResolveTCPAddr("tcp", ":8080")
if err != nil {
return err
}
// init the new listener of the port 8080
listener, err := net.ListenTCP("tcp", tcpAddr)
if err != nil {
return err
}Afterwards, we need to run our server. To do so, we need to attach our listener to a new grpc server.
// creating a new grpc server
grpcServer := grpc.NewServer()
// attaching our processors object, which include the implementations for the grpc Message Service to the grpc server
pb.RegisterMessageServiceServer(grpcServer, &Processor{})
// serve the traffic for the incoming calls
if err = grpcServer.Serve(listener); err != nil {
logrus.Panicf("failed to run server. err: %v", err)
}We should be able to start recieving traffic at this point.
Client
Grpc connections are safe to be used concurrently, and have support for multiplexing through an existing HTTP/2 connection. Therefore, (as much as I hate it!) for this implementation, we will follow gRPC's best practices, and use a single connection. Connection pools are cool and all, but I guess thanks to gRPC we don't need to deal with them.
To start we need to open a new grpc connection to our server
conn, err := grpc.Dial(":8080", grpc.WithInsecure())
// dont forget to close the connection at the end
// handle the err if we fail to connect to the server
if err != nil {
return nil, err
}After we have our grpc connection, we can start using our client service simply by creating a new instance of the Client interface
c := pb.NewMessageServiceClient(conn)To send a message using our newly created Client, we can simply call the rpc function underneath it and watch the magic happen
resp, err := c.SendMessage(context.Background(),
&pb.MessageReq{Body: proto.String("Hello")})Conclusion
gRPC package can make implementing a TCP connection much easier than implementing it from scratch. And with advantages of gRPC over REST, it's personally my prefered method when it comes to client-server communication.
I will try to implement the same approach using TCP, to have a better comparison in code complexity, and time taken as well.