Bouw een Eenvoudige Chat Server met gRPC in .Net Core

In deze artikel gaan we een eenvoudige gelijktijdige gRPC chat server applicatie maken. We gaan .NET Core, een kruisbesturingssysteem, open source en modulair kader, gebruiken om onze chat server applicatie te bouwen. We behandelen de volgende onderwerpen:

  • A brief introduction to gRPC
  • Het opzetten van de gRPC omgeving en het definiëren van de service contract
  • Het implementeren van de chat service en het afhandelen van client verzoeken
  • Het afhandelen van meerdere clients gelijktijdig met behulp van asynchroon programmeren
  • Het verzenden van chat berichten naar alle verbonden clients in dezelfde kamer

Aan het einde van deze tutorial zult u een begrip hebben van hoe u gRPC kunt gebruiken om een chat server te bouwen.

Wat Is gRPC?

gRPC staat voor Google Remote Procedure Calls. Het werd oorspronkelijk ontwikkeld door Google en wordt nu onderhouden door de Cloud Native Computing Foundation (CNCF). gRPC stelt u in staat om te verbinden, aanroepen, opereren en fouten opsporen in heterogene toepassingen te distribueren zo gemakkelijk als een lokale functieaanroep maken. 

gRPC gebruikt HTTP/2 voor transport, een contract-first aanpak voor API ontwikkeling, Protocol Buffers (Protobuf) als de interface definitiesprong en als onderliggende berichten uitwisselingsformaat. Het kan vier soorten API ondersteunen (Unary RPC, Server streaming RPC, Client streaming RPC en Bidirectionele  streaming RPC). U kunt meer lezen over gRPC hier.

Aan de slag

Voordat we beginnen met coderen, moet .NET Core worden geïnstalleerd, en zorg ervoor dat je de volgende vereisten hebt:

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

Stap 1: Maak een gRPC Project aan vanuit Visual Studio of Command Line

  • Je kunt de volgende opdracht gebruiken om een nieuw project te maken. Als het succesvol is, zou het in de door jou opgegeven map moeten worden gemaakt met de naam ‘ChatServer.’
PowerShell

 

dotnet new grpc -n ChatServerApp

  • Open het project met je gekozen editor. Ik gebruik Visual Studio voor Mac.

Stap 2: Definieer de Protobuf Berichten in een Proto Bestand

Protobuf Contract:

  1. Maak een .proto bestand met de naam server.proto binnen de protos map. Het proto bestand wordt gebruikt om de structuur van de service te definiëren, inclusief de bericht typen en de methoden die de service ondersteunt. 
ProtoBuf

 

syntax = "proto3";

option csharp_namespace = "ChatServerApp.Protos";

package chat;

service ChatServer {
  // Bidirectionele communicatiestroom tussen client en server
  rpc HandleCommunication(stream ClientMessage) returns (stream ServerMessage);

}

//Client Berichten:
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 Berichten
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 definieert de hoofddienst van ons chattoepassingen, dat omvat een enkele RPC-methode genaamd HandleCommunication. De methode wordt gebruikt voor bidirectionele streaming tussen de client en de server. Het neemt een stream van ClientMessage als invoer en retourneert een stream van ServerMessage als uitvoer. 
ProtoBuf

 

service ChatServer {
  // Bidirectionele communicatiestream tussen client en server
  rpc HandleCommunication(stream ClientMessage) returns (stream ServerMessage);

}

  • ClientMessageLogin, die door de client wordt verzonden, heeft twee velden genaamd chat_room_id en user_name. Dit berichtstype wordt gebruikt om aanmeldingsgegevens van de client naar de server te sturen. Het chat_room_id veld specificeert de chatkamer die de client wil toetreden, terwijl het user_name veld het gebruikersnaam specificeert die de client in de chatkamer wil gebruiken
ProtoBuf

 

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


  • ClientMessageChat die wordt gebruikt om chatberichten van de client naar de server te sturen. Het bevat een enkel veld text.
ProtoBuf

 

message ClientMessageChat {
  string text = 1;
}


  • ClientMessage definieert de verschillende soorten berichten die een client kan verzenden naar de server. Het bevat een oneof veld, wat betekent dat slechts één van de velden tegelijk kan worden ingesteld. Als je oneof gebruikt, zal de gegenereerde C#-code een enumeratie bevatten die aangeeft welke velden zijn ingesteld. De veldnamen zijn “login” en “chat” die corresponderen met de ClientMessageLogin en ClientMessageChat berichten respectievelijk.
ProtoBuf

 

message ClientMessage {
  oneof content {
	ClientMessageLogin login = 1;
	ClientMessageChat chat = 2;
  }
}

  • ServerMessageLoginFailure definieert de boodschap die door de server wordt verzonden om aan te geven dat een client niet is ingelogd in de chatroom. Het redenveld specificeert de oorzaak van het mislukken.
ProtoBuf

 

message ServerMessageLoginFailure {
  string reason = 1;
}

  •  ServerMessageLoginSuccess definieert de boodschap die door de server wordt verzonden om aan te geven dat een client succesvol is ingelogd in de chatroom. Het bevat geen velden en geeft simpelweg aan dat de login succesvol was. Wanneer een client een ClientMessageLogin boodschap stuurt, zal de server reageren met ofwel een ServerMessageLoginSuccess boodschap of een ServerMessageLoginFailure boodschap, afhankelijk van of de login succesvol was of niet. Als de login succesvol was, kan de client vervolgens ClientMessageChat berichten sturen om chatberichten te beginnen.
ProtoBuf

 

message ServerMessageLoginSuccess {
}

  • Bericht ServerMessageUserJoined definieert de boodschap die door de server wordt verzonden naar de client wanneer een nieuwe gebruiker de chatroom bezoekt.
ProtoBuf

 

message ServerMessageUserJoined {
  string user_name = 1;
}

  • Bericht ServerMessageChat definieert de boodschap die door de server wordt verzonden om aan te geven dat er een nieuw chatbericht is ontvangen. Het text veld specificeert de inhoud van het chatbericht, en het user_name veld specificeert de gebruikersnaam van de gebruiker die het bericht heeft verzonden.
ProtoBuf

 

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

  • Bericht ServerMessage definieert de verschillende soorten berichten die van de server naar de client kunnen worden verzonden. Het bevat een oneof veld genaamd content met meerdere opties. De veldnamen zijn “login_success,” “login_failure,” “user_joined,” en “chat,” die corresponderen met de ServerMessageLoginSuccess, ServerMessageLoginFailure, ServerMessageUserJoined, en ServerMessageChat berichten, respectievelijk.
ProtoBuf

 

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

Stap 3: Voeg een ChatService Klasse

Toevoegen van een ChatService klasse die is afgeleid van ChatServerBase (gegenereerd uit het server.proto bestand met behulp van de gRPC codegen protoc). We gaan dan de HandleCommunication methode overschrijven. De implementatie van de HandleCommunication methode zal verantwoordelijk zijn voor het afhandelen van de communicatie tussen de client en de 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);
    }
}

Stap 4: Configureer gRPC

In program.cs bestand:

C#

 

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

var builder = WebApplication.CreateBuilder(args);


/*
// Aanvullende configuratie is vereist om gRPC succesvol uit te voeren op macOS.
// Voor instructies over hoe u Kestrel en gRPC-clients op macOS kunt configureren,
// bezoek 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 =>
{
    // Stel een HTTP/2-eindpunt in zonder TLS.
    options.ListenLocalhost(50051, o => o.Protocols =
        HttpProtocols.Http2);
});


// Voeg services toe aan de container.
builder.Services.AddGrpc();
builder.Services.AddSingleton();

var app = builder.Build();

// Configureer de HTTP-aanvraagpijplijn.
app.MapGrpcService();
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();

Opmerking: De ASP.NET Core gRPC-sjabloon en -voorbeelden gebruiken standaard TLS. Maar voor ontwikkelingsdoeleinden configureren we Kestrel en de gRPC-client om HTTP/2 te gebruiken zonder TLS.

Stap 5: Maak een ChatRoomService en implementeer verschillende methoden die nodig zijn in HandleCommunication

De ChatRoomService-klasse is verantwoordelijk voor het beheren van chatkamers en clients, evenals het afhandelen van berichten die tussen clients worden verzonden. Het gebruikt een ConcurrentDictionary om chatkamers op te slaan en een lijst van ChatClient-objecten voor elke kamer. De AddClientToChatRoom-methode voegt een nieuwe client toe aan een chatkamer en de BroadcastClientJoinedRoomMessage-methode stuurt een bericht naar alle clients in de kamer wanneer een nieuwe client toetreedt. De BroadcastMessageToChatRoom-methode stuurt een bericht naar alle clients in een kamer, behalve de afzender van het bericht. 

De ChatClient-klasse bevat een StreamWriter-object voor het schrijven van berichten naar de client, evenals een UserName-eigenschap voor het identificeren van de client.

C#

 

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

namespace ChatServer.Services
{
    public class ChatRoomService
    {
        private static readonly ConcurrentDictionary> _chatRooms = new ConcurrentDictionary>();

        
        Lees een enkel bericht van de client.
        


        public async Task ReadMessageWithTimeoutAsync(IAsyncStreamReader 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();
            }
        }

        






        public async Task AddClientToChatRoom(string chatRoomId, ChatClient chatClient)
        {
            if (!_chatRooms.ContainsKey(chatRoomId))
            {
                _chatRooms[chatRoomId] = new List { chatClient };
            }
            else
            {
                var existingUser = _chatRooms[chatRoomId].FirstOrDefault(c => c.UserName == chatClient.UserName);
                if (existingUser != null)
                {
                    Een gebruiker met dezelfde gebruikersnaam bestaat al in de chatruimte
                    throw new InvalidOperationException("User with the same name already exists in the chat room");
                }
                _chatRooms[chatRoomId].Add(chatClient);
            }

            await Task.CompletedTask;
        }
        
        Broadcast een bericht dat de client zich heeft aangesloten bij de ruimte.
        



        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();

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

                await Task.WhenAll(tasks);
            }
        }

        





        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();
                var streamList = _chatRooms[chatRoomId];
                foreach (var stream in _chatRooms[chatRoomId])
                {
                    Deze senderName kan iets zijn van een unieke ID voor elke gebruiker.
                    if (stream != null && stream != default && stream.UserName != senderName)
                    {
                        tasks.Add(stream.StreamWriter.WriteAsync(message));
                    }
                }

                await Task.WhenAll(tasks);
            }
        }
    }

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

Stap 6: Implementeer tenslotte de gRPC HandleCommunication methode uit Stap 3

De HandleCommunication ontvangt een requestStream van de client en stuurt een responseStream terug naar de client. De methode leest een bericht van de client, haalt de gebruikersnaam en chatRoomId op en behandelt twee gevallen: een aanmeldingsgeval en een chatgeval.

  • In het inlogscenario controleert de methode of de gebruikersnaam en chatRoomId geldig zijn en stuurt daaropvolgend een antwoordbericht naar de client. Als de inlogprocedure succesvol is, wordt de client aan de chatkamer toegevoegd en wordt een broadcastbericht verzonden naar alle clients in de chatkamer. 
  • In het chatscenario wordt het bericht uitgezonden naar alle clients in de chatkamer. 
C#

 

using System;
using ChatServer;
using Grpc.Core;

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

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

        public override async Task HandleCommunication(IAsyncStreamReader requestStream, IServerStreamWriter responseStream, ServerCallContext context)
        {
            var userName = string.Empty;
            var chatRoomId = string.Empty;
            
            while (true)
             {
                //Lees een bericht van de client in.
                var clientMessage = await _chatRoomService.ReadMessageWithTimeoutAsync(requestStream, Timeout.InfiniteTimeSpan);

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

                        var loginMessage = clientMessage.Login;
                        //Haal gebruikersnaam en chatRoom Id op uit clientMessage.
                        chatRoomId = loginMessage.ChatRoomId;
                        userName = loginMessage.UserName;

                        if (string.IsNullOrEmpty(userName) || string.IsNullOrEmpty(chatRoomId))
                        {
                            //Stuur een inlogmislukkingbericht.
                            var failureMessage = new ServerMessage
                            {
                                LoginFailure = new ServerMessageLoginFailure { Reason = "Invalid username" }
                            };

                            await responseStream.WriteAsync(failureMessage);

                            return;
                        }

                        //Stuur een succesvol inlogbericht naar de client
                        var successMessage = new ServerMessage { LoginSuccess = new ServerMessageLoginSuccess() };
                        await responseStream.WriteAsync(successMessage);

                        //Voeg de client toe aan de chatkamer.
                        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)
                        {
                            //Zend het bericht uit naar de kamer
                            await _chatRoomService.BroadcastMessageToChatRoom(chatRoomId, userName, chatMessage.Text);
                        }

                        break;
                }
            }
        }
    }
}

Volledige projectdirectory:

Dat is alles voor deel 1. In het volgende deel 2 zal ik een clientproject maken met de clientimplementatie om deze chatapplicatie te voltooien.

Source:
https://dzone.com/articles/create-a-concurrent-grpc-chat-messaging-in-net-7