gRPC in .NET 6
gRPC is a modern open-source high-performance Remote Procedure Call (RPC) framework that can run in any environment. It uses Protobuf as its Interface Definition Language (IDL).
This is now becoming really popular with the advent of Cloud Native apps.
The project
The gRPC is a CNCF (Cloud Native Computing Foundation)1 project that was accepted to CNCF on February 16, 2017, and is at the Incubating project maturity level.
Incubating projects are considered stable and used successfully in production.
Going backwards, this initiative started with Google, to make an open-source project out of an existing RPC infrastructure called Stubby, used to connect microservices in their own platform.
For more details and background on gRPC, check this link.
HTTP/2
Google is the father of HTTP/2, an evolution of an experimental protocol called SPDY.
HTTP/2 introduced a new binary framing layer, which dictates how the messages are encapsulated and transferred between client and server.
Source: https://web.dev/performance-http2/
This makes HTTP/2 compacted (binary) and faster (TCP) when compared with HTTP/1.x, which only leverages simple text and HTTP as a message transporter.
Terminology
- Stream: A bidirectional flow of bytes within an established connection, which may carry one or more messages. Different from the HTTP/1.x that can only transmit a single request or response within an established connection.
- Message: A complete sequence of frames that map to a logical request or response message.
- Frame: The smallest unit of communication, each containing a frame header, which identifies the stream to which the frame belongs.
Source: https://web.dev/performance-http2/
In summary:
- Within a single TCP connection many streams are transmitted.
- Within each stream, there can be many messages.
- The messages are a sequence of frames.
- Frames contain the information transmitted.
Characteristics
Multiplexing
With HTTP/1.x, if the client wants to make multiple parallel requests to improve performance, then multiple TCP connections must be used.
Source: https://web.dev/performance-http2/
The new binary framing layer in HTTP/2 removes these limitations and enables full request and response multiplexing.
It also resolves the head-of-line blocking problem2 found in HTTP/1.x and eliminates the need for multiple connections. As a result, this makes our applications faster, simpler, and cheaper to deploy.
Stream prioritization
When the message is split into multiple frames, it can be ordered in a way to optimise performance. The HTTP/2 standard allows each stream to have an associated weight and dependency:
- Each stream may be assigned an integer weight between 1 and 256.
- Each stream may be given an explicit dependency on another stream.
Other characteristics
Considering what is important to enabling gRPC, the previous characteristics have enabled the RPC framework. There are other HTTP/2 inherent characteristics that are very well described here, so I'm not extending the explanation of those additional characteristics in this article.
Protobuf
Protocol buffers provide a language-neutral, platform-neutral, extensible mechanism for serializing structured data that generates native language bindings.
It means that you get to create a generic definition of a data contract, using .proto
files, that get compiled using a language-specific runtime library as C#/Python and other languages, then generating a language-specific contract to interface with data.
Another important aspect of protocol buffers is the serialization format for data that is written to a file (or sent across a network connection).
Message
The syntax of protobuf messages is simple. Below there are some examples I created.
File: address.proto
syntax = "proto3";
option csharp_namespace = "GrpcServiceSample";
package address;
message Address {
string number = 1;
string street_name = 2;
string suburb = 3;
string city = 4;
string country = 5;
}
Note that we are using proto3
as syntax.
In particular on C#, the option csharp_namespace
was set as language specific, and it will be translated into a C# namespace when compiled.
Packages help organise messages. You cannot have the same message name duplicated on a package, but you can create the same message name in a different package.
Messages have attributes, and they need to be numerically identifiable.
File: user.proto
syntax = "proto3";
option csharp_namespace = "GrpcServiceSample";
package user;
import "protos/address.proto";
service UserService {
rpc GetUserDetails(UserRequest) returns (UserResponse);
}
message UserRequest {
int32 user_id = 1;
}
message UserResponse {
int32 user_id = 1;
string first_name = 2;
string last_name = 3;
address.Address address = 4;
}
We can use import to reference other proto messages.
Services need to be specified in proto files, for rpc calls between client and server.
For further details on creating proto files, the Protobuf style guide contains recommendations / best practices.
Running the service
Consider the VStudio 2022 template:
Server:
using GrpcServiceSample.Interceptors;
using GrpcServiceSample.Services;
var builder = WebApplication.CreateBuilder(args);
// Additional configuration is required to successfully run gRPC on macOS.
// For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682
// Add services to the container.
builder.Services.AddGrpc(options =>
{
options.Interceptors.Add<ServerLoggerInterceptor>();
});
var app = builder.Build();
// Configure the HTTP request pipeline.
app.MapGrpcService<GreeterService>();
app.MapGrpcService<UserService>();
app.MapGet("/", () => "Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909");
app.Run();
The UserService
is created:
using Grpc.Core;
namespace GrpcServiceSample.Services
{
public class UserService : GrpcServiceSample.UserService.UserServiceBase
{
private readonly ILogger<UserService> _logger;
public UserService(ILogger<UserService> logger)
{
_logger = logger;
}
public override Task<UserResponse> GetUserDetails(UserRequest request, ServerCallContext context)
{
return Task.FromResult(new UserResponse
{
UserId = 1,
FirstName = "Bill",
LastName = "Lumbergh",
Address = new Address()
{
Number = "1A",
StreetName = "Initech Street",
Suburb = "Initech Suburb",
City = "Austin, Texas",
Country = "USA"
}
});
}
}
}
An interceptor is added to the server, that will capture the request, and log details before the request happens:
using Grpc.Core;
using Grpc.Core.Interceptors;
namespace GrpcServiceSample.Interceptors
{
public class ServerLoggerInterceptor : Interceptor
{
private readonly ILogger _logger;
public ServerLoggerInterceptor(ILogger<ServerLoggerInterceptor> logger)
{
_logger = logger;
}
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
TRequest request,
ServerCallContext context,
UnaryServerMethod<TRequest, TResponse> continuation)
{
_logger.LogInformation($"Starting receiving call. Type: {MethodType.Unary}. " +
$"Method: {context.Method}.");
try
{
return await continuation(request, context);
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error thrown by {context.Method}.");
throw;
}
}
}
}
Then add a new Console Application, and make sure to service reference the gRPC server:
Generate the client:
In the end, you should have referenced both address.proto
and user.proto
files:
Then to make a request to the server, this is the client implementation:
using System.Text.Json;
using Grpc.Net.Client;
using GrpcServiceSample;
var channel = GrpcChannel.ForAddress("https://localhost:7147");
var client = new UserService.UserServiceClient(channel);
var userDetails = await client.GetUserDetailsAsync(new UserRequest() {UserId = 1});
Console.WriteLine(JsonSerializer.Serialize(userDetails));
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
The channel is created, and then the UserService can be requested.
This is what the solution looks like:
Check this sample in the PlayGoKids repository.
Where to use
gRPC is well suited to the following scenarios3:
- Microservices: gRPC is great for lightweight microservices where efficiency is critical because it is designed for low latency and high throughput communication.
- Point-to-point real-time communication: gRPC services can push messages in real-time without polling because it has excellent support for bi-directional streaming.
- Multi-language environments: gRPC tooling supports all popular development languages.
- Network constrained environments: gRPC messages are serialized with Protobuf, a lightweight message format.
- Inter-process communication (IPC): IPC transports such as Unix domain sockets and named pipes can be used with gRPC to communicate between apps on the same machine.