Build a Simple Chat Server With gRPC in .Net Core - DZone (2023)

  1. DZone
  2. Coding
  3. Tools
  4. 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.

Build a Simple Chat Server With gRPC in .Net Core - DZone (1) by

Okosodo Victor

·

May. 19, 23 · Tutorial

Like (4)

Save

Share

3.47K 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?

gRPCis 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.


Build a Simple Chat Server With gRPC in .Net Core - DZone (2)

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 Messagesmessage 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;}
  • ServerMessageLoginSuccessdefinesthe 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 ServerMessageUserJoineddefines 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 oneoffield 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:Build a Simple Chat Server With gRPC in .Net Core - DZone (3)

That is all for part 1. In the next part 2, I will createa 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.

Trending

  • Orchestration Pattern: Managing Distributed Transactions

  • How To Choose the Right DevOps Tool for Your Project

  • A Complete Continuous Testing Tutorial: Comprehensive Guide With Best Practices

  • AWS Multi-Region Resiliency Aurora MySQL Global DB With Headless Clusters

Comments

Top Articles
Latest Posts
Article information

Author: The Hon. Margery Christiansen

Last Updated: 26/06/2023

Views: 6346

Rating: 5 / 5 (70 voted)

Reviews: 85% of readers found this page helpful

Author information

Name: The Hon. Margery Christiansen

Birthday: 2000-07-07

Address: 5050 Breitenberg Knoll, New Robert, MI 45409

Phone: +2556892639372

Job: Investor Mining Engineer

Hobby: Sketching, Cosplaying, Glassblowing, Genealogy, Crocheting, Archery, Skateboarding

Introduction: My name is The Hon. Margery Christiansen, I am a bright, adorable, precious, inexpensive, gorgeous, comfortable, happy person who loves writing and wants to share my knowledge and understanding with you.