Construct a Easy Chat Server With gRPC in .Web Core

On this article, we’ll create a easy concurrent gRPC chat server utility. We are going to use .NET Core, a cross-platform, open-source, and modular framework, to construct our chat server utility. We are going to cowl the next subjects:

  • A quick introduction to gRPC.
  • Establishing the gRPC surroundings and defining the service contract.
  • Implementing the chat service and dealing with consumer requests.
  • Dealing with a number of purchasers concurrently utilizing asynchronous programming
  • Broadcasting chat messages to all linked purchasers in the identical room.

By the top of this tutorial, you’ll have an understanding of the best way to use gRPC to construct a chat server.

What Is gRPC?

gRPC is an acronym that stands for Google Distant Process Calls. It was initially developed by Google and is now maintained by the Cloud Native Computing Basis (CNCF).  gRPC permits you to join, invoke, function, and debug distributed heterogeneous purposes as simply as making an area operate name. 

gRPC makes use of HTTP/2 for transport,  a contract-first strategy to API growth, protocol Buffers (protobuf) because the interface definition language in addition to its underlying message interchange format. It could possibly help 4 sorts of API (Unary RPC, Server streaming RPC, Shopper streaming RPC, and Bidirectional  streaming RPC). You possibly can learn extra about gRPC here.

Getting Began:

Earlier than we begin to write code, an set up of .NET core must be completed, and ensure you have the next stipulations in place:

  • Visible Studio Code, Visible Studio, or JetBrains Rider IDE.
  • .NET Core.
  • gRPC .NET
  • Protobuf

Step 1: Create a gRPC Mission From the Visible Studio or Command Line

  • You should use the next command to create a brand new venture. If profitable, you must have it created within the listing you specify with the title ‘ChatServer.’
dotnet new grpc -n ChatServerApp

  • Open the venture along with your chosen editor. I’m utilizing visible studio for Mac.

Open the project

Step 2: Outline the Protobuf Messages in a Proto File

Protobuf Contract:

  1. Create .proto file named server.proto inside the protos folder. The proto file is used to outline the construction of the service, together with the message sorts and the strategies that the service helps. 
syntax = "proto3";

possibility csharp_namespace = "ChatServerApp.Protos";

package deal chat;

service ChatServer 
  // Bidirectional communication stream between consumer and server
  rpc HandleCommunication(stream ClientMessage) returns (stream ServerMessage);



//Shopper Messages:
message ClientMessage 
  oneof content material 
	ClientMessageLogin login = 1;
	ClientMessageChat chat = 2;
  


message ClientMessageLogin 
  string chat_room_id = 1;
  string user_name = 2;



message ClientMessageChat 
  string textual content = 1;


//Server Messages
message ServerMessage 
  oneof content material 
	ServerMessageLoginSuccess login_success = 1;
	ServerMessageLoginFailure login_failure = 2;
	ServerMessageUserJoined user_joined = 3;
	ServerMessageChat chat = 4;
  


message ServerMessageLoginFailure 
  string cause = 1;


message ServerMessageLoginSuccess 


message ServerMessageUserJoined 
  string user_name = 1;


message ServerMessageChat 
  string textual content = 1;
  string user_name = 2;

  • ChatServer defines the principle service of our chat utility, which features a single RPC technique known as HandleCommunication.  The tactic is used for bidirectional streaming between the consumer and the server. It takes a stream of ClientMessage as enter and returns a stream of ServerMessage as output. 
service ChatServer 
  // Bidirectional communication stream between consumer and server
  rpc HandleCommunication(stream ClientMessage) returns (stream ServerMessage);

  • ClientMessageLogin, which will likely be despatched by the consumer, has two fields known as chat_room_id and user_name. This message sort is used to ship login info from the consumer to the server. The chat_room_id discipline specifies the chat room that the consumer desires to hitch, whereas the user_name discipline specifies the username that the consumer desires to make use of within the chat room
message ClientMessageLogin 
  string chat_room_id = 1;
  string user_name = 2;

  • ClientMessageChat which will likely be used to ship chat messages from the consumer to the server. It incorporates a single discipline textual content.
message ClientMessageChat 
  string textual content = 1;

  • ClientMessage defines the various kinds of messages {that a} consumer can ship to the server. It incorporates a oneof discipline, which implies that solely one of many fields will be set at a time. should you use oneof, the generated C# code will comprise an enumeration indicating which fields have been set. The sphere names are “login” and “chat“which corresponds to the ClientMessageLogin and ClientMessageChat messages respectively
message ClientMessage 
  oneof content material 
	ClientMessageLogin login = 1;
	ClientMessageChat chat = 2;
  

  • ServerMessageLoginFailure defines the message despatched by the server to point {that a} consumer did not log in to the chat room. The rationale discipline specifies the explanation for the failure.
message ServerMessageLoginFailure 
  string cause = 1;

  •  ServerMessageLoginSuccess defines the message despatched by the server to point {that a} consumer has efficiently logged in to the chat room. It incorporates no fields and easily alerts that the login was profitable. When a consumer sends a ClientMessageLogin message, the server will reply with both a ServerMessageLoginSuccess message or a ServerMessageLoginFailure message, relying on whether or not the login was profitable or not. If the login was profitable, the consumer can then begin to ship ClientMessageChat messages to start out chat messages.
message ServerMessageLoginSuccess 

  • Message ServerMessageUserJoined defines the message despatched by the server to the consumer when a brand new person joins the chat room.
message ServerMessageUserJoined 
  string user_name = 1;

  • Message ServerMessageChat defines the message despatched by the server to point {that a} new chat message has been acquired. The textual content discipline specifies the content material of the chat message, and the user_name discipline specifies the username of the person who despatched the message.
message ServerMessageChat 
  string textual content = 1;
  string user_name = 2;

  • Message ServerMessage defines the various kinds of messages that may be despatched from the server to the consumer. It incorporates a oneof discipline named content material with a number of choices. The sphere names are “login_success,” “login_failure,” “user_joined,” and “chat,” which correspond to the ServerMessageLoginSuccess, ServerMessageLoginFailure, ServerMessageUserJoined, and ServerMessageChat messages, respectively.
message ServerMessage 
  oneof content material 
	ServerMessageLoginSuccess login_success = 1;
	ServerMessageLoginFailure login_failure = 2;
	ServerMessageUserJoined user_joined = 3;
	ServerMessageChat chat = 4;
  

Step 3: Add a ChatService Class

Add a ChatService class that’s derived from ChatServerBase(generated from the server.proto file utilizing the gRPC codegen protoc). We then override the HandleCommunication technique. The implementation of the HandleCommunication technique will likely be liable for dealing with the communication between the consumer and the server.

public class ChatService : ChatServerBase

    non-public readonly ILogger<ChatService> _logger;

    public ChatService(ILogger<ChatService> logger)
    
        _logger = logger;
    

    public override Activity HandleCommunication(IAsyncStreamReader<ClientMessage> requestStream, IServerStreamWriter<ServerMessage> responseStream, ServerCallContext context)
    
        return base.HandleCommunication(requestStream, responseStream, context);
    

Step 4: Configure gRPC

In program.cs file:

utilizing ChatServer.Providers;
utilizing Microsoft.AspNetCore.Server.Kestrel.Core;

var builder = WebApplication.CreateBuilder(args);


/*
// Further configuration is required to efficiently run gRPC on macOS.
// For directions on the best way to configure Kestrel and gRPC purchasers on macOS,
// go to https://go.microsoft.com/fwlink/?linkid=2099682

   To keep away from lacking ALPN help problem on Mac. To work round this problem, configure Kestrel and the gRPC consumer to make use of HTTP/2 with out TLS.
   It's best to solely do that throughout growth. Not utilizing TLS will end in gRPC messages being despatched with out encryption.
   
   https://study.microsoft.com/en-us/aspnet/core/grpc/troubleshoot?view=aspnetcore-7.0
*/
builder.WebHost.ConfigureKestrel(choices =>

    // Setup a HTTP/2 endpoint with out TLS.
    choices.ListenLocalhost(50051, o => o.Protocols =
        HttpProtocols.Http2);
);


// Add providers to the container.
builder.Providers.AddGrpc();
builder.Providers.AddSingleton<ChatRoomService>();

var app = builder.Construct();

// Configure the HTTP request pipeline.
app.MapGrpcService<ChatService>();
app.MapGet("https://dzone.com/", () => "Communication with gRPC endpoints have to be made by a gRPC consumer. To learn to create a consumer, go to: https://go.microsoft.com/fwlink/?linkid=2086909");

Console.WriteLine($"gRPC server about to listening on port:50051");

app.Run();

Observe:  ASP.NET Core gRPC template and samples use TLS by default. However for growth functions, we configure Kestrel and the gRPC consumer to make use of HTTP/2 with out TLS.

Step 5: Create a ChatRoomService and Implement Numerous Strategies Wanted in HandleCommunication

The ChatRoomService class is liable for managing chat rooms and purchasers, in addition to dealing with messages despatched between purchasers. It makes use of a ConcurrentDictionary to retailer chat rooms and a listing of ChatClient objects for every room. The AddClientToChatRoom technique provides a brand new consumer to a chat room, and the BroadcastClientJoinedRoomMessage technique sends a message to all purchasers within the room when a brand new consumer joins. The BroadcastMessageToChatRoom technique sends a message to all purchasers in a room apart from the sender of the message. 

The ChatClient class incorporates a StreamWriter object for writing messages to the consumer, in addition to a UserName property for figuring out the consumer.

utilizing System;
utilizing ChatServer;
utilizing Grpc.Core;
utilizing System.Collections.Concurrent;

namespace ChatServer.Providers
{
    public class ChatRoomService
    {
        non-public static readonly ConcurrentDictionary<string, Record<ChatClient>> _chatRooms = new ConcurrentDictionary<string, Record<ChatClient>>();

        /// <abstract>
        /// Learn a single message from the consumer.
        /// </abstract>
        /// <exception cref="ConnectionLostException"></exception>
        /// <exception cref="TimeoutException"></exception>
        public async Activity<ClientMessage> ReadMessageWithTimeoutAsync(IAsyncStreamReader<ClientMessage> requestStream, TimeSpan timeout)
        
            CancellationTokenSource cancellationTokenSource = new();

            cancellationTokenSource.CancelAfter(timeout);

            attempt
            
                bool moveNext = await requestStream.MoveNext(cancellationTokenSource.Token);

                if (moveNext == false)
                
                    throw new Exception("connection dropped exception");
                

                return requestStream.Present;
            
            catch (RpcException ex) when (ex.StatusCode == StatusCode.Cancelled)
            
                throw new TimeoutException();
            
        

        /// <abstract>
        /// <abstract>
        /// </abstract>
        /// <param title="chatRoomId"></param>
        /// <param title="person"></param>
        /// <returns></returns>

        public async Activity AddClientToChatRoom(string chatRoomId, ChatClient chatClient)
        
            if (!_chatRooms.ContainsKey(chatRoomId))
            
                _chatRooms[chatRoomId] = new Record<ChatClient>  chatClient ;
            
            else
            
                var existingUser = _chatRooms[chatRoomId].FirstOrDefault(c => c.UserName == chatClient.UserName);
                if (existingUser != null)
                
                    // A person with the identical person title already exists within the chat room
                    throw new InvalidOperationException("Consumer with the identical title already exists within the chat room");
                
                _chatRooms[chatRoomId].Add(chatClient);
            

            await Activity.CompletedTask;
        
        /// <abstract>
        /// Broad consumer joined the room message.
        /// </abstract>
        /// <param title="userName"></param>
        /// <param title="chatRoomId"></param>
        /// <returns></returns>
        public async Activity BroadcastClientJoinedRoomMessage(string userName, string chatRoomId)
        
            if (_chatRooms.ContainsKey(chatRoomId))
            
                var message = new ServerMessage  UserJoined = new ServerMessageUserJoined  UserName = userName  ;

                var duties = new Record<Activity>();

                foreach (var stream in _chatRooms[chatRoomId])
                
                    if (stream != null && stream != default)
                    
                        duties.Add(stream.StreamWriter.WriteAsync(message));
                    
                

                await Activity.WhenAll(duties);
            
        

        /// <abstract>
        /// </abstract>
        /// <param title="chatRoomId"></param>
        /// <param title="senderName"></param>
        /// <param title="textual content"></param>
        /// <returns></returns>
        public async Activity BroadcastMessageToChatRoom(string chatRoomId, string senderName, string textual content)
        
            if (_chatRooms.ContainsKey(chatRoomId))
            
                var message = new ServerMessage  Chat = new ServerMessageChat  UserName = senderName, Textual content = textual content  ;

                var duties = new Record<Activity>();
                var streamList = _chatRooms[chatRoomId];
                foreach (var stream in _chatRooms[chatRoomId])
                
                    //This senderName will be one thing of distinctive Id for every person.
                    if (stream != null && stream != default && stream.UserName != senderName)
                    
                        duties.Add(stream.StreamWriter.WriteAsync(message));
                    
                

                await Activity.WhenAll(duties);
            
        
    }

    public class ChatClient
    
        public IServerStreamWriter<ServerMessage> StreamWriter  get; set; 
        public string UserName  get; set; 
    
}

Step 6: Lastly, Implement the gRPC HandleCommunication Methodology in Step 3

The HandleCommunication receives a requestStream from the consumer and sends a responseStream again to the consumer. The tactic reads a message from the consumer, extracts the username and chatRoomId, and handles two circumstances: a login case and a chat case. 

  • Within the login case, the tactic checks if the username and chatRoomId are legitimate and sends a response message to the consumer accordingly. If the login is profitable, the consumer is added to the chat room, and a broadcast message is shipped to all purchasers within the chat room. 
  • Within the chat case, the tactic broadcasts the message to all purchasers within the chat room. 
utilizing System;
utilizing ChatServer;
utilizing Grpc.Core;

namespace ChatServer.Providers
{
    public class ChatService : ChatServer.ChatServerBase
    {
        non-public readonly ILogger<ChatService> _logger;
        non-public readonly ChatRoomService _chatRoomService;

        public ChatService(ChatRoomService chatRoomService, ILogger<ChatService> logger)
        
            _chatRoomService = chatRoomService;
            _logger = logger;
        

        public override async Activity HandleCommunication(IAsyncStreamReader<ClientMessage> requestStream, IServerStreamWriter<ServerMessage> responseStream, ServerCallContext context)
        {
            var userName = string.Empty;
            var chatRoomId = string.Empty;
            
            whereas (true)
             {
                //Learn a message from the consumer.
                var clientMessage = await _chatRoomService.ReadMessageWithTimeoutAsync(requestStream, Timeout.InfiniteTimeSpan);

                change (clientMessage.ContentCase)
                
            }
        }
    }
}

Full venture listing:Complete project directory

That’s all for half 1. Within the subsequent half 2, I’ll create a consumer venture with the consumer implementation to finish this chat utility.