במאמר זה, ניצור יישום שיחת gRPC מקבילית פשוטה. נשתמש ב-.NET Core, רשת חופשי פלטפורמה, פתוח, ומודולרי, לבניית יישום שיחת השרת שלנו. נכסה את הנושאים הבאים:
- A brief introduction to gRPC
- הקמת הסביבה של gRPC והגדרת חוזה השירות
- יישום שירות השיחה וטיפול בבקשות הלקוח
- טיפול במספר רב של לקוחות במקביל באמצעות תכנות אסימכרוני
- שידור הודעות שיחה לכל הלקוחות המחוברים באותו חדר
בסוף הדרכה זו, תהיה הבנה של איך להשתמש בgRPC לבניית שרת שיחה.
מה זה gRPC?
gRPC הוא ראשי תיבות המסמל קריאות פונקציה רחוקות של Google. זה פותח במקור על ידי Google וכיום מתוחם על ידי קרן החינוך הענן העולמי (CNCF). gRPC מאפשר לך להתחבר, לקרוא, לפעול ולסדר יישומים שונים ושונות במשולב כמו קריאת פונקציה מקומית.
gRPC משתמש ב-HTTP/2 להספק, גישה ראשונה לפיתוח API, פרוטוקול בלוקים (Protobuf) כשפת ההגדרה הבינארית כמו גם צורת ההחלפה הבסיסית של הודעות. זה יכול לתמוך בארבעה סוגים של API (Unary RPC, Server Streaming RPC, Client Streaming RPC, ובקו זימון דו כיווני). אפשר לקרוא עוד על gRPC כאן.
התחלה
לפני שנתחיל לכתוב קוד, יש לבצע התקנה של .NET Core ולוודא שיש לך את הדרישות הבסיסיות הבאות:
- Visual Studio Code, Visual Studio, או JetBrains Rider IDE
- .NET Core
- gRPC .NET
- Protobuf
שלב 1: יצירת פרויקט gRPC מן ה-Visual Studio או שורת הפקודה
- אפשר להשתמש בפקודה הבאה ליצירת פרויקט חדש. אם הפעולה הצליחה, עליך לקבל אותו בספרייה שבחרת עם השם 'ChatServer.'
dotnet new grpc -n ChatServerApp
- פתח את הפרויקט בעורך הנבחר שלך. אני משתמש ב-Visual Studio עבור Mac.
שלב 2: הגדרת הודעות Protobuf בקובץ Proto
חוזה Protobuf:
- צור קובץ .proto בשם server.proto בתיקיה protos. הקובץ proto משמש להגדרת מבנה השירות, כולל סוגי הודעות והשיטות שהשירות תומך בהם.
syntax = "proto3";
option csharp_namespace = "ChatServerApp.Protos";
package chat;
service ChatServer {
// זרם תקשורת דו כיווני בין לקוח לשרת
rpc HandleCommunication(stream ClientMessage) returns (stream ServerMessage);
}
// הודעות לקוח:
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;
}
// הודעות שרת
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
מגדיר את השירות העיקרי של יישום הצ'אט שלנו, שכולל שיטה RPC יחידה הנקראתHandleCommunication
. השיטה משמשת לזרם דו-כיווני בין הלקוח והשרת. היא מקבלת זרם שלClientMessage
כקלט ומחזירה זרם שלServerMessage
כפלט.
service ChatServer {
// זרם התקשורת הדו-כיווני בין לקוח לשרת
rpc HandleCommunication(stream ClientMessage) returns (stream ServerMessage);
}
ClientMessageLogin
, שישלח על ידי הלקוח, כולל שני שדות הנקראים chat_room_id ו-user_name. סוג הודעה זה משמש לשליחת מידע רגע כניסה מהלקוח לשרת. השדהchat_room_id
מציין את חדר הצ'אט שהלקוח רוצה להצטרף, והשדהuser_name
מציין את שם המשתמש שהלקוח רוצה להשתמש בחדר הצ'אט
message ClientMessageLogin {
string chat_room_id = 1;
string user_name = 2;
}
ClientMessageChat
שישמש לשליחת הודעות צ'אט מהלקוח לשרת. הוא מכיל שדה יחידtext
.
message ClientMessageChat {
string text = 1;
}
ClientMessage
מגדיר את סוגי ההודעות השונים שלקוח יכול לשלוח לשרת. הוא מכיל שדה oneof, כלומר רק אחד מהשדות יכול להיות מוגדר בכל פעם. אם תשתמש ב-oneof
, הקוד המיוצר ב-C# יכיל ערכה המציין אילו שדות מוגדרים. שמות השדות הם "login
" ו- "chat
" שמתאימים להודעותClientMessageLogin
ו-ClientMessageChat
בהתאמה.
message ClientMessage {
oneof content {
ClientMessageLogin login = 1;
ClientMessageChat chat = 2;
}
}
ServerMessageLoginFailure
מגדיר את ההודעה שנשלחת על ידי השרת לציון כי ללקוח נכשל בהתחברות לחדר השיחה. השדה סיבה מציין את הסיבה לכשלון.
message ServerMessageLoginFailure {
string reason = 1;
}
-
ServerMessageLoginSuccess
מגדיר את ההודעה שנשלחת על ידי השרת לציון כי ללקוח הצליח להתחבר לחדר השיחה. אין לה שדות ופשוט מאותתת כי ההתחברות הייתה מוצלחת. כשלקוח שולח הודעהClientMessageLogin
, השרת יגבה באחת משתי ההודעותServerMessageLoginSuccess
אוServerMessageLoginFailure
, תלוי בהצלחת ההתחברות או לא. אם ההתחברות הייתה מוצלחת, הלקוח יכול להתחיל לשלוח הודעותClientMessageChat
להתחלת הודעות שיחה.
message ServerMessageLoginSuccess {
}
- הודעה
ServerMessageUserJoined
מגדירה את ההודעה שנשלחת על ידי השרת ללקוח כשמשתמש חדש נכנס לחדר השיחה.
message ServerMessageUserJoined {
string user_name = 1;
}
- הודעה
ServerMessageChat
מגדירה את ההודעה שנשלחת על ידי השרת לציון כי קיבלה הודעת שיחה חדשה. השדהtext
מציין את תוכן ההודעת השיחה, והשדהuser_name
מציין את שם המשתמש של המשתמש ששלח את ההודעה.
message ServerMessageChat {
string text = 1;
string user_name = 2;
}
- הודעה
ServerMessage
מגדירה את הסוגים השונים של הודעות שניתן לשלוח מהשרת ללקוח. היא מכילה שדהoneof
בשם content עם מספר אפשרויות. שמות השדות הם "login_success
," "login_failure
," "user_joined
," ו- "chat
," שמתאימים, בהתאמה, להודעותServerMessageLoginSuccess
,ServerMessageLoginFailure
,ServerMessageUserJoined
, ו-ServerMessageChat
.
message ServerMessage {
oneof content {
ServerMessageLoginSuccess login_success = 1;
ServerMessageLoginFailure login_failure = 2;
ServerMessageUserJoined user_joined = 3;
ServerMessageChat chat = 4;
}
}
שלב 3: הוספת מחלקה ChatService
הוסף מחלקה ChatService
המויעה מ- ChatServerBase
(נוצר מקובץ server.proto באמצעות ייצור קוד gRPC protoc). לאחר מכן נוסיף על השיטה HandleCommunication
. היישום של השיטה HandleCommunication
יהיה אחראי לטיפול בתקשורת בין הלקוח לשרת.
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);
}
}
שלב 4: תכונות gRPC
בקובץ program.cs:
using ChatServer.Services;
using Microsoft.AspNetCore.Server.Kestrel.Core;
var builder = WebApplication.CreateBuilder(args);
/*
// נדרש כונן נוסף לריצה בהצלחה של gRPC ב- macOS.
// להוראות כיצד להתאים את Kestrel ולקוחות gRPC ב- macOS,
// גלה באתר 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 =>
{
// הגדרת נקודת HTTP/2 ללא TLS.
options.ListenLocalhost(50051, o => o.Protocols =
HttpProtocols.Http2);
});
// הוספת שירותים למיכל.
builder.Services.AddGrpc();
builder.Services.AddSingleton();
var app = builder.Build();
// הגדרת זרימת הבקשות ה- 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();
הערה: תבנית ודוגמאות gRPC של ASP.NET Core משתמשות ב-TLS כברירת מחדל. אך למטרות פיתוח, אנו מכוונים את Kestrel והלקוח gRPC להשתמש ב-HTTP/2 ללא TLS.
שלב 5: צור שירות ChatRoomService
ויישם שיטות שונות הנחוצות בHandleCommunication
המחלקה ChatRoomService
אחראית לניהול חדרי צ'אט ולקוחות, כמו גם להעברת הודעות בין לקוחות. היא משתמשת ב-ConcurrentDictionary
לאחסון חדרי צ'אט וברשימה של אובייקטים ChatClient
לכל חדר. השיטה AddClientToChatRoom
מוסיפה לקוח חדש לחדר צ'אט, והשיטה BroadcastClientJoinedRoomMessage
שולחת הודעה לכל הלקוחות בחדר כאשר לקוח חדש נכנס. השיטה BroadcastMessageToChatRoom
שולחת הודעה לכל הלקוחות בחדר מלבד שולח ההודעה.
המחלקה ChatClient
מכילה אובייקט StreamWriter
לכתיבת הודעות ללקוח, כמו גם שדה UserName לזיהוי הלקוח.
using System;
using ChatServer;
using Grpc.Core;
using System.Collections.Concurrent;
namespace ChatServer.Services
{
public class ChatRoomService
{
private static readonly ConcurrentDictionary> _chatRooms = new ConcurrentDictionary>();
///
/// לקרוא מסר יחיד מהלקוח.
///
///
///
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)
{
// קיים משתמש בשם זהה כבר בחדר השיחה
throw new InvalidOperationException("User with the same name already exists in the chat room");
}
_chatRooms[chatRoomId].Add(chatClient);
}
await Task.CompletedTask;
}
///
/// הודעת הצטרפות הלקוח לחדר.
///
///
///
///
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])
{
// השמה זו של senderName יכולה להיות משהו ייחודי לכל משתמש.
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; }
}
}
שלב 6: לבסוף, ליישם את שיטת gRPC HandleCommunication
בשלב 3
שיטת HandleCommunication
מקבלת requestStream
מהלקוח ושולחת responseStream
חזרה ללקוח. השיטה קוראת מסר מהלקוח, מוצאת את שמן המשתמש ואת chatRoomId
, ומטפלת בשני מקרים: מקרה ההתחברות ומקרה השיחה.
- במקרה ההתחברות, השיטה בודקת אם השם משתמש והמספר
chatRoomId
תקינים ושולחת הודעת תגובה ללקוח בהתאם. אם ההתחברות הצליחה, הלקוח מוסף לחדר הצ'אט, והודעה מקדימה נשלחת לכל הלקוחות בחדר הצ'אט. - במקרה הצ'אט, השיטה משדרת את ההודעה לכל הלקוחות בחדר הצ'אט.
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)
{
//קרא הודעה מהלקוח.
var clientMessage = await _chatRoomService.ReadMessageWithTimeoutAsync(requestStream, Timeout.InfiniteTimeSpan);
switch (clientMessage.ContentCase)
{
case ClientMessage.ContentOneofCase.Login:
var loginMessage = clientMessage.Login;
//קבל את שם המשתמש ומספר חדר הצ'אט מההודעה של הלקוח.
chatRoomId = loginMessage.ChatRoomId;
userName = loginMessage.UserName;
if (string.IsNullOrEmpty(userName) || string.IsNullOrEmpty(chatRoomId))
{
//שלח הודעת כשלון התחברות.
var failureMessage = new ServerMessage
{
LoginFailure = new ServerMessageLoginFailure { Reason = "Invalid username" }
};
await responseStream.WriteAsync(failureMessage);
return;
}
//שלח הודעת הצלחה בהתחברות ללקוח
var successMessage = new ServerMessage { LoginSuccess = new ServerMessageLoginSuccess() };
await responseStream.WriteAsync(successMessage);
//הוסף את הלקוח לחדר הצ'אט.
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)
{
//שדר את ההודעה לחדר
await _chatRoomService.BroadcastMessageToChatRoom(chatRoomId, userName, chatMessage.Text);
}
break;
}
}
}
}
}
ספריית פרויקט מושלמת:
זה הכל לחלק 1. בחלק הבא חלק 2, אני אצור פרויקט לקוח עם היישום ללקוח כדי להשלים את יישום הצ'אט הזה.
Source:
https://dzone.com/articles/create-a-concurrent-grpc-chat-messaging-in-net-7