gRPC in Action

In this article, we will learn about gRPC and how we can use it by examples.

First: What is gRPC?

let’s take part of the word first, RPC: stands for Remote Procedure Call and it’s a mechanism used when you have client-server model in a shared network to transfer the data between the client and the server through a synchronous process and it’s called “Unary” which means when you send request you wait to get the response from the server, it also supports streaming from the client to the server, the opposite or bidirectional.

RPC is usually used with distributed systems(microservices) to allow the services to communicate with each other in an easy, fast and efficient way.
It can be services with many different languages and technologies or with even the same language, and you don’t have to write a lot of boilerplate code to configure it, it’s dead easy to use.

Now, what is gRPC?

It’s an opensource RPC project developed by Google, uses HTTP/2 for transport and Protocol Buffer (Protobuf) for efficient and fast data serialization.

gRPC supports many languages such as C#, Java, Python and more you check the supported languages from this link.

We will learn the basic concepts of gRPC by doing, we will write a simple service in go to deal with user accounts as a server and use C# as a client to call and use it.

Defining the service and message types by using Protobuf.

let’s create a file called account.proto

syntax = "proto3";

package accountproto;

message CreateAccountRequest{
    string username = 1;
    string email = 2;
}

message CreateAccountResponse{
    string id = 1;
}

message GetAccountInformationRequest{
    string id = 1;
}

message GetAccountInformationResponse{
    string id = 1;
    string username = 2;
    string email = 3;
}

service AccountService{
    rpc CreateAccount(CreateAccountRequest) returns (CreateAccountResponse){}
    rpc GetAccountInformation(GetAccountInformationRequest) returns (GetAccountInformationResponse){}
}

At the begging, we specified that we will use proto3 which is the latest version of protocol buffers language, if we didn’t specify it, it will assume that we’re using proto2 and we don’t want this to happen.

After that, we specified the package name which will be the package name in go or the namespace in C# later, for more information check the link.
The rest of the file consists of messages and service.
message is what will be input or output from a service method which we define after, properties inside the message can be string, bytes, int32, float, double, and you check the rest of the supported types here.
the number we specify after each property must be unique because it will represent the property name after the serialization process.

Inside the service we specify the methods that we need to use in our service, we specify what input we need and what output will be produced from this method and the type of input and output is a message.

We have inside the service two methods one for creating an account which will take CreateAccountRequest as an input and return CreateAccountResponse.
The other one to get the account information which will take GetAccountInformationRequest as an input and return GetAccountInformationResponse.

Ok, this is the definition but where I’ll write my implementation? wait this will come next.

How to compile this file:

  • First: you have to install the compiler from here.
  • Goto the folder and write this command by using the Terminal:
    protoc -I ../account-proto --go_out=plugins=grpc:./proto-go ../account-proto/account.proto
  • After executing this command and hopefully without any errors you will find there a new file called account.pb.go which contain all the commands and the code that you will use to register and use this service, will also contain structs generated from our messages and also the service interface that we have to implement.
  • Then you create main.go and write this implementation.
package main

import (
	"context"
	"fmt"
	"log"
	"net"
	"errors"

	pb "github.com/Ahmad-Magdy/grpc-by-example/proto-go"

	"google.golang.org/grpc"
	"google.golang.org/grpc/reflection"
	"github.com/satori/go.uuid"
)

type Account struct {
	ID       string
	Username string
	Email    string
}

var (
	accounts []*Account
)

type server struct{}

func (s *server) CreateAccount(ctx context.Context, accountRequest *pb.CreateAccountRequest) (*pb.CreateAccountResponse, error) {
	fmt.Printf("Server: Recived, %s\n", accountRequest.GetUsername())

	newAccount := &Account{
		ID: uuid.NewV4().String()  ,
		Username: accountRequest.GetUsername(),
		Email:    accountRequest.GetEmail(),
	}

	accounts = append(accounts, newAccount)
	return &pb.CreateAccountResponse{Id: newAccount.ID}, nil
}

func (s *server) GetAccountInformation(ctx context.Context, m *pb.GetAccountInformationRequest) (*pb.GetAccountInformationResponse,error){
	 for _,accountItem:= range accounts{
		 if accountItem.ID == m.GetId(){
			 return &pb.GetAccountInformationResponse{
											Id:accountItem.ID, 
											Username : accountItem.Username, 
											Email:accountItem.Email, }, nil
		 }

	 }
	 return nil, errors.New("account not found")
}

func main() {
	lis, err := net.Listen("tcp", ":3000")
	if err != nil {
		log.Fatal("Failed to listen: %v", err)
	}

	s := grpc.NewServer()
	pb.RegisterAccountServiceServer(s, &server{})
	reflection.Register(s)
	if err := s.Serve(lis); err != nil {
		log.Fatal("Failed to serve: %v", err)
	}
}

Let me explain what i have made here step by step:

1- I imported pb “github.com/Ahmad-Magdy/grpc-by-example/proto-go” which contains the compiled proto file (“account.pb.go”).
2- I defined a new struct called server which will contain the implementation of the methods we defined in the proto file.
3- We have two methods CreateAccount and GetAccountInformation so i created them.
4- I wrote a simple implementation of the two methods, it’s not efficient or the best but it’s enough to show you how it works.
5- Inside func main i created a new grpc server and register my service in it, the code of registering the service RegisterAccountServiceServer is autogenerated code from the compiled proto file that we defined.

Trying the server:

enough talking and let’s try to run the server program.

No error message, so everything is just fine ??.

The C# part:

let’s go the client part in C# and create a new solution and project.

dotnet new sln --name xyz
dotnet new classlib --name AccountServiceBase
dotnet sln account-service-client.sln add AccountServiceBase/AccountServiceBase.csproj
cd AccountServiceBase
dotnet add package Grpc
dotnet add package Grpc.Tools
dotnet add package Google.Protobuf

We made a new solution then we created a new project as base which will contain the proto packages in C# and also the generated files from the proto file in C# format.

We have to include <Protobuf> as Item group the comiler the .proto file every time we build the project.

By the end of the process we should have csproj looks like this:

<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Google.Protobuf" Version="3.6.1" />
<PackageReference Include="Grpc" Version="1.18.0" />
<PackageReference Include="Grpc.Tools" Version="1.18.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<Protobuf Include="../../**/*.proto" OutputDir="%(RelativePath)" CompileOutputs="false" />
</ItemGroup>

</Project>

If you run “dotnet build” you should be able to see two generated files, Account and AccountGrpc which we will use later.

Now let’s create the actual client in the solutions’ folder.

dotnet new console --name AccountServiceClient
dotnet sln account-service-client.sln add AccountServiceClient/AccountServiceClient.csproj
dotnet add AccountServiceClient/AccountServiceClient.csproj reference AccountServiceBase/AccountServiceBase.csproj
static void Main(string[] args)
{
    var channel = new Channel("localhost:3000", ChannelCredentials.Insecure);

    var client = new AccountService.AccountServiceClient(channel);

    var reply = client.CreateAccount(new CreateAccountRequest() {Username = "Adam",Email = "a@amagdy.me"});
   
    Console.WriteLine($"Reply from the server with message {reply.Id}");

    channel.ShutdownAsync().Wait();
}
  • We defined the Channel to connect to the server which we defined in go before with the same port “:3000” and we chose the Credentials to be Insecure and with that, it will be without SSL, you can read more about that from here.
  • After that, we defined the client to our service.
  • We executed CreateAccount and waited for the response from the server.
  • All the models were created for us thanks to the proto compiler.

Conclusion

In this article, we learned about how to get started with gRPC, we learned about it by doing a small service in go and use it in C# which totally different language.

TODO: Share the project in Github.


This post was originally written for Medium.

About the author

Ahmed Magdy

Software Developer

Add Comment

Ahmed Magdy

Software Developer

Get in touch

You can reach me out through of these channels.