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.
Step 2: Outline the Protobuf Messages in a Proto File
Protobuf Contract:
- 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 asHandleCommunication
. The tactic is used for bidirectional streaming between the consumer and the server. It takes a stream ofClientMessage
as enter and returns a stream ofServerMessage
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. Thechat_room_id
discipline specifies the chat room that the consumer desires to hitch, whereas theuser_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 disciplinetextual 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 useoneof
, the generated C# code will comprise an enumeration indicating which fields have been set. The sphere names are “login
” and “chat
“which corresponds to theClientMessageLogin
andClientMessageChat
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 aClientMessageLogin
message, the server will reply with both aServerMessageLoginSuccess
message or aServerMessageLoginFailure
message, relying on whether or not the login was profitable or not. If the login was profitable, the consumer can then begin to shipClientMessageChat
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. Thetextual content
discipline specifies the content material of the chat message, and theuser_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 aoneof
discipline named content material with a number of choices. The sphere names are “login_success
,” “login_failure
,” “user_joined
,” and “chat
,” which correspond to theServerMessageLoginSuccess
,ServerMessageLoginFailure
,ServerMessageUserJoined
, andServerMessageChat
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:
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.