First experience with gRPC

vzldev

VzlDev

Posted on June 21, 2024

First experience with gRPC

I heard some colleagues talking about gRPC framework and its benefits and advantages regarding Rest, so I dig a little in the gRPC world and its concepts to learn new ways to develop an API.

What is gRPC?

gRPC is an open-source API architecture and system. It’s based on the Remote Procedure Call (RPC) model. While the RPC model is broad, gRPC is a specific implementation.
gRPC is a system that implements traditional RPC with several optimizations. For instance, gRPC uses Protocol Buffers and HTTP 2 for data transmission. It also abstracts the data exchange mechanism from the developer.

gRPC vs REST

The following image compares the basics of REST APIs and gRPC:

Image description

Implementation

To better learn the gRPC concepts, I created a simple chat room scenario.

Project Setup

We'll create two projects within a solution:

  • ChatAppServer: The gRPC server that handles chat room logic.
  • ChatAppClient: The client application that users will run to join chat rooms and send messages (This is a console App project).

Then we'll install the following nuget packages:

  • Grpc.AspNetCore
  • Grpc.Tools
  • Grpc.AspNetCore.Server.ClientFactory (this one only on the ChatAppClient project)

Proto file

We'll need to create a '.proto' file to define the gRPC service. This file specifies the service methods and message types.
This is my chat.proto file:

syntax = "proto3";

option csharp_namespace = "ChatApp";

package chat;

service ChatService {
  rpc JoinRoom (JoinRoomRequest) returns (JoinRoomResponse);
  rpc SendMessage (SendMessageRequest) returns (SendMessageResponse);
  rpc ReceiveMessages (ReceiveMessagesRequest) returns (stream ChatMessage);
}

message JoinRoomRequest {
  string username = 1;
  string room = 2;
}

message JoinRoomResponse {
  bool success = 1;
}

message SendMessageRequest {
  string username = 1;
  string room = 2;
  string message = 3;
}

message SendMessageResponse {
  bool success = 1;
}

message ReceiveMessagesRequest {
  string room = 1;
}

message ChatMessage {
  string username = 1;
  string message = 2;
  int64 timestamp = 3;
}

Enter fullscreen mode Exit fullscreen mode

We have to include the .proto file on the .csproj so we can enable the code generator, so on the ChatAppServer.csproj file add the following:

<ItemGroup>
  <Protobuf Include="Protos\chat.proto" GrpcServices="Server" />
</ItemGroup>
Enter fullscreen mode Exit fullscreen mode

And on the ChatAppClient.csproj add the following:

<ItemGroup>
  <Protobuf Include="Protos\chat.proto" GrpcServices="Client" />
</ItemGroup>
Enter fullscreen mode Exit fullscreen mode

After this, if we clean, restore and build the projects, the code will be generated and you can check it out on the path "obj\Debug\net6.0\Protos".

Implement the server

In the server project, implement the ChatService.

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading.Tasks;
using ChatApp;
using Grpc.Core;
using Microsoft.Extensions.Logging;

namespace ChatAppServer.Services
{
    public class ChatServiceImpl : ChatService.ChatServiceBase
    {
        private static readonly ConcurrentDictionary<string, List<IServerStreamWriter<ChatMessage>>> _rooms = new ConcurrentDictionary<string, List<IServerStreamWriter<ChatMessage>>>();

        public override Task<JoinRoomResponse> JoinRoom(JoinRoomRequest request, ServerCallContext context)
        {
            if (!_rooms.ContainsKey(request.Room))
            {
                _rooms[request.Room] = new List<IServerStreamWriter<ChatMessage>>();
            }
            return Task.FromResult(new JoinRoomResponse { Success = true });
        }

        public override Task<SendMessageResponse> SendMessage(SendMessageRequest request, ServerCallContext context)
        {
            if (_rooms.TryGetValue(request.Room, out var clients))
            {
                var message = new ChatMessage
                {
                    Username = request.Username,
                    Message = request.Message,
                    Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
                };

                foreach (var client in clients)
                {
                    client.WriteAsync(message);
                }
            }
            return Task.FromResult(new SendMessageResponse { Success = true });
        }

        public override async Task ReceiveMessages(ReceiveMessagesRequest request, IServerStreamWriter<ChatMessage> responseStream, ServerCallContext context)
        {
            if (_rooms.TryGetValue(request.Room, out var clients))
            {
                clients.Add(responseStream);
                try
                {
                    await Task.Delay(Timeout.Infinite, context.CancellationToken);
                }
                finally
                {
                    clients.Remove(responseStream);
                }
            }
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

And we need to add the following on the Progra.cs file:

builder.Services.AddGrpc();
app.MapGrpcService<ChatServiceImpl>();
Enter fullscreen mode Exit fullscreen mode

Implement the client

Modify the Program.cs file of the client project:

using System;
using System.Threading.Tasks;
using Grpc.Net.Client;
using ChatApp;
using Grpc.Core;

class Program
{
    static async Task Main(string[] args)
    {
        // Create a channel to the gRPC server
        var channel = GrpcChannel.ForAddress("https://localhost:7187");
        var client = new ChatService.ChatServiceClient(channel);

        Console.WriteLine("Enter your username:");
        var username = Console.ReadLine();

        Console.WriteLine("Enter the room you want to join:");
        var room = Console.ReadLine();

        // Join the specified room
        var joinResponse = await client.JoinRoomAsync(new JoinRoomRequest
        {
            Username = username,
            Room = room
        });

        if (joinResponse.Success)
        {
            Console.WriteLine($"Joined room: {room}");
        }
        else
        {
            Console.WriteLine("Failed to join room");
            return;
        }

        // Task for receiving messages
        var receiveTask = Task.Run(async () =>
        {
            using var call = client.ReceiveMessages(new ReceiveMessagesRequest { Room = room });
            await foreach (var message in call.ResponseStream.ReadAllAsync())
            {
                Console.WriteLine($"[{message.Username}] {message.Message}");
            }
        });

        // Loop to send messages
        while (true)
        {
            var message = Console.ReadLine();
            if (message.ToLower() == "exit") break;

            var sendMessageResponse = await client.SendMessageAsync(new SendMessageRequest
            {
                Username = username,
                Room = room,
                Message = message
            });

            if (!sendMessageResponse.Success)
            {
                Console.WriteLine("Failed to send message");
            }
        }

        // Wait for the receiving task to complete
        await receiveTask;
    }
}

Enter fullscreen mode Exit fullscreen mode

Running the application

Use the dotnet run command to run the projects.
Open 2 terminals of clients to interact with each other.

Image description

Image description

Conclusion

This is still a very simple case of implementing gRPC framework to develop an API, and I still have a lot to work on and learn about this, I'm very excited!

And that's it guys, I hope you liked it, stay tuned for more!

💖 💪 🙅 🚩
vzldev
VzlDev

Posted on June 21, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

First experience with gRPC
grpc First experience with gRPC

June 21, 2024

First experience with gRPC
grpc First experience with gRPC

June 21, 2024