In diesem Artikel werden wir eine einfache parallele gRPC-Chat-Server-Anwendung erstellen. Wir werden .NET Core, eine plattformübergreifende, quelloffene und modulare Framework, verwenden, um unsere Chat-Server-Anwendung zu erstellen. Wir werden die folgenden Themen behandeln:
- A brief introduction to gRPC
- Einrichten der gRPC-Umgebung und Definieren des Dienstvertrags
- Implementieren des Chat-Dienstes und Behandeln von Clientanfragen
- Behandeln mehrerer Clients parallel mithilfe asynchroner Programmierung
- Übertragen von Chatnachrichten an alle verbundenen Clients im gleichen Raum
Am Ende dieses Tutorials werden Sie ein Verständnis dafür haben, wie man gRPC verwendet, um einen Chat-Server zu erstellen.
Was ist gRPC?
gRPC ist ein Akronym, das für Google Remote Procedure Calls steht. Es wurde ursprünglich von Google entwickelt und wird nun von der Cloud Native Computing Foundation (CNCF) gepflegt. gRPC ermöglicht es Ihnen, verteilte, heterogene Anwendungen zu verbinden, aufzurufen, zu betreiben und zu debuggen, so einfach wie einen lokalen Funktionsaufruf zu machen.
gRPC verwendet HTTP/2 für den Transport, einen vertragsspezifisch vorrangigen Ansatz für die API-Entwicklung, Protocol Buffers (Protobuf) als Schnittstellendefinitionssprache sowie als zugrunde liegende Nachrichtenaustauschformat. Es kann vier Arten von API unterstützen (Unary RPC, Server Streaming RPC, Client Streaming RPC und Bidirektional Streaming RPC). Weitere Informationen zu gRPC finden Sie hier.
Erste Schritte
Bevor wir mit dem Schreiben von Code beginnen, muss eine Installation von .NET Core durchgeführt werden, und stellen Sie sicher, dass die folgenden Voraussetzungen erfüllt sind:
- Visual Studio Code, Visual Studio oder JetBrains Rider IDE
- .NET Core
- gRPC .NET
- Protobuf
Schritt 1: Erstellen eines gRPC-Projekts aus Visual Studio oder der Kommandozeile
- Sie können den folgenden Befehl verwenden, um ein neues Projekt zu erstellen. Wenn erfolgreich, sollte es im von Ihnen angegebenen Verzeichnis mit dem Namen ‚ChatServer‚ erstellt worden sein.
dotnet new grpc -n ChatServerApp
- Öffnen Sie das Projekt mit Ihrem ausgewählten Editor. Ich verwende Visual Studio für Mac.
Schritt 2: Definieren der Protobuf-Nachrichten in einer Proto-Datei
Protobuf-Vertrag:
- Erstellen Sie eine .proto-Datei namens server.proto im protos-Ordner. Die Proto-Datei wird verwendet, um die Struktur des Dienstes zu definieren, einschließlich der Nachrichtentypen und der vom Dienst unterstützten Methoden.
syntax = "proto3";
option csharp_namespace = "ChatServerApp.Protos";
package chat;
service ChatServer {
// Bidirektionale Kommunikationsströme zwischen Client und Server
rpc HandleCommunication(stream ClientMessage) returns (stream ServerMessage);
}
//Client-Nachrichten:
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-Nachrichten
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
definiert den Hauptservice unserer Chat-Anwendung, der eine einzige RPC-Methode namensHandleCommunication
umfasst. Diese Methode dient zur bidirektionalen Streaming-Kommunikation zwischen dem Client und dem Server. Sie nimmt einen Stream vonClientMessage
als Eingabe und gibt einen Stream vonServerMessage
als Ausgabe zurück.
service ChatServer {
// Bidirektionale Kommunikationsstream zwischen Client und Server
rpc HandleCommunication(stream ClientMessage) returns (stream ServerMessage);
}
ClientMessageLogin
, welcher vom Client gesendet wird, hat zwei Felder namens chat_room_id und user_name. Diese Nachrichtstyp wird verwendet, um Anmeldeinformationen vom Client zum Server zu senden. Daschat_room_id
Feld gibt den Chat-Raum an, den der Client beitreten möchte, während dasuser_name
Feld den Benutzernamen angibt, den der Client im Chat-Raum verwenden möchte
message ClientMessageLogin {
string chat_room_id = 1;
string user_name = 2;
}
ClientMessageChat
, welcher verwendet wird, um Chat-Nachrichten vom Client zum Server zu senden. Es enthält ein einzelnes Feldtext
.
message ClientMessageChat {
string text = 1;
}
ClientMessage
definiert die verschiedenen Arten von Nachrichten, die ein Client an den Server senden kann. Es enthält ein oneof Feld, was bedeutet, dass jeweils nur eines der Felder gesetzt werden kann. Wenn Sieoneof
verwenden, wird der generierte C#-Code eine Enumeration enthalten, die angibt, welche Felder gesetzt wurden. Die Feldnamen sind „login
“ und „chat
„, was denClientMessageLogin
undClientMessageChat
Nachrichten entspricht.
message ClientMessage {
oneof content {
ClientMessageLogin login = 1;
ClientMessageChat chat = 2;
}
}
ServerMessageLoginFailure
definiert die Nachricht, die der Server anzeigt, um zu signalisieren, dass ein Client beim Versuch, sich in den Chatraum einzuloggen, gescheitert ist. Der Grundfeld gibt den Grund für das Scheitern an.
message ServerMessageLoginFailure {
string reason = 1;
}
-
ServerMessageLoginSuccess
definiert die Nachricht, die der Server sendet, um anzuzeigen, dass ein Client erfolgreich in den Chatraum eingeloggt wurde. Sie enthält keine Felder und signalisiert lediglich, dass die Anmeldung erfolgreich war. Wenn ein Client eineClientMessageLogin
Nachricht sendet, wird der Server mit einerServerMessageLoginSuccess
Nachricht oder einerServerMessageLoginFailure
Nachricht antworten, abhängig davon, ob die Anmeldung erfolgreich war oder nicht. Wenn die Anmeldung erfolgreich war, kann der Client dann mit dem Senden vonClientMessageChat
Nachrichten beginnen, um Chatnachrichten zu starten.
message ServerMessageLoginSuccess {
}
- Nachricht
ServerMessageUserJoined
definiert die Nachricht, die der Server an den Client sendet, wenn ein neuer Benutzer dem Chatraum beitritt.
message ServerMessageUserJoined {
string user_name = 1;
}
- Nachricht
ServerMessageChat
definiert die Nachricht, die der Server sendet, um anzuzeigen, dass eine neue Chatnachricht empfangen wurde. Dastext
Feld gibt den Inhalt der Chatnachricht an, und dasuser_name
Feld gibt den Benutzernamen des Benutzers an, der die Nachricht gesendet hat.
message ServerMessageChat {
string text = 1;
string user_name = 2;
}
- Nachricht
ServerMessage
definiert die verschiedenen Arten von Nachrichten, die vom Server an den Client gesendet werden können. Sie enthält einoneof
Feld namens content mit mehreren Optionen. Die Feldnamen sind „login_success
,“ „login_failure
,“ „user_joined
,“ und „chat
,“ die denServerMessageLoginSuccess
,ServerMessageLoginFailure
,ServerMessageUserJoined
, undServerMessageChat
Nachrichten entsprechen, jeweils.
message ServerMessage {
oneof content {
ServerMessageLoginSuccess login_success = 1;
ServerMessageLoginFailure login_failure = 2;
ServerMessageUserJoined user_joined = 3;
ServerMessageChat chat = 4;
}
}
Schritt 3: Fügen Sie eine ChatService
Klasse hinzu
Fügen Sie eine ChatService
Klasse hinzu, die von ChatServerBase
(generiert aus der server.proto Datei unter Verwendung des gRPC Codegen protoc) abgeleitet ist. Wir überschreiben dann die HandleCommunication
Methode. Die Implementierung der HandleCommunication
Methode ist dafür verantwortlich, die Kommunikation zwischen dem Client und dem Server zu behandeln.
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);
}
}
Schritt 4: Konfigurieren von gRPC
In der Datei program.cs:
using ChatServer.Services;
using Microsoft.AspNetCore.Server.Kestrel.Core;
var builder = WebApplication.CreateBuilder(args);
/*
// Zusätzliche Konfiguration ist erforderlich, um gRPC erfolgreich unter macOS auszuführen.
// Für Anweisungen zur Konfiguration von Kestrel und gRPC-Clients unter macOS,
// besuchen Sie 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 =>
{
// Stellen Sie eine HTTP/2-Endpunkt ohne TLS ein.
options.ListenLocalhost(50051, o => o.Protocols =
HttpProtocols.Http2);
});
// Fügen Sie Dienste zum Container hinzu.
builder.Services.AddGrpc();
builder.Services.AddSingleton();
var app = builder.Build();
// Konfigurieren Sie den HTTP-Anforderungs-Pipeline.
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();
Hinweis: Das ASP.NET Core gRPC-Vorlage und -Beispiele verwenden standardmäßig TLS. Für Entwicklungszwecke konfigurieren wir jedoch Kestrel und den gRPC-Client, um HTTP/2 ohne TLS zu verwenden.
Schritt 5: Erstellen Sie eine ChatRoomService
und Implementieren Sie verschiedene Methoden, die in HandleCommunication
benötigt werden
Die ChatRoomService
-Klasse ist dafür zuständig, Chat-Räume und Clients zu verwalten sowie Nachrichten zwischen den Clients auszutauschen. Sie verwendet ein ConcurrentDictionary
zur Speicherung von Chat-Räumen und eine Liste von ChatClient
-Objekten für jedes Zimmer. Die AddClientToChatRoom
-Methode fügt einem Chat-Raum einen neuen Client hinzu, und die BroadcastClientJoinedRoomMessage
-Methode sendet eine Nachricht an alle Clients im Raum, wenn ein neuer Client beitritt. Die BroadcastMessageToChatRoom
-Methode sendet eine Nachricht an alle Clients in einem Raum mit Ausnahme des Absenders der Nachricht.
Die ChatClient
-Klasse enthält ein StreamWriter
-Objekt zum Schreiben von Nachrichten an den Client sowie eine UserName-Eigenschaft zur Identifizierung des Clients.
using System;
using ChatServer;
using Grpc.Core;
using System.Collections.Concurrent;
namespace ChatServer.Services
{
public class ChatRoomService
{
private static readonly ConcurrentDictionary> _chatRooms = new ConcurrentDictionary>();
/// Eine einzelne Nachricht vom Client lesen.
///
///
///
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)
{
// Ein Benutzer mit demselben Benutzernamen existiert bereits im Chatraum
throw new InvalidOperationException("User with the same name already exists in the chat room");
}
_chatRooms[chatRoomId].Add(chatClient);
}
await Task.CompletedTask;
}
/// Breite Nachricht, dass ein Client dem Raum beigetreten ist.
///
///
///
///
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])
{
//Dieser senderName kann eine eindeutige ID für jeden Benutzer sein.
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; }
}
}
Schritt 6: Implementieren Sie schließlich den gRPC-HandleCommunication
-Methode aus Schritt 3
Die HandleCommunication
-Methode empfängt einen requestStream
vom Client und sendet einen responseStream
zurück zum Client. Die Methode liest eine Nachricht vom Client, extrahiert den Benutzernamen und die chatRoomId
und behandelt zwei Fälle: einen Anmeldefall und einen Chatfall.
- Im Login-Fall überprüft die Methode, ob der Benutzername und
chatRoomId
gültig sind, und sendet entsprechend eine Antwortnachricht an den Client. Wenn die Anmeldung erfolgreich ist, wird der Client dem Chatraum hinzugefügt und eine Broadcast-Nachricht an alle Clients im Chatraum gesendet. - Im Chat-Fall sendet die Methode die Nachricht an alle Clients im Chatraum.
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)
{
//Eine Nachricht vom Client lesen.
var clientMessage = await _chatRoomService.ReadMessageWithTimeoutAsync(requestStream, Timeout.InfiniteTimeSpan);
switch (clientMessage.ContentCase)
{
case ClientMessage.ContentOneofCase.Login:
var loginMessage = clientMessage.Login;
//Benutzername und ChatRoom-ID aus der ClientNachricht abrufen.
chatRoomId = loginMessage.ChatRoomId;
userName = loginMessage.UserName;
if (string.IsNullOrEmpty(userName) || string.IsNullOrEmpty(chatRoomId))
{
//Eine Anmeldefehler-Nachricht senden.
var failureMessage = new ServerMessage
{
LoginFailure = new ServerMessageLoginFailure { Reason = "Invalid username" }
};
await responseStream.WriteAsync(failureMessage);
return;
}
//Anmelderfolg-Nachricht an den Client senden
var successMessage = new ServerMessage { LoginSuccess = new ServerMessageLoginSuccess() };
await responseStream.WriteAsync(successMessage);
//Client zum Chatraum hinzufügen.
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)
{
//Die Nachricht im Raum verbreiten
await _chatRoomService.BroadcastMessageToChatRoom(chatRoomId, userName, chatMessage.Text);
}
break;
}
}
}
}
}
Kompletter Projektordner:
Das war’s für Teil 1. Im nächsten Teil 2 werde ich ein Clientprojekt mit der Clientimplementierung erstellen, um diese Chat-Anwendung zu vervollständigen.
Source:
https://dzone.com/articles/create-a-concurrent-grpc-chat-messaging-in-net-7