Dans cet article, nous allons créer une application de serveur de chat gRPC concurrentiel simple. Nous utiliserons .NET Core, un framework open source, cross-plateforme et modulaire, pour construire notre application de serveur de chat. Nous aborderons les sujets suivants :
- A brief introduction to gRPC
- Configuration de l’environnement gRPC et définition du contrat de service
- Mise en œuvre du service de chat et gestion des requêtes des clients
- Gestion de plusieurs clients de manière concurrentielle à l’aide de la programmation asynchrone
- Diffusion des messages de chat à tous les clients connectés dans la même salle
À la fin de ce tutoriel, vous comprendrez comment utiliser gRPC pour construire un serveur de chat.
Qu’est-ce que gRPC?
gRPC est un acronyme qui signifie Google Remote Procedure Calls. Il a été initialement développé par Google et est maintenant maintenu par la Cloud Native Computing Foundation (CNCF). gRPC vous permet de connecter, d’invoquer, d’opérer et de déboguer des applications distribuées hétérogènes aussi facilement qu’en appelant une fonction locale.
gRPC utilise HTTP/2 pour la transmission, une approche de développement d’API axée sur le contrat, Protocol Buffers (Protobuf) en tant que langage de définition d’interface ainsi que son format de communication de messages sous-jacent. Il peut prendre en charge quatre types d’API (RPC Unary, Server streaming RPC, Client streaming RPC et Bidirectional streaming RPC). Vous pouvez en savoir plus sur gRPC ici.
Premières étapes
Avant de commencer à écrire du code, une installation de .NET Core doit être effectuée, et assurez-vous d’avoir les prérequis suivants en place:
- Visual Studio Code, Visual Studio ou JetBrains Rider IDE
- .NET Core
- gRPC .NET
- Protobuf
Étape 1 : Créer un projet gRPC à partir de Visual Studio ou de la ligne de commande
- Vous pouvez utiliser la commande suivante pour créer un nouveau projet. Si elle réussit, vous devriez le voir créé dans le répertoire spécifié avec le nom ‘ChatServer.’
dotnet new grpc -n ChatServerApp
- Ouvrez le projet avec l’éditeur de votre choix. J’utilise Visual Studio pour Mac.
Étape 2 : Définir les messages Protobuf dans un fichier Proto
Contrat Protobuf:
- Créez un fichier .proto nommé server.proto dans le dossier protos. Le fichier proto est utilisé pour définir la structure du service, y compris les types de messages et les méthodes que le service prend en charge.
syntax = "proto3";
option csharp_namespace = "ChatServerApp.Protos";
package chat;
service ChatServer {
// Flux de communication bidirectionnelle entre le client et le serveur
rpc HandleCommunication(stream ClientMessage) returns (stream ServerMessage);
}
//Messages du client:
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;
}
//Messages du serveur
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
définit le service principal de notre application de chat, qui comprend une seule méthode RPC appeléeHandleCommunication
. La méthode est utilisée pour la communication bidirectionnelle en continu entre le client et le serveur. Elle prend en entrée un flux deClientMessage
et renvoie en sortie un flux deServerMessage
.
service ChatServer {
// Flux de communication bidirectionnel entre le client et le serveur
rpc HandleCommunication(stream ClientMessage) returns (stream ServerMessage);
}
ClientMessageLogin
, qui sera envoyé par le client, a deux champs appelés chat_room_id et user_name. Ce type de message est utilisé pour envoyer des informations de connexion du client au serveur. Le champchat_room_id
spécifie la salle de chat que le client souhaite rejoindre, tandis que le champuser_name
spécifie le nom d’utilisateur que le client souhaite utiliser dans la salle de chat
message ClientMessageLogin {
string chat_room_id = 1;
string user_name = 2;
}
ClientMessageChat
qui sera utilisé pour envoyer des messages de chat du client au serveur. Il contient un seul champtext
.
message ClientMessageChat {
string text = 1;
}
ClientMessage
définit les différents types de messages qu’un client peut envoyer au serveur. Il contient un champ oneof, ce qui signifie qu’un seul des champs peut être défini à la fois. Si vous utilisezoneof
, le code C# généré contiendra une énumération indiquant quels champs ont été définis. Les noms de champ sont «login
» et «chat
» qui correspondent respectivement aux messagesClientMessageLogin
etClientMessageChat
message ClientMessage {
oneof content {
ClientMessageLogin login = 1;
ClientMessageChat chat = 2;
}
}
ServerMessageLoginFailure
définit le message envoyé par le serveur pour indiquer qu’un client n’a pas réussi à se connecter à la salle de chat. Le champ « raison » spécifie la raison de l’échec.
message ServerMessageLoginFailure {
string reason = 1;
}
-
ServerMessageLoginSuccess
définit le message envoyé par le serveur pour indiquer qu’un client a réussi à se connecter à la salle de chat. Il ne contient aucun champ et signale simplement que la connexion a été réussie. Lorsqu’un client envoie un messageClientMessageLogin
, le serveur répondra avec un messageServerMessageLoginSuccess
ou un messageServerMessageLoginFailure
, selon que la connexion a été réussie ou non. Si la connexion a été réussie, le client peut alors commencer à envoyer des messagesClientMessageChat
pour démarrer les messages de chat.
message ServerMessageLoginSuccess {
}
- Message
ServerMessageUserJoined
définit le message envoyé par le serveur au client lorsqu’un nouvel utilisateur rejoint la salle de chat.
message ServerMessageUserJoined {
string user_name = 1;
}
- Message
ServerMessageChat
définit le message envoyé par le serveur pour indiquer qu’un nouveau message de chat a été reçu. Le champtexte
spécifie le contenu du message de chat, et le champnom_utilisateur
spécifie le nom d’utilisateur de l’utilisateur qui a envoyé le message.
message ServerMessageChat {
string text = 1;
string user_name = 2;
}
- Message
ServerMessage
définit les différents types de messages pouvant être envoyés du serveur au client. Il contient un champoneof
nommé content avec plusieurs options. Les noms de champs sont «login_success
, » «login_failure
, » «user_joined
, » et «chat
, » qui correspondent respectivement aux messagesServerMessageLoginSuccess
,ServerMessageLoginFailure
,ServerMessageUserJoined
, etServerMessageChat
.
message ServerMessage {
oneof content {
ServerMessageLoginSuccess login_success = 1;
ServerMessageLoginFailure login_failure = 2;
ServerMessageUserJoined user_joined = 3;
ServerMessageChat chat = 4;
}
}
Étape 3: Ajouter une classe ChatService
Ajoutez une classe ChatService
dérivée de ChatServerBase
(générée à partir du fichier server.proto en utilisant le générateur gRPC protoc). Nous devons ensuite surcharger la méthode HandleCommunication
. L’implémentation de la méthode HandleCommunication
sera responsable du traitement de la communication entre le client et le serveur.
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);
}
}
Étape 4: Configurer gRPC
Dans le fichier program.cs :
using ChatServer.Services;
using Microsoft.AspNetCore.Server.Kestrel.Core;
var builder = WebApplication.CreateBuilder(args);
/*
// Une configuration supplémentaire est requise pour exécuter gRPC avec succès sur macOS.
// Pour des instructions sur la façon de configurer Kestrel et les clients gRPC sur macOS,
// visitez 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 =>
{
// Configurer un point de terminaison HTTP/2 sans TLS.
options.ListenLocalhost(50051, o => o.Protocols =
HttpProtocols.Http2);
});
// Ajouter des services au conteneur.
builder.Services.AddGrpc();
builder.Services.AddSingleton();
var app = builder.Build();
// Configurer le pipeline de demande HTTP.
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();
Note: ASP.NET Core gRPC template et exemples utilisent par défaut TLS. Mais pour des fins de développement, nous configurons Kestrel et le client gRPC pour utiliser HTTP/2 sans TLS.
Étape 5: Créer un ChatRoomService
et mettre en œuvre les différentes méthodes nécessaires dans HandleCommunication
La classe ChatRoomService
est responsable de la gestion des salles de chat et des clients, ainsi que du traitement des messages échangés entre les clients. Elle utilise un ConcurrentDictionary
pour stocker les salles de chat et une liste d’objets ChatClient
pour chaque salle. La méthode AddClientToChatRoom
ajoute un nouveau client à une salle de chat, et la méthode BroadcastClientJoinedRoomMessage
envoie un message à tous les clients de la salle lorsqu’un nouveau client rejoint. La méthode BroadcastMessageToChatRoom
envoie un message à tous les clients d’une salle à l’exception de l’expéditeur du message.
La classe ChatClient
contient un objet StreamWriter
pour écrire des messages au client, ainsi qu’une propriété UserName pour identifier le client.
using System;
using ChatServer;
using Grpc.Core;
using System.Collections.Concurrent;
namespace ChatServer.Services
{
public class ChatRoomService
{
private static readonly ConcurrentDictionary> _chatRooms = new ConcurrentDictionary>();
Lire un message unique provenant du 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)
{
Un utilisateur avec le même nom d'utilisateur existe déjà dans le salon de discussion
throw new InvalidOperationException("User with the same name already exists in the chat room");
}
_chatRooms[chatRoomId].Add(chatClient);
}
await Task.CompletedTask;
}
Diffuser le message "client a rejoint le salon".
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])
{
Ce senderName peut être quelque chose d'identifiant unique pour chaque utilisateur.
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; }
}
}
Étape 6 : Enfin, implémentez la méthode gRPC HandleCommunication
de l’étape 3
La méthode HandleCommunication
reçoit un requestStream
du client et envoie un responseStream
en retour au client. La méthode lit un message du client, extrait le nom d’utilisateur et le chatRoomId
, et gère deux cas : un cas de connexion et un cas de discussion.
- Dans le cas de la connexion, la méthode vérifie si le nom d’utilisateur et
chatRoomId
sont valides et envoie un message de réponse au client en conséquence. Si la connexion est réussie, le client est ajouté à la salle de chat, et un message de diffusion est envoyé à tous les clients de la salle de chat. - Dans le cas de la messagerie, la méthode diffuse le message à tous les clients de la salle de chat.
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)
{
//Lire un message du client.
var clientMessage = await _chatRoomService.ReadMessageWithTimeoutAsync(requestStream, Timeout.InfiniteTimeSpan);
switch (clientMessage.ContentCase)
{
case ClientMessage.ContentOneofCase.Login:
var loginMessage = clientMessage.Login;
//Obtenir le nom d'utilisateur et l'ID de la salle de chat à partir du message client.
chatRoomId = loginMessage.ChatRoomId;
userName = loginMessage.UserName;
if (string.IsNullOrEmpty(userName) || string.IsNullOrEmpty(chatRoomId))
{
//Envoyer un message de connexion échec.
var failureMessage = new ServerMessage
{
LoginFailure = new ServerMessageLoginFailure { Reason = "Invalid username" }
};
await responseStream.WriteAsync(failureMessage);
return;
}
//Envoyer un message de connexion réussie au client
var successMessage = new ServerMessage { LoginSuccess = new ServerMessageLoginSuccess() };
await responseStream.WriteAsync(successMessage);
//Ajouter le client à la salle de chat.
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)
{
//Diffuser le message à la salle
await _chatRoomService.BroadcastMessageToChatRoom(chatRoomId, userName, chatMessage.Text);
}
break;
}
}
}
}
}
Répertoire du projet complet:
C’est tout pour partie 1. Dans la prochaine partie 2, je vais créer un projet client avec la mise en œuvre du client pour compléter cette application de chat.
Source:
https://dzone.com/articles/create-a-concurrent-grpc-chat-messaging-in-net-7