Buat server obrolan sederhana dengan gRPC di .Net Core
Buat server obrolan sederhana dengan gRPC di .Net Core
[ad_1]
Pada artikel ini, kita akan membuat aplikasi server obrolan gRPC konkuren sederhana. Kami akan menggunakan .NET Core, kerangka kerja lintas platform, sumber terbuka, dan modular, untuk membangun aplikasi server obrolan kami. Kami akan membahas topik-topik berikut:
- Pengenalan singkat tentang gRPC.
- Penerapan lingkungan gRPC dan definisi kontrak layanan.
- Menyiapkan layanan obrolan dan memproses permintaan pelanggan.
- Mengelola Banyak Klien Secara Bersamaan Menggunakan Pemrograman Asinkron
- Siarkan pesan obrolan ke semua klien yang terhubung di ruangan yang sama.
Di akhir tutorial ini, Anda akan memahami cara menggunakan gRPC untuk membuat server obrolan.
Apa itu gRPC?
gRPC adalah singkatan dari Panggilan prosedur jarak jauh Google. Awalnya dikembangkan oleh Google dan sekarang dikelola oleh Cloud Native Computing Foundation (CNCF). gRPC memungkinkan Anda menghubungkan, menjalankan, mengoperasikan, dan men-debug aplikasi terdistribusi heterogen semudah panggilan fungsi lokal.
gRPC menggunakan HTTP/2 untuk transportasi, pendekatan kontrak untuk pengembangan API, buffer protokol (protobuf) sebagai bahasa definisi antarmuka, dan format pertukaran pesan yang mendasarinya. Itu dapat mendukung empat jenis API (Unary RPC, Server streaming RPC, Client streaming RPC dan dua arah RPC terus menerus). Anda dapat membaca lebih lanjut tentang gRPC Di Sini.
Mulai:
Sebelum Anda mulai menulis kode, penginstalan inti .NET harus dilakukan dan memastikan bahwa prasyarat berikut tersedia:
- Kode Visual Studio, Visual Studio atau JetBrains Rider IDE.
- .NET Inti.
- gRPC .NET
- Protobuf
Langkah 1: Buat proyek gRPC dari Visual Studio atau baris perintah
- Anda dapat menggunakan perintah berikut untuk membuat proyek baru. Jika berhasil, Anda harus membuatnya di direktori yang Anda tentukan dengan nama ‘Server Obrolan.’
dotnet new grpc -n ChatServerApp
- Buka proyek dengan editor pilihan Anda. Saya menggunakan Visual Studio untuk Mac.
Langkah 2: Tentukan pesan Protobuf dalam file Proto
Kontrak Protobuf:
- Buat file .proto bernama server.proto dalam folder prototipe. File proto digunakan untuk menentukan struktur layanan, termasuk jenis pesan dan metode yang didukung oleh layanan.
syntax = "proto3";
option csharp_namespace = "ChatServerApp.Protos";
package chat;
service ChatServer {
// Bidirectional communication stream between client and server
rpc HandleCommunication(stream ClientMessage) returns (stream ServerMessage);
}
//Client Messages:
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 Messages
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
mendefinisikan layanan utama aplikasi obrolan kami, yang mencakup metode RPC tunggal yang disebutHandleCommunication
. Metode ini digunakan untuk streaming dua arah antara klien dan server. Dibutuhkan aliranClientMessage
sebagai input dan mengembalikan aliranServerMessage
sebagai keluaran.
service ChatServer {
// Bidirectional communication stream between client and server
rpc HandleCommunication(stream ClientMessage) returns (stream ServerMessage);
}
ClientMessageLogin
, yang akan dikirimkan oleh customer, memiliki dua field bernama chat_room_id dan user_name. Jenis pesan ini digunakan untuk mengirim informasi koneksi dari klien ke server. ITUchat_room_id
bidang menentukan ruang obrolan yang ingin diikuti oleh pelanggan, sedangkanuser_name
bidang menentukan nama pengguna yang ingin digunakan pelanggan di ruang obrolan
message ClientMessageLogin {
string chat_room_id = 1;
string user_name = 2;
}
ClientMessageChat
yang akan digunakan untuk mengirimkan pesan chat dari client ke server. Ini berisi satu bidangtext
.
message ClientMessageChat {
string text = 1;
}
ClientMessage
mendefinisikan berbagai jenis pesan yang klien dapat mengirim ke server. Ini berisi a satu dari field, yang berarti hanya satu field yang dapat diatur pada satu waktu. jika Anda menggunakanoneof
, kode C# yang dihasilkan akan berisi pencacahan yang menunjukkan kolom mana yang telah ditentukan. Nama bidangnya adalah “login
” Dan “chat
” yang sesuai denganClientMessageLogin
DanClientMessageChat
pesan masing-masing
message ClientMessage {
oneof content {
ClientMessageLogin login = 1;
ClientMessageChat chat = 2;
}
}
ServerMessageLoginFailure
mendefinisikan pesan yang dikirim oleh server untuk menunjukkan bahwa klien gagal terhubung ke ruang obrolan. Bidang alasan menentukan alasan kegagalan.
message ServerMessageLoginFailure {
string reason = 1;
}
-
ServerMessageLoginSuccess
mendefinisikan pesan yang dikirim oleh server untuk menunjukkan bahwa klien telah berhasil terhubung ke ruang obrolan. Itu tidak berisi bidang dan hanya menunjukkan bahwa koneksi berhasil. Ketika pelanggan mengirim aClientMessageLogin
pesan, server akan merespon baik dengan aServerMessageLoginSuccess
pesan atau aServerMessageLoginFailure
pesan, tergantung pada apakah koneksi berhasil atau tidak. Jika koneksi berhasil, klien kemudian dapat mulai mengirimClientMessageChat
pesan untuk memulai pesan obrolan.
message ServerMessageLoginSuccess {
}
- Pesan
ServerMessageUserJoined
mendefinisikan pesan yang dikirim oleh server ke klien saat pengguna baru bergabung dengan ruang obrolan.
message ServerMessageUserJoined {
string user_name = 1;
}
- Pesan
ServerMessageChat
menentukan pesan yang dikirim oleh server untuk menunjukkan bahwa pesan obrolan baru telah diterima. ITUtext
bidang menentukan konten pesan obrolan, danuser_name
bidang menentukan nama pengguna dari pengguna yang mengirim pesan.
message ServerMessageChat {
string text = 1;
string user_name = 2;
}
- Pesan
ServerMessage
mendefinisikan berbagai jenis pesan yang dapat dikirim dari server ke klien. Ini berisi aoneof
bidang bernama konten dengan beberapa opsi. Nama bidangnya adalah “login_success
,” “login_failure
,” “user_joined
,” Dan “chat
“, yang sesuai denganServerMessageLoginSuccess
,ServerMessageLoginFailure
,ServerMessageUserJoined
DanServerMessageChat
posting, masing-masing.
message ServerMessage {
oneof content {
ServerMessageLoginSuccess login_success = 1;
ServerMessageLoginFailure login_failure = 2;
ServerMessageUserJoined user_joined = 3;
ServerMessageChat chat = 4;
}
}
Langkah 3: Tambahkan a ChatService
Kelas
Tambahkan satu ChatService
kelas berasal dari ChatServerBase
(dihasilkan dari server.proto file menggunakan codegen gRPC protokol). Kami kemudian mengganti HandleCommunication
metode. Implementasi dari HandleCommunication
akan bertanggung jawab untuk mengelola komunikasi antara klien dan server.
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);
}
}
Langkah 4: Konfigurasikan gRPC
Dalam file program.cs:
using ChatServer.Services;
using Microsoft.AspNetCore.Server.Kestrel.Core;
var builder = WebApplication.CreateBuilder(args);
/*
// Additional configuration is required to successfully run gRPC on macOS.
// For instructions on how to configure Kestrel and gRPC clients on macOS,
// visit
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.
*/
builder.WebHost.ConfigureKestrel(options =>
{
// Setup a HTTP/2 endpoint without TLS.
options.ListenLocalhost(50051, o => o.Protocols =
HttpProtocols.Http2);
});
// Add services to the container.
builder.Services.AddGrpc();
builder.Services.AddSingleton<ChatRoomService>();
var app = builder.Build();
// Configure the HTTP request pipeline.
app.MapGrpcService<ChatService>();
app.MapGet(" () => "Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit:
Console.WriteLine($"gRPC server about to listening on port:50051");
app.Run();
Catatan: Template dan sampel gRPC ASP.NET Core menggunakan TLS secara default. Namun untuk tujuan pengembangan, kami mengonfigurasi Kestrel dan klien gRPC untuk menggunakan HTTP/2 tanpa TLS.
Langkah 5: Buat a ChatRoomService
dan menerapkan berbagai metode yang diperlukan dalam HandleCommunication
ITU ChatRoomService
class bertanggung jawab untuk mengelola ruang obrolan dan klien, serta mengelola pesan yang dikirim antar klien. Dia menggunakan a ConcurrentDictionary
untuk menyimpan ruang obrolan dan daftar ChatClient
objek untuk setiap ruangan. ITU AddClientToChatRoom
menambahkan pelanggan baru ke ruang obrolan, dan metodenya BroadcastClientJoinedRoomMessage
Metode mengirimkan pesan ke semua pelanggan di ruangan saat pelanggan baru bergabung. ITU BroadcastMessageToChatRoom
Metode mengirimkan pesan ke semua pelanggan di ruangan kecuali pengirim pesan.
ITU ChatClient
kelas berisi a StreamWriter
objek untuk menulis pesan ke klien, dan properti UserName untuk mengidentifikasi klien.
using System;
using ChatServer;
using Grpc.Core;
using System.Collections.Concurrent;
namespace ChatServer.Services
{
public class ChatRoomService
{
private static readonly ConcurrentDictionary<string, List<ChatClient>> _chatRooms = new ConcurrentDictionary<string, List<ChatClient>>();
/// <summary>
/// Read a single message from the client.
/// </summary>
/// <exception cref="ConnectionLostException"></exception>
/// <exception cref="TimeoutException"></exception>
public async Task<ClientMessage> ReadMessageWithTimeoutAsync(IAsyncStreamReader<ClientMessage> 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();
}
}
/// <summary>
/// <summary>
/// </summary>
/// <param name="chatRoomId"></param>
/// <param name="user"></param>
/// <returns></returns>
public async Task AddClientToChatRoom(string chatRoomId, ChatClient chatClient)
{
if (!_chatRooms.ContainsKey(chatRoomId))
{
_chatRooms[chatRoomId] = new List<ChatClient> { chatClient };
}
else
{
var existingUser = _chatRooms[chatRoomId].FirstOrDefault(c => c.UserName == chatClient.UserName);
if (existingUser != null)
{
// A user with the same user name already exists in the chat room
throw new InvalidOperationException("User with the same name already exists in the chat room");
}
_chatRooms[chatRoomId].Add(chatClient);
}
await Task.CompletedTask;
}
/// <summary>
/// Broad client joined the room message.
/// </summary>
/// <param name="userName"></param>
/// <param name="chatRoomId"></param>
/// <returns></returns>
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<Task>();
foreach (var stream in _chatRooms[chatRoomId])
{
if (stream != null && stream != default)
{
tasks.Add(stream.StreamWriter.WriteAsync(message));
}
}
await Task.WhenAll(tasks);
}
}
/// <summary>
/// </summary>
/// <param name="chatRoomId"></param>
/// <param name="senderName"></param>
/// <param name="text"></param>
/// <returns></returns>
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<Task>();
var streamList = _chatRooms[chatRoomId];
foreach (var stream in _chatRooms[chatRoomId])
{
//This senderName can be something of unique Id for each user.
if (stream != null && stream != default && stream.UserName != senderName)
{
tasks.Add(stream.StreamWriter.WriteAsync(message));
}
}
await Task.WhenAll(tasks);
}
}
}
public class ChatClient
{
public IServerStreamWriter<ServerMessage> StreamWriter { get; set; }
public string UserName { get; set; }
}
}
Langkah 6: Terakhir, implementasikan gRPC HandleCommunication
Metode pada langkah 3
ITU HandleCommunication
menerima a requestStream
pelanggan dan mengirimkan a responseStream
kembali ke pelanggan. Metode membaca pesan dari klien, mengekstrak nama pengguna dan chatRoomId
dan menangani dua kasus: kasus login dan kasus obrolan.
- Dalam kasus login, metode memeriksa apakah nama pengguna dan
chatRoomId
valid dan mengirimkan pesan respons ke klien yang sesuai. Jika koneksi berhasil, pelanggan ditambahkan ke ruang obrolan dan pesan siaran dikirim ke semua pelanggan di ruang obrolan. - Dalam kasus obrolan, metode ini menyiarkan pesan ke semua pelanggan di ruang obrolan.
using System;
using ChatServer;
using Grpc.Core;
namespace ChatServer.Services
{
public class ChatService : ChatServer.ChatServerBase
{
private readonly ILogger<ChatService> _logger;
private readonly ChatRoomService _chatRoomService;
public ChatService(ChatRoomService chatRoomService, ILogger<ChatService> logger)
{
_chatRoomService = chatRoomService;
_logger = logger;
}
public override async Task HandleCommunication(IAsyncStreamReader<ClientMessage> requestStream, IServerStreamWriter<ServerMessage> responseStream, ServerCallContext context)
{
var userName = string.Empty;
var chatRoomId = string.Empty;
while (true)
{
//Read a message from the client.
var clientMessage = await _chatRoomService.ReadMessageWithTimeoutAsync(requestStream, Timeout.InfiniteTimeSpan);
switch (clientMessage.ContentCase)
{
case ClientMessage.ContentOneofCase.Login:
var loginMessage = clientMessage.Login;
//get username and chatRoom Id from clientMessage.
chatRoomId = loginMessage.ChatRoomId;
userName = loginMessage.UserName;
if (string.IsNullOrEmpty(userName) || string.IsNullOrEmpty(chatRoomId))
{
//Send a login Failure message.
var failureMessage = new ServerMessage
{
LoginFailure = new ServerMessageLoginFailure { Reason = "Invalid username" }
};
await responseStream.WriteAsync(failureMessage);
return;
}
//Send login succes message to client
var successMessage = new ServerMessage { LoginSuccess = new ServerMessageLoginSuccess() };
await responseStream.WriteAsync(successMessage);
//Add client to chat room.
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)
{
//broad cast the message to the room
await _chatRoomService.BroadcastMessageToChatRoom(chatRoomId, userName, chatMessage.Text);
}
break;
}
}
}
}
}
Direktori proyek lengkap:
Itu semua untuk Bagian 1. Setelah itu bagian 2Saya akan membuat proyek pelanggan dengan implementasi pelanggan untuk menyelesaikan aplikasi obrolan ini.
[ad_2]