.Net gRPC: Performanslı ve Ölçeklenebilir Servisler Geliştirmek (Örnek Proje)
gRPC Nedir?
gRPC (gRPC Remote Procedure Call), Google tarafından geliştirilen ve açık kaynaklı bir uzaktan prosedür çağrısı (RPC) çerçevesidir. gRPC, istemci ve sunucu uygulamaları arasında hızlı, düşük gecikmeli ve yüksek performanslı iletişim sağlamak amacıyla tasarlanmıştır.
gRPC, çeşitli programlama dilleri arasında kolayca iletişim kurmayı sağlayan, HTTP/2 protokolü üzerine inşa edilmiş bir framework’tür. Mesajların tanımlanması ve seri hale getirişmesi için Protokol Buffers (Protobuf) adı verilen verimli ve taşınabilir bir format kullanır.
gRPC, dört farklı iletişim modeli sunar:
gRPC İletişim Modelleri
Unary: İstemci bir mesaj gönderir ve sunucu bir yanıt alır.
Server Streaming: İstemci, bir mesaj gönderir ve sunucu zaman içinde birden fazla yanıt gönderir.
Client Streaming: İstemci, sunucuya zaman içerisinde birden fazla mesaj gönderir ve sunucu, tüm mesajları aldıktan sonra bir yanıt gönderir.
Bidirectional Streaming: Hem istemci hem de sunucu birbirlerine sürekli olarak mesaj gönderir ve alır.
gRPC, günümüzde mikro hizmet mimarileri, bulut tabanlı uygulamalar ve yüksek performans gerektiren sistemler için popüler bir seçim olmaya devam etmektedir. Özellikle, HTTP/2'nin sunduğu avantajlardan faydalanarak daha hızlı, güvenli ve verimli iletişim sağlar. Netflix, Square, Dropbox gibi büyük firmalar, yüksek performanslı mikro hizmet mimarilerini oluşturmak için gRPC’yi tercih etmişlerdir.
gRPC’yi tanıdığımız bu kısa girişten sonra şimdi .net 8 framework’ü ile örnek bir uygulama geliştirmeye başlayabiliriz.
Öncelikle aşağıdaki resimde olduğu gibi GrpcExample isimli bir solution ve o solution altında Server ve Client isminde iki WebApi projesi oluşturalım:
Bu projede; server uygulamasını gRPC servisimiz için “Sunucu”; Client uygulaması ise gRPC projemiz için “istemci” olacak şekilde ayarlayacağız.
Server uygulumasından başlayalım;
Öncelikle aşağıdaki nuget paketleri projemize ekleyelim.
- Grpc.AspNetCore: Bu paket, ASP.NET Core uygulamalarında gRPC servisini barındırmak için gerekli bileşenleri içerir.
- Google.Protobuf: Protokol Buffers (Protobuf) mesajlarını tanımlamak ve işlemek için gerekli olan pakettir.
- Grpc.Tools: Protokol Buffers dosyalarını (.proto) derlemek için gerekli olan araçları sağlar. Bu paket sadece geliştirme zamanında kullanılır, çalışma zamanında (runtime) gerekli değildir.
Server.csproj dosyamızın içeriği aşağıdaki gibi oldu.
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Google.Protobuf" Version="3.27.1" />
<PackageReference Include="Grpc.AspNetCore" Version="2.63.0" />
<PackageReference Include="Grpc.Tools" Version="2.64.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
Server projemizde “Protos” isimli bir klasör oluşturalım. Bu klasör altına greet.proto isminde bir dosya oluşturacağız. greet.proto dosya içeriği;
syntax = "proto3"; /*Protobuf 3 sürümünü kullanacağımızı beklirttik*/
option csharp_namespace = "Server"; /*Oluşturulacak C# sınıflarımızın Server isimli namespace de yer alacağını belirttik*/
package server; /* Bu satır protobuf dosyasının server adlı bir paket içinde olduğunu belirtir
Bu, kodu organize etmeye ve isim çakışmalarını önlemeye yardımcı olur */
import "google/protobuf/wrappers.proto"; /* google.protobuf.StringValue tipini kullanabilmek için bu dosyayı import ettik */
service Greeter { /* Greeter isimli bir gRPC servis oluşturduk*/
rpc SayHelloUnary(HelloRequest) returns (HelloReply); /* Unary RPC Metot*/
rpc SayHelloServerStream(HelloRequest) returns (stream HelloReply); /* Server Stream RPC Metot*/
rpc SayHelloClientStream(stream HelloRequest) returns (HelloReply); /* Client Stream RPC Metot*/
rpc SayHelloBidirectionalStream(stream HelloRequest) returns (stream HelloReply); /* Bi-directional Stream RPC Metot*/
}
message HelloRequest {
string first_name = 1;
string last_name = 2;
google.protobuf.StringValue mother_name = 3; /*nullable string*/
}
message HelloReply {
string message = 1;
}
Protobuf dosyamız içerisinde her bir satırı açıklamaya çalıştım. Her bir mesaj (message) içeriğimizde dikkat ettiğiniz gibi her bir parametreye (property) alan numarası verdik. Protobuf mesajlarında alan numaralarının sıralı ve benzersiz olması önemlidir.
Ancak; belirli bir sıraya bağlı kalmak zorunda da değiliz. Alan numaralarını aşağıdaki gibi de tanımlayabilirdik.
message HelloRequest {
string first_name = 1;
string last_name = 3;
google.protobuf.StringValue mother_name = 4; /*nullable string*/
}
Bu, Protobuf’un çalışma şeklini etkilemez, çünkü her alan numarası benzersizdir ve veri paketleme ve çözme süreçlerinde doğru şekilde kullanılacaktır.
Alan numaraları arasında yukarıdaki örnekte olduğu gibi boşluklar bırakmanın avantajları ve dezavantajlarına değinelim;
Avantajlar;
- Esneklik: Alan numaralarını 1,3,4 örneğinde olduğu gibi boşluklar vererek tanımlamak, gelecekte eklemeler yapmanıza olanak tanır. Örneğin 2 numaralı alanı sonradan ekleyebilirsiniz.
- Uyumluluk: Alan numaralarının benzersiz ve tutarlı olması, geriye ve ileriye dönük uyumluluğu korumanıza yardımcı olur.
Dezavantajlar;
- Düzensizlik: Alan numaralarının sıralı olmaması, mesajın okunabilirliğini ve yönetilebilirliğini zorlaştırabilir.
- Boş Alanlar: Alan numaralarının atlanması, mesaj yapısında gereksiz boşluklar bırakabilir, ancak bu genellikle bir sorun yaratmaz.
Protobuf mesajlarının alan numaralarına da kısaca değinmiş olduk. Burada avantaj ve dezavantajlara göre değerlendirmek sizlere kalmış olsa da iyi uygulama önerilerine (best practice) uymak daha doğru olabilir.
Şimdi kodumuzu yazmaya devam edelim..
“greet.proto” Protobuf dosyamızı oluşturduktan sonra; hem dosyanın derlenip c# class larının oluşması hem de projemiz canlıya alınırken Protobuf dosyamızın dizinde olmasını garanti etmek için projemizin .csproj dosyasını aşağıdaki hale getirelim.
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Google.Protobuf" Version="3.27.1" />
<PackageReference Include="Grpc.AspNetCore" Version="2.63.0" />
<PackageReference Include="Grpc.Tools" Version="2.64.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Protobuf Include="Protos\greet.proto" ProtoRoot="Protos\" GrpcServices="Server" />
</ItemGroup>
<ItemGroup>
<None Update="Protos\**\*.*">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
Bundan sonrasında; Server projemizi derlediğimiz zaman “Grpc.Tools” paketi bizim adımıza C# class’larımızı oluşturacaktır.
Bizde “Grpc.Tools” paketinin bizim için oluşturduğu Base class’ı kullanmak için “GreetGrpcService.cs” isimli bir class oluşturalım ve “Greeter.GreeterBase” base class’ından kalıtım alalım.
Artık override ederek Protobuf dosyasında oluşturduğumuz “rpc” metotlarını kullanıp içeriğini aşağıdaki gibi ayarlayabiliriz:
using Grpc.Core;
namespace Server.Services;
public class GreetGrpcService : Greeter.GreeterBase // Greeter.GreeterBase sınıfından türeyen GreetGrpcService sınıfı.
{
// Unary gRPC çağrısını gerçekleştiren metot.
public override Task<HelloReply> SayHelloUnary(HelloRequest request, ServerCallContext context)
{
// HelloReply mesajı oluşturur ve döner. İstemciden gelen ad ve soyadı birleştirir ve mesaj olarak döner.
return Task.FromResult(new HelloReply
{
Message = request.FirstName + " " + request.LastName + " from Unary"
});
}
// Server Streaming gRPC çağrısını gerçekleştiren metot.
public override async Task SayHelloServerStream(HelloRequest request, IServerStreamWriter<HelloReply> responseStream, ServerCallContext context)
{
int i = 0; // Döngü sayacını başlatır.
while (!context.CancellationToken.IsCancellationRequested && i < 10) // İptal edilmediği sürece ve sayaç 10'dan küçük olduğu sürece döngü devam eder.
{
// Sunucuya her bir döngüde yanıt mesajı gönderir.
await responseStream.WriteAsync(new HelloReply
{
Message = request.FirstName + " " + request.LastName + " from Server Streaming"
});
i++; // Sayaç artırılır.
}
}
// Client Streaming gRPC çağrısını gerçekleştiren metot.
public override async Task<HelloReply> SayHelloClientStream(IAsyncStreamReader<HelloRequest> requestStream, ServerCallContext context)
{
var names = new List<string>(); // İstemciden gelen ad ve soyadları tutacak bir liste oluşturur.
await foreach (var message in requestStream.ReadAllAsync()) // İstemciden gelen tüm mesajları okur.
{
// Her bir mesajdan adı ve soyadı alıp listeye ekler.
names.Add(message.FirstName + " " + message.LastName);
}
// Tüm adları birleştirir ve yanıt olarak döner.
return new HelloReply
{
Message = string.Join(", ", names) + " from Client Streaming"
};
}
// Bidirectional Streaming gRPC çağrısını gerçekleştiren metot.
public override async Task SayHelloBidirectionalStream(IAsyncStreamReader<HelloRequest> requestStream, IServerStreamWriter<HelloReply> responseStream,
ServerCallContext context)
{
await foreach (var message in requestStream.ReadAllAsync()) // İstemciden gelen tüm mesajları okur.
{
// Her bir mesaj için sunucuya yanıt mesajı gönderir.
await responseStream.WriteAsync(new HelloReply
{
Message = message.FirstName + " " + message.LastName + " from Bidirectional Streaming"
});
}
}
}
Örnek rpc metotlarımızı tamamlamış olduk. Şimdi “Program.cs” dosyası içerisinde yapmamız gereken konfigürasyonları yapalım.
“Program.cs” dosyasımızın son hali aşağıdaki gibi olacaktır.
using System.Net;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Server.Services;
var builder = WebApplication.CreateBuilder(args);
//ConfigurationKestrel
builder.WebHost.ConfigureKestrel((context, options) =>
{
options.Listen(IPAddress.Any, 5858); //http trafiğini için
options.Listen(IPAddress.Any, 5859, listenOptions => //gRPC trafiğini dinleyecek
{
listenOptions.Protocols = HttpProtocols.Http2; //gRPC için HTTP/2 protokolünü kullanacağız
// listenOptions.UseHttps("certificate.pfx", "password"); //https yapılandırması gerekirse..
});
});
// Add services to the container.
builder.Services.AddGrpc();//gRPC hizmetlerini DI (Dependency Injection) konteynerine ekler.
var app = builder.Build();
// Configure the HTTP request pipeline.
app.MapGrpcService<GreetGrpcService>(); //GreetGrpcService adlı gRPC hizmetini HTTP istek hattına (pipeline) ekler. Bu hizmet, gRPC isteklerini karşılar.
app.MapGet("/protos/greet-protobuf", async httpContext => ///greet-protobuf endpointi yardımıyla Protobuf dosyasımızı istemcilere sunacağız.
{ //Bu kısımı generic hale getirip belli bir klasöraltındaki tüm proto dosyalarını sunacak şekilde değiştirebiliriz. Bu kısmı hayal gücünüze bırakıyorum. :)
httpContext.Response.ContentType = "text/plain;charset=utf-8";
using var fs = File.OpenRead("Protos/greet.proto");
using var sr = new StreamReader(fs, System.Text.Encoding.UTF8);
while (!sr.EndOfStream)
{
var protoLine = await sr.ReadLineAsync();
if(protoLine != "/*>>" || protoLine != "<<*/")
{
await httpContext.Response.WriteAsync(protoLine);
}
}
});
app.Run();
gRPC Server Örnek Uygulamamızı bu şekilde tamamlamış olduk.
Şimdi Client uygulamamızın kodlamasıyla devam edelim..
Burada kuracağımız topolojiyi basit halde anlatmak gerekirse her bir gRPC metotumuz için birer RestAPI kodlayacağız. Bu REST API’ler içerisinden de gRPC server’ımıza istek atacağız. Basit bir çizim yaparsak bu örneğimiz aşağıdaki gibi çalışacak:
Şimdi “client” Uygulamamızı kodlamaya başlayabiliriz.
Öncelikle “client” uygulamamız için gerekli nuget paketleri ekleyelim.
- Grpc.Net.Client: Bu paket, gRPC istemcilerini oluşturmak ve yönetmek için kullanılan bir pakettir.
- Grpc.Net.ClientFactory: ASP.NET Core uygulamalarında gRPC istemcilerini oluşturmak ve yapılandırmak için kullanılan bir fabrikadır. Bu paket, Dependency Injection (DI) ile gRPC istemcilerini kaydetmenizi ve yapılandırmanızı sağlar. HttpClientFactory ile benzer bir şekilde çalışır ve istemci yapılandırmalarını merkezi bir yerden yönetmenize olanak tanır.
- Google.Protobuf: Protokol Buffers (Protobuf) mesajlarını tanımlamak ve işlemek için gerekli olan pakettir.
- Grpc.Tools: Protokol Buffers dosyalarını (.proto) derlemek için gerekli olan araçları sağlar. Bu paket sadece geliştirme zamanında kullanılır, çalışma zamanında (runtime) gerekli değildir.
Paketlerimizi yükledikten sonra “Client.csproj” dosyamızın son hali aşağıdaki gibi olacaktır:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Google.Protobuf" Version="3.27.2" />
<PackageReference Include="Grpc.Net.Client" Version="2.63.0" />
<PackageReference Include="Grpc.Net.ClientFactory" Version="2.63.0" />
<PackageReference Include="Grpc.Tools" Version="2.64.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
Şimdi daha önce yayınladığımız protobuf dosyamızı;
Adresinden Client projemize import edelim. Bu işlem için adımlar:
Connected Services (sağ click) ⇒ Manage Connected Services
Ok İşaretine Tıklıyoruz
gRPC seçip Next diyoruz
Ekrandan URL seçtik ve yayında olan Server için Protobuf Adresimizi Yazdık.
Vee success aldık
Visual Studio gRPC nuget paketlerimizinde yardımıyla bize GrpcClient sınıfımızı oluşturdu.
Artık RPC metotlarımızı çağıracağımız endpointlerimizi hazırlayabiliriz. Bu endpointler için .Net Minimal API’lardan faydalanacağımız Apis klasörü altında “GreetMinimalApi.cs” isimli static class ımızı aşağıdaki gibi oluşturalım ve “Grpc.Tools” nuget paketinin oluşturduğu “Greeter.GreeterClient” sınıfımızı bu endpointler altında aşağıdaki gibi kullanalım.
using Grpc.Core;
using Microsoft.AspNetCore.Mvc;
using Server;
namespace Client.Apis;
public static class GreetMinimalApi
{
// Minimal API endpointlerini eklemek için extension metodu.
public static void UseGreetEndPoints(this IEndpointRouteBuilder routes)
{
// Tüm greet endpointlerini gruplayarak organize eder.
var group = routes.MapGroup("greet");
// Unary gRPC çağrısını gerçekleştiren GET endpointi.
group.MapGet("unary", UnaryExample).WithName("unary").WithOpenApi();
// Server Streaming gRPC çağrısını gerçekleştiren GET endpointi.
group.MapGet("serverstream", ServerStreamExample).WithName("serverstream").WithOpenApi();
// Client Streaming gRPC çağrısını gerçekleştiren GET endpointi.
group.MapGet("clientstream", ClientStreamExample).WithName("clientstream").WithOpenApi();
// Bidirectional Streaming gRPC çağrısını gerçekleştiren GET endpointi.
group.MapGet("bidirectionalstream", BidirectionalStreamExample).WithName("bidirectionalstream").WithOpenApi();
}
// Unary gRPC çağrısı örneği. İstemciden adı ve soyadı alır, sunucuya gönderir ve yanıtı döner.
private static async Task<string> UnaryExample([FromServices] Greeter.GreeterClient grpClient, [FromQuery] string name, [FromQuery] string surname)
{
// Sunucuya bir HelloRequest mesajı gönderir ve yanıtı alır.
var grpcResponse = await grpClient.SayHelloUnaryAsync(new HelloRequest { FirstName = name, LastName = surname });
// Yanıt mesajını döner.
return grpcResponse.Message;
}
// Server Streaming gRPC çağrısı örneği. İstemciden adı ve soyadı alır, sunucuya gönderir ve sunucudan birden fazla yanıt alır.
private static async Task<string> ServerStreamExample([FromServices] Greeter.GreeterClient grpClient, [FromQuery] string name, [FromQuery] string surname)
{
// Sunucuya bir HelloRequest mesajı gönderir ve yanıt akışını alır.
var grpcResponse = grpClient.SayHelloServerStream(new HelloRequest { FirstName = name, LastName = surname });
var response = "";
// Yanıt akışından tüm mesajları okur ve birleştirir.
await foreach (var message in grpcResponse.ResponseStream.ReadAllAsync())
{
response += message.Message + "\n";
}
// Tüm mesajları döner.
return response;
}
// Client Streaming gRPC çağrısı örneği. İstemciden adı ve soyadı alır, sunucuya birden fazla mesaj gönderir ve sunucudan yanıt alır.
private static async Task<string> ClientStreamExample([FromServices] Greeter.GreeterClient grpClient, [FromQuery] string name, [FromQuery] string surname)
{
// Sunucuya mesaj akışı başlatır.
var grpcResponse = grpClient.SayHelloClientStream();
// İstemciden gelen mesajları sunucuya gönderir.
await grpcResponse.RequestStream.WriteAsync(new HelloRequest { FirstName = name, LastName = surname });
await grpcResponse.RequestStream.CompleteAsync();
// Sunucudan gelen yanıtı döner.
return (await grpcResponse).Message;
}
// Bidirectional Streaming gRPC çağrısı örneği. İstemciden adı ve soyadı alır, sunucuya ve istemciye sürekli mesaj gönderir ve alır.
private static async Task<string> BidirectionalStreamExample([FromServices] Greeter.GreeterClient grpClient, [FromQuery] string name, [FromQuery] string surname)
{
// Sunucuya iki yönlü mesaj akışı başlatır.
var grpcResponse = grpClient.SayHelloBidirectionalStream();
// İstemciden gelen mesajları sunucuya gönderir.
await grpcResponse.RequestStream.WriteAsync(new HelloRequest { FirstName = name, LastName = surname });
await grpcResponse.RequestStream.CompleteAsync();
var response = "";
// Sunucudan gelen tüm mesajları okur ve birleştirir.
await foreach (var message in grpcResponse.ResponseStream.ReadAllAsync())
{
response += message.Message + "\n";
}
// Tüm mesajları döner.
return response;
}
}
Şimdi “client” projemizin “Program.cs” class’ını oluşturduğumuz endpoint’lerin çalışması, Swagger ve ClientFactory işlemleri için yeniden organize edelim;
var builder = WebApplication.CreateBuilder(args); // Web uygulaması oluşturucu nesnesini oluşturur.
builder.Services.AddEndpointsApiExplorer(); // API uç noktalarını keşfetmek ve belgelenmek için hizmetleri ekler.
builder.Services.AddSwaggerGen(c =>
{
// Swagger/OpenAPI belgelerini yapılandırır.
c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" });
});
builder.Services.AddGrpcClient<Greeter.GreeterClient>(o => o.Address = new Uri("http://localhost:5859"));
// gRPC istemcisini hizmet olarak ekler ve bu istemcinin sunucu adresini yapılandırır.
var app = builder.Build(); // Uygulama nesnesini oluşturur.
app.UseSwagger(); // Swagger orta katmanını etkinleştirir.
app.UseSwaggerUI(); // Swagger kullanıcı arayüzünü etkinleştirir.
app.UseGreetEndPoints(); // GreetMinimalApi sınıfındaki gRPC endpointlerini yapılandırır.
app.Run(); // Uygulamayı çalıştırır.
Ve böylelikle client projemizi de tamamlamış olduk.
Pojeleri çalıştırıp yukarıdaki SwaggerUI ekranında test etmeyi size bırakıyorum 😃
Kodları aşağıdaki adresten indirebilirsiniz;
Bir sonraki yazımda sizlere Duende Identity Server — CIBA (Client Initiated Backchannel Authentication) ‘ı örnek bir projeyle anlatmaya çalışacağım.
Bir sonraki yazımızda görüşmek üzere…
0 Yorum