Introduction
gRPC is a modern open source high performance RPC framework that can run in any environment, initially developed by Google. It can efficiently connect services in and across data centers with pluggable support for load balancing, tracing, health checking and authentication. It is also applicable in last mile of distributed computing to connect devices, mobile applications and browsers to backend services.
The main usage scenarios of gRPC are:
- Efficiently connecting polyglot services in microservices style architecture
- Connecting mobile devices, browser clients to backend services
- Generating efficient client libraries
gRPC passes data through protocol buffers or protobuf. They are defined by Google’s developer guide as:
“… a flexible, efficient, automated mechanism for serializing structured data — think XML, but smaller, faster, and simpler. You define how you want your data to be structured once, and then you can use special generated source code to easily write and read your structured data to and from a variety of data streams and using a variety of languages. You can even update your data structure without breaking deployed programs that are compiled against the “old†format.â€
So, protocol buffers are to gRPC what XML or JSON are to REST. They’re a fast, small, and flexible way of serializing data to be passed over the network.
Just like REST, gRPC can be used cross-language which means that if we have written a web service in Golang, a Java written application can still use that web service, which makes gRPC web services very scalable. gRPC uses HTTP/2 to support highly performant and scalable API’s and makes use of binary data rather than just text which makes the communication more compact and more efficient. It is also type-safe. This basically means that we can’t give an apple while a banana is expected. When the server expects an integer, gRPC won’t allow us to send a string because these are two different types.
In this article, we will use following steps to create a typical client-server application using gRPC:
- Define a service in a .proto file
- Create batch file for compiling .proto file
- Generate server and client code using the protocol buffers compiler
- Create the server application, implementing the generated services interfaces and spawning the gRPC server
- Create the client application, making RPC calls using generated stubs
Prerequisites
Before starting, we will need to ensure that .NET Core is installed in machine. Along with it, we will also need to install dependencies libraries from nuget package manager.
- Grpc : It is a c# implementation of gRPC. We can install it through following command in package manager console. PM> Install-Package Grpc -Version 1.17.0
- Tools: This library is gRPC and protocol buffer compiler for managed C# and native C++ projects. We can install it through following command in package manager console. PM> Install-Package Grpc.Tools -Version 1.17.0
- Protobuf: It is a C# runtime library for Protocol Buffers – Google’s data interchange format. Following command is used to install this library in package manager console. PM> Install-Package Google.Protobuf -Version 3.6.1
Define a protobuf service
Before defining service, we should know that gRPC supports C# from proto3 only. Currently, proto2 is the default protocol buffers version. It supports C#, Dart, Go and Ruby from proto3.
Here we have created a proto file ‘javraservice.proto’
syntax="proto3"; package javra.grpc.test; service JavraService{ Â Â Â rpc Search(SearchRequest) returns (SearchResponse); } message SearchRequest{ Â Â Â string query=1; Â Â Â int32 page_number=2; Â Â Â enum ServiceType{ Â Â Â Â Â Â Â UNIVERSAL=0; Â Â Â Â Â Â Â WEB=1; Â Â Â Â Â Â Â LOCAL=2; Â Â Â } Â Â Â ServiceType service_type=3; } message SearchResponse{ Â Â Â string result=1; }
As mentioned earlier, we have defined the version of protocol buffers as proto3 through
syntax=proto3;
And we have defined the package as javra.grpc.test; This same package name will be used as namespace when converting protocol buffers file into C# generated code.
We have defined two messages, SearchRequest and SearchResponse. SearchRequest has three properties, query as string, page_number as integer and ServiceType which is an enum. Similarly, SearchResponse has one property, result which is string.
Then, we have defined a service: JavraService which contains a method Search(). This service has SearchRequest message in its parameter and will return SearchResponse as result. To specify service, we specify named service in .proto file. Then we define rpc methods inside our service definition, specifying request and response type.
In this article we have used simple RPC where the client sends a request to the server using the client object and waits for response to come back, just like a normal function call. It is also knows as Unary RPC. There are other RPC too.
A server-side streaming RPC is where a client sends request to server, and gets stream to read a sequence of messages back. The client reads from the returned stream until there are no more messages. We specify a server-side streaming method by placing the stream keyword before the response type as following:
rpc Search(SearchRequest) returns (stream SearchResponse);
A client-side streaming RPC is where the client writes a sequence of messages and sends them to the server, again using a provided stream. Once the client has finished writing the messages, it waits for the server to read them all and return its response. We specify a client-side streaming method by placing the stream keyword before the request type.
rpc Search(stream SearchRequest) returns (SearchResponse);
A bidirectional streaming RPC is where both sides send a sequence of messages using a read-write stream. The two streams operate independently, so clients and servers can read and write in whatever order they like. We specify this type of method by placing the stream keyword before both the request and the response.
rpc Search(stream SearchRequest) returns (stream SearchResponse);
Creating batch file for compiling protocol buffers file
Following batch code is used to compile the provided ‘javraservice.proto’
setlocal set PROTOC=%UserProfile%\.nuget\packages\Google.Protobuf.Tools\3.6.1\tools\windows_x64\protoc.exe set PLUGIN=%UserProfile%\.nuget\packages\Grpc.Tools\1.17.0\tools\windows_x64\grpc_csharp_plugin.exe REM Defining proto file location. There shouldn't be whitespace before path set ProtoFilePath=protos\javraservice.proto REM Project path for proto set ProtoOutputPath=JavraGrpc\JavraGrpc\Protos REM Services path set ServiceOutputPath=JavraGrpc\JavraGrpc\Services REM Create folder if path doesnot exist MKDIR .\JavraGrpc\JavraGrpc\Protos MKDIR .\JavraGrpc\JavraGrpc\Services REM compile protofile %PROTOC% -I.\ --csharp_out %ProtoOutputPath% %ProtoFilePath% --grpc_out %ServiceOutputPath% --plugin=protoc-gen-grpc=%PLUGIN% endlocal
PROTOC variable refers to the location of protoc.exe, which is generated when we install Grpc.Tools library through nuget package manager. Similarly PLUGIN refers to the location of grpc_csharp_plugin.exe
NOTE:Â Root directory for given batch code will be the path where the batch file is located.
ProtoFilePath refers to the path where .proto file is located from root directory. In case of multiple proto files in Windows OS, we will have to define path for each individual files. However in Linux, we can easily get all proto files by using wildcard (*). i.e.,
proto_directory_path/*.proto
ProtoOutputPath refers to the path where a C# generated code of messages to populate, serialize, and retrieve request and response of protocol buffers after compiling protocol buffers will be saved. Here JavraGrpc\JavraGrpc refers to SolutionName\ProjectName
ServiceOutputPath refers to the path where a C# generated code of service defined in protocol buffers file will be saved. It includes an abstract class JavraService.JavraServiceBase to inherit from when defining JavraService service implementation. It also includes JavraService.JavraServiceClient that can be used to access remote JavraService instances
%PROTOC% -I.\ --csharp_out %ProtoOutputPath% %ProtoFilePath% --grpc_out %ServiceOutputPath% --plugin=protoc-gen-grpc=%PLUGIN%
Above command will compile the protocol buffers file into C# generated code.
Creating server application
In existing solution, we will add a new console project ‘JavraGrpc.Server’ where we will define a server for JavraGrpc service. We will need to add reference of ‘JavraGrpc’ project in this new project.
Firstly, we will add a new CS file ‘JavraSearch’ which will extend a partial class JavraService.JavraServiceBase (this class is auto generated by gRPC compiler and is located in JavraserviceGrpc.cs). In this class, we will override a Search method (this method is the service function we defined in JavraService.proto). Here, we can perform all the data manipulations.  For simplicity, we will convert the request object sent by client to JSON and then return it as result.
public class JavraSearch : JavraService.JavraServiceBase  {    public override Task<SearchResponse> Search(SearchRequest request, ServerCallContext context)    {      var result = JsonConvert.SerializeObject(request);      return Task.FromResult(new SearchResponse      {        Result = result      });    }  }
Then, we will define a server in Main method as below.
class Program {    static void Main(string[] args)    {        const int servicePort = 50081;        var server = new Grpc.Core.Server        {            Services = { JavraService.BindService(new JavraSearch()) },            Ports = { new ServerPort("localhost", servicePort, ServerCredentials.Insecure) }        };        server.Start();        Console.WriteLine("Server started");        Console.WriteLine("Press any key to exit");        Console.ReadKey();    } }
When defining a server, we will need to bind a service which we just created earlier as ‘JavraSearch’ and specify a port. Then we will need to start the server and then server is ready to operate.
Creating a client application
Similarly, we will create a new project ‘JavraGrpc.Client’ to create a client application. In Main method, we will perform following steps to create a channel and client and then request to server.
static void Main(string[] args) {    Console.WriteLine("Program started");    //define channel    var channel = new Channel("127.0.0.1:50081", ChannelCredentials.Insecure);    //define client. It is also known as stub in other languages like python    var client = new JavraService.JavraServiceClient(channel);    //create request object    var searchRequest = new SearchRequest    {        Query = "This is a sample query..",        PageNumber = 1,        ServiceType = SearchRequest.Types.ServiceType.Web    };    //send service request    var response = client.Search(searchRequest);    Console.WriteLine($"Response= {response}");    //shutdown channel    channel.ShutdownAsync().Wait();    Console.WriteLine("Press any key to exit");    Console.ReadKey(); }
We create a Channel that connects to a specific host. Here, ‘127.0.0.1’ refers to the ‘localhost’ and ‘50081’ refers to the server port we defined in JavraGrpc.Server project.
Then we can define a Client or Stub for JavraService by passing channel we defined a step ago.
Once a client is created, we can create a request object and then send service request to the server ‘JavraGrpc.Server’.
Based on how we implemented Search service earlier on ‘JavraGrpc.Server’ project, the server will convert the request into JSON string and then respond it to the client. Then we will shut down a channel cleanly.
Making calls to server from client
We will need to run both ‘JavraGrpc.Server’ and ‘JavraGrpc.Client’ project in order to make calls to server from client. We can perform it by starting project in two visual studio windows, making each project as startup project in each window. Or we can open one of the projects in command prompt and start project with following command.
dotnet run –f netcoreapp2.1
We will get result in JSON format as shown in above figure as we have defined while creating server application.
Conclusion
In this article, we have managed to create a simple gRPC server in C# .NET Core and made RPC call from client based on protocol buffers definition.