DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Because the DevOps movement has redefined engineering responsibilities, SREs now have to become stewards of observability strategy.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Related

  • Alexa Skill With .NET Core
  • How to Enhance the Performance of .NET Core Applications for Large Responses
  • Coding Once, Thriving Everywhere: A Deep Dive Into .NET MAUI’s Cross-Platform Magic
  • Penetration Testing: A Comprehensive Guide

Trending

  • Apache Spark 4.0: Transforming Big Data Analytics to the Next Level
  • Introducing Graph Concepts in Java With Eclipse JNoSQL, Part 3: Understanding Janus
  • How GitHub Copilot Helps You Write More Secure Code
  • Security by Design: Building Full-Stack Applications With DevSecOps
  1. DZone
  2. Coding
  3. Tools
  4. Build a Simple Chat Server With gRPC in .Net Core

Build a Simple Chat Server With gRPC in .Net Core

Learn how to build a chat server using gRPC, a modern remote procedure call framework, and its support for streaming data.

By 
Okosodo Victor user avatar
Okosodo Victor
·
May. 19, 23 · Tutorial
Likes (4)
Comment
Save
Tweet
Share
10.0K Views

Join the DZone community and get the full member experience.

Join For Free

In this article, we will create a simple concurrent gRPC chat server application. We will use .NET Core, a cross-platform, open-source, and modular framework, to build our chat server application. We will cover the following topics:

  • A brief introduction to gRPC
  • Setting up the gRPC environment and defining the service contract
  • Implementing the chat service and handling client requests
  • Handling multiple clients concurrently using asynchronous programming
  • Broadcasting chat messages to all connected clients in the same room

By the end of this tutorial, you will have an understanding of how to use gRPC to build a chat server.

What Is gRPC?

gRPC is an acronym that stands for Google Remote Procedure Calls. It was initially developed by Google and is now maintained by the Cloud Native Computing Foundation (CNCF).  gRPC allows you to connect, invoke, operate, and debug distributed heterogeneous applications as easily as making a local function call. 

gRPC uses HTTP/2 for transport,  a contract-first approach to API development, protocol Buffers (Protobuf) as the interface definition language as well as its underlying message interchange format. It can support four types of API (Unary RPC, Server streaming RPC, Client streaming RPC, and Bidirectional  streaming RPC). You can read more about gRPC here.

Getting Started

Before we start to write code, an installation of .NET core needs to be done, and make sure you have the following prerequisites in place:

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

Step 1: Create a gRPC Project From the Visual Studio or Command Line

  • You can use the following command to create a new project. If successful, you should have it created in the directory you specify with the name 'ChatServer.'
PowerShell
 
dotnet new grpc -n ChatServerApp


  • Open the project with your chosen editor. I am using visual studio for Mac.
    Open the project

Step 2: Define the Protobuf Messages in a Proto File

Protobuf Contract:

  1. Create .proto file named server.proto within the protos folder. The proto file is used to define the structure of the service, including the message types and the methods that the service supports. 
ProtoBuf
 
syntax = "proto3";

option csharp_namespace = "ChatServerApp.Protos";

package chat;

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

}

//Client Messages:
message ClientMessage {
  oneof content {
	ClientMessageLogin login = 1;
	ClientMessageChat chat = 2;
  }
}

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


message ClientMessageChat {
  string text = 1;
}

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

message ServerMessageLoginFailure {
  string reason = 1;
}

message ServerMessageLoginSuccess {
}

message ServerMessageUserJoined {
  string user_name = 1;
}

message ServerMessageChat {
  string text = 1;
  string user_name = 2;
}


  • ChatServer defines the main service of our chat application, which includes a single RPC method called HandleCommunication.  The method is used for bidirectional streaming between the client and the server. It takes a stream of ClientMessage as input and returns a stream of ServerMessage as output. 
ProtoBuf
 
service ChatServer {
  // Bidirectional communication stream between client and server
  rpc HandleCommunication(stream ClientMessage) returns (stream ServerMessage);

}


  • ClientMessageLogin, which will be sent by the client, has two fields called chat_room_id and user_name. This message type is used to send login information from the client to the server. The chat_room_id field specifies the chat room that the client wants to join, while the user_name field specifies the username that the client wants to use in the chat room
ProtoBuf
 
message ClientMessageLogin {
  string chat_room_id = 1;
  string user_name = 2;
}


  • ClientMessageChat which will be used to send chat messages from the client to the server. It contains a single field text.
ProtoBuf
 
message ClientMessageChat {
  string text = 1;
}


  • ClientMessage defines the different types of messages that a client can send to the server. It contains a oneof field, which means that only one of the fields can be set at a time. if you use oneof, the generated C# code will contain an enumeration indicating which fields have been set. The field names are "login" and "chat"which corresponds to the ClientMessageLogin and ClientMessageChat messages respectively
ProtoBuf
 
message ClientMessage {
  oneof content {
	ClientMessageLogin login = 1;
	ClientMessageChat chat = 2;
  }
}


  • ServerMessageLoginFailure defines the message sent by the server to indicate that a client failed to log in to the chat room. The reason field specifies the reason for the failure.
ProtoBuf
 
message ServerMessageLoginFailure {
  string reason = 1;
}


  •  ServerMessageLoginSuccess defines the message sent by the server to indicate that a client has successfully logged in to the chat room. It contains no fields and simply signals that the login was successful. When a client sends a ClientMessageLogin message, the server will respond with either a ServerMessageLoginSuccess message or a ServerMessageLoginFailure message, depending on whether the login was successful or not. If the login was successful, the client can then start to send ClientMessageChat messages to start chat messages.
ProtoBuf
 
message ServerMessageLoginSuccess {
}


  • Message ServerMessageUserJoined defines the message sent by the server to the client when a new user joins the chat room.
ProtoBuf
 
message ServerMessageUserJoined {
  string user_name = 1;
}


  • Message ServerMessageChat defines the message sent by the server to indicate that a new chat message has been received. The text field specifies the content of the chat message, and the user_name field specifies the username of the user who sent the message.
ProtoBuf
 
message ServerMessageChat {
  string text = 1;
  string user_name = 2;
}


  • Message ServerMessage defines the different types of messages that can be sent from the server to the client. It contains a oneof field named content with multiple options. The field names are "login_success," "login_failure," "user_joined," and "chat," which correspond to the ServerMessageLoginSuccess, ServerMessageLoginFailure, ServerMessageUserJoined, and ServerMessageChat messages, respectively.
ProtoBuf
 
message ServerMessage {
  oneof content {
	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 is derived from ChatServerBase(generated from the server.proto file using the gRPC codegen protoc). We then override the HandleCommunication method. The implementation of the HandleCommunication method will be responsible for handling the communication between the client and the server.

C#
 
public class ChatService : ChatServerBase
{
    private readonly ILogger<ChatService> _logger;

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

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


Step 4: Configure gRPC

In program.cs file:

C#
 
using ChatServer.Services;
using Microsoft.AspNetCore.Server.Kestrel.Core;

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

   To avoid missing ALPN support issue on Mac. To work around this issue, configure Kestrel and the gRPC client to use HTTP/2 without TLS.
   You should only do this during development. Not using TLS will result in gRPC messages being sent without encryption.
   
   https://learn.microsoft.com/en-us/aspnet/core/grpc/troubleshoot?view=aspnetcore-7.0
*/
builder.WebHost.ConfigureKestrel(options =>
{
    // Setup a HTTP/2 endpoint without TLS.
    options.ListenLocalhost(50051, o => o.Protocols =
        HttpProtocols.Http2);
});


// Add services to the container.
builder.Services.AddGrpc();
builder.Services.AddSingleton<ChatRoomService>();

var app = builder.Build();

// Configure the HTTP request pipeline.
app.MapGrpcService<ChatService>();
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");

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

app.Run();


Note:  ASP.NET Core gRPC template and samples use TLS by default. But for development purposes, we configure Kestrel and the gRPC client to use HTTP/2 without TLS.

Step 5: Create a ChatRoomService and Implement Various Methods Needed in HandleCommunication

The ChatRoomService class is responsible for managing chat rooms and clients, as well as handling messages sent between clients. It uses a ConcurrentDictionary to store chat rooms and a list of ChatClient objects for each room. The AddClientToChatRoom method adds a new client to a chat room, and the BroadcastClientJoinedRoomMessage method sends a message to all clients in the room when a new client joins. The BroadcastMessageToChatRoom method sends a message to all clients in a room except for the sender of the message. 

The ChatClient class contains a StreamWriter object for writing messages to the client, as well as a UserName property for identifying the client.

C#
 
using System;
using ChatServer;
using Grpc.Core;
using System.Collections.Concurrent;

namespace ChatServer.Services
{
    public class ChatRoomService
    {
        private static readonly ConcurrentDictionary<string, List<ChatClient>> _chatRooms = new ConcurrentDictionary<string, List<ChatClient>>();

        /// <summary>
        /// Read a single message from the client.
        /// </summary>
        /// <exception cref="ConnectionLostException"></exception>
        /// <exception cref="TimeoutException"></exception>
        public async Task<ClientMessage> ReadMessageWithTimeoutAsync(IAsyncStreamReader<ClientMessage> requestStream, TimeSpan timeout)
        {
            CancellationTokenSource cancellationTokenSource = new();

            cancellationTokenSource.CancelAfter(timeout);

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

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

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

        /// <summary>
        /// <summary>
        /// </summary>
        /// <param name="chatRoomId"></param>
        /// <param name="user"></param>
        /// <returns></returns>

        public async Task AddClientToChatRoom(string chatRoomId, ChatClient chatClient)
        {
            if (!_chatRooms.ContainsKey(chatRoomId))
            {
                _chatRooms[chatRoomId] = new List<ChatClient> { chatClient };
            }
            else
            {
                var existingUser = _chatRooms[chatRoomId].FirstOrDefault(c => c.UserName == chatClient.UserName);
                if (existingUser != null)
                {
                    // A user with the same user name already exists in the chat room
                    throw new InvalidOperationException("User with the same name already exists in the chat room");
                }
                _chatRooms[chatRoomId].Add(chatClient);
            }

            await Task.CompletedTask;
        }
        /// <summary>
        /// Broad client joined the room message.
        /// </summary>
        /// <param name="userName"></param>
        /// <param name="chatRoomId"></param>
        /// <returns></returns>
        public async Task BroadcastClientJoinedRoomMessage(string userName, string chatRoomId)
        {
            if (_chatRooms.ContainsKey(chatRoomId))
            {
                var message = new ServerMessage { UserJoined = new ServerMessageUserJoined { UserName = userName } };

                var tasks = new List<Task>();

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

                await Task.WhenAll(tasks);
            }
        }

        /// <summary>
        /// </summary>
        /// <param name="chatRoomId"></param>
        /// <param name="senderName"></param>
        /// <param name="text"></param>
        /// <returns></returns>
        public async Task BroadcastMessageToChatRoom(string chatRoomId, string senderName, string text)
        {
            if (_chatRooms.ContainsKey(chatRoomId))
            {
                var message = new ServerMessage { Chat = new ServerMessageChat { UserName = senderName, Text = text } };

                var tasks = new List<Task>();
                var streamList = _chatRooms[chatRoomId];
                foreach (var stream in _chatRooms[chatRoomId])
                {
                    //This senderName can be something of unique Id for each user.
                    if (stream != null && stream != default && stream.UserName != senderName)
                    {
                        tasks.Add(stream.StreamWriter.WriteAsync(message));
                    }
                }

                await Task.WhenAll(tasks);
            }
        }
    }

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


Step 6: Finally, Implement the gRPC HandleCommunication Method in Step 3

The HandleCommunication receives a requestStream from the client and sends a responseStream back to the client. The method reads a message from the client, extracts the username and chatRoomId, and handles two cases: a login case and a chat case. 

  • In the login case, the method checks if the username and chatRoomId are valid and sends a response message to the client accordingly. If the login is successful, the client is added to the chat room, and a broadcast message is sent to all clients in the chat room. 
  • In the chat case, the method broadcasts the message to all clients in the chat room. 
C#
 
using System;
using ChatServer;
using Grpc.Core;

namespace ChatServer.Services
{
    public class ChatService : ChatServer.ChatServerBase
    {
        private readonly ILogger<ChatService> _logger;
        private readonly ChatRoomService _chatRoomService;

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

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

                switch (clientMessage.ContentCase)
                {
                    case ClientMessage.ContentOneofCase.Login:

                        var loginMessage = clientMessage.Login;
                        //get username and chatRoom Id from clientMessage.
                        chatRoomId = loginMessage.ChatRoomId;
                        userName = loginMessage.UserName;

                        if (string.IsNullOrEmpty(userName) || string.IsNullOrEmpty(chatRoomId))
                        {
                            //Send a login Failure message.
                            var failureMessage = new ServerMessage
                            {
                                LoginFailure = new ServerMessageLoginFailure { Reason = "Invalid username" }
                            };

                            await responseStream.WriteAsync(failureMessage);

                            return;
                        }

                        //Send login succes message to client
                        var successMessage = new ServerMessage { LoginSuccess = new ServerMessageLoginSuccess() };
                        await responseStream.WriteAsync(successMessage);

                        //Add client to chat room.
                        await _chatRoomService.AddClientToChatRoom(chatRoomId, new ChatClient
                        {
                            StreamWriter = responseStream,
                            UserName = userName
                        });

                        break;

                    case ClientMessage.ContentOneofCase.Chat:

                        var chatMessage = clientMessage.Chat;

                        if (userName is not null && chatRoomId is not null)
                        {
                            //broad cast the message to the room
                            await _chatRoomService.BroadcastMessageToChatRoom(chatRoomId, userName, chatMessage.Text);
                        }

                        break;
                }
            }
        }
    }
}


Complete project directory:Complete project directory

That is all for part 1. In the next part 2, I will create a client project with the client implementation to complete this chat application.

gRPC application ASP.NET Core Visual Studio Code Web application .NET

Opinions expressed by DZone contributors are their own.

Related

  • Alexa Skill With .NET Core
  • How to Enhance the Performance of .NET Core Applications for Large Responses
  • Coding Once, Thriving Everywhere: A Deep Dive Into .NET MAUI’s Cross-Platform Magic
  • Penetration Testing: A Comprehensive Guide

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!