gRPC ile Raspberry Pi için bir şeyler kurcalarken tanışmıştım. .NET Core tarafında da destekleniyor olması bir çok kişinin daha tanışmasına vesile oldu sanırım. Nedir, ne değildir bunlara çok ayrıntılı girmeyeceğim; ama özet olması için, Google tarafından geliştirilen, HTTP/2 protokolü üzerinde çalışan bir Remote Procedure Call(RPC) framework’ü diyebilirim.
En önemli artısı yazılım dili bağımsız olması ve yüksek bir performansa sahip olması. protobuf(Protocol Buffers) olarak adlandıralan yapısal verileri serialize(binary olarak) eden bir mekanizması oluğundan performansı ihtiyaçlara göre oldukça iyi. Bir çok yazılım dili desteğinde, C# ve .NET Core da mevcut. .NET Core tarafında gRPC servislerini ASP.NET Core çatısı altında oldukça kolay bir şekilde geliştirebiliyoruz. gRPC servislerinde, belli bir kontrat ile veriler yapısal iletildiği için, .NET Framework yapılarındaki servis kavramlarına denk olarak anılıyor, .NET Core’da. Ancak özellikle belirtmek isterim ki gRPC, WCF ya da *.wsdl yapılarının .NET Core’da ki karşılığı kesinlikle değil.
Gelelim konumuza… “gRPC nedir?”, “.NET Core’daki gRPC nedir?” gibi sorularıdan çok, “Peki neden?” sorusu üzerinden gidip, “.NET Core’da neden gRPC var?”, “Neden kullanalım ki?” diyerek, beğendiğim bir özelliğinden, .NET Core çatısı altında bahsetmek istiyorum…
Streaming…
Remote Procedure Call diye havalı olarak söylediğimiz kavramın, en basit ve çok kullandığımız yalın ifadesi “servis” bildiğiniz gibi. En çok karşımıza çıkan “Request-Response” mesajlaşma modeli gibi, gerçek zamanlı mesajlaşma modeli(a.k.a streaming) de servislerin olmazsa olmazı.
İşte bu streaming ihtiyaçları için, .NET Core’daki gRPC desteği dikkat çeken, güzel bir özellik. Belli bir kontrat yapısı ile verilerin akışını sağlayan servisleri ASP.NET Core tarafında bu şekilde kolaylıkla, genel standartlara uygun bir şekilde geliştirebiliyoruz.
100 soruluk bir sınav yapmış olalım. Sınav kağıdımızı teslim edelim ve soruların cevabını bekleyelim. Sınav kağıdımız tamamen okunana kadar, sorduğumuz soruların cevaplarını öğrenmek pek mümkün olmaz.
Servis çağrımlarında da böyle durumlar olabiliyor; standart bir request-response modelinde, 1000 tane veriyi işleme sokulsun diye bir servise gönderdiğimizde, 1000 tane verinin de tamamen işlenmesi sonrasında sonuçları alabiliyoruz. Servise gönderdiğimizde sonuç olarak, direkt işlenen verileri sırayla gönderse iyi olmaz mıydı…
Bu örneğe benzer bir senaryomuz olsun. 1+2, 5*10, 6/3 şeklinde matematik işlemlerini toplu bir şekilde sorulabileceği bir servisimiz olsun. Servis de bu işlemlerin sonuçlarını yaptıkça dönsün…
Çok konuştuk biraz da kodlar üzerinden gidelim. Öncelikle *.proto dosyamızda servis kontratına bir bakalım.
syntax = "proto3"; option csharp_namespace = "gRPC.Server"; package Quiz; service Maths { rpc AskQuestion (QuestionRequest) returns (stream AnswerReply); } message QuestionRequest { repeated string texts = 1; } message AnswerReply { string question = 1; double answer=2; }
Maths diye bir servisimiz var, AskQuestion() ile bir çağrı(remote procedure call) yapacağız. Bu çağrıyı QuestionRequest yapısı ile içinde tekrarlanan bir yapı ile soruları ileteceğiz. Bu çağrımız AnswerReply yapısı ile bize geri dönecek. Ama bir akış(stream) şeklinde döneceğini belirtiyoruz.
Şimdi servis kontratımız bu şekilde. Servisin, ne yapacağı tanımlı ama nasıl yapılacağı daha yok. Nasıl yapılacağını .NET Core ile ifade etmemiz için, .NET Core projemizdeki bu *.proto dosyasını arka tarafta derliyor. Bunu proje dosyasındaki(*.csproj) içeriğe göre yapıyor.
<ItemGroup> <Protobuf Include="Protos\maths.proto" GrpcServices="Server" /> </ItemGroup>
Bu derleyip oluşturduğu sınıftan(proxy sınıfı) türeterek servisimizin ne yapacağını yazabiliyoruz.
public class QuizService : Maths.MathsBase { private readonly ILogger<QuizService> _logger; public QuizService(ILogger<QuizService> logger) { _logger = logger; } public override async Task AskQuestion(QuestionRequest questions, IServerStreamWriter<AnswerReply> responseStream, ServerCallContext context) { foreach (var question in questions.Texts.ToList()) { try { if (!string.IsNullOrEmpty(question)) { var dt = new DataTable(); var answer = Convert.ToDouble(dt.Compute(question, string.Empty)); await Task.Delay(800); await responseStream.WriteAsync(new AnswerReply { Answer = answer, Question = question }); } } catch (Exception) { await responseStream.WriteAsync(new AnswerReply { Answer = 0, Question = "It seems that something is wrong.!!!" }); } } } }
Servis kontratımızda olan AskQuestion() metodunu override ettiğimizde IServerStreamWriter<T> şeklinde bir parametre ile göreceğiz. Bu servis çağrılarına, bir akış şeklinde dönüş yapabilmemiz için ihtiyacımız olan parametre.
Yukarıdaki kod da, gelen soruları tek tek işleyip;
await responseStream.WriteAsync(new AnswerReply { Answer = answer, Question = question });
responseStream parametresinin WriteAsync() methodu ile dönebiliyoruz. [1+2, 5*10, 6/3] şeklinde yaptığımız bir çağrının cevabı önce 1+2=3 daha sonra 5*10=50, daha sonra da 6/3=2 şeklinde olacaktır.
Direkt gRPC ile alakasız. Ama küçük ip ucu olması adına; DataTable nesnesinin Compute() metodu ile string olarak verdiğimiz matematik işlemleri gerçekleştirebiliyoruz. Hiç ummadığınız yerde fayda sağlar, kenarda dursun. 😃😃
var dt = new DataTable();
var answer = Convert.ToDouble(dt.Compute(“4+5*(10/2)”, string.Empty));
//answer=29
Bu servisi çağıran küçük bir konsol uygulaması(projesi) ile servisleri nasıl çağırıyoruz ve cevapları nasıl alıyoruz buna da bakalım…
Öncelikle bu projemizde de proto dosyamız olmalı. Başta da dediğim gibi kontrat üzerinden iletişim sınırları çizildiği için, istek yapacak uygulamanın da kontratı biliyor olması lazım. (*.wsdl gibi). Burada küçük ama önemli bir hatırlatma yapmak isterim. Bu noktada servisimize istek yapacak bir uygulama(client) geliştirdiğimiz için *.proto dosyamızı projemize eklerken onun Client için olduğunu belirtmemiz gerekiyor ki oluşturulacak sınıflar ona göre olsun.
<ItemGroup> <Protobuf Include="Protos\maths.proto"> <GrpcServices>Client</GrpcServices> </Protobuf> </ItemGroup>
Direkt Visual Studio üzerinden de, *.proto dosyasının özelliklerinden ayarlamak mümkün.
Kontratımızı da ekledikten sonra, az önce yazdığımız ASP.NET Core gRPC servisini çağırabiliriz.
class Program { static void Main(string[] args) { Console.WriteLine("Hello World!"); CancellationTokenSource tokenSource = new CancellationTokenSource(); string address = "https://localhost:5001"; var channel = GrpcChannel.ForAddress(address); var client = new Maths.MathsClient(channel); List<string> questions = new List<string>() { {"22+54"},{"1-1"},{"435-36"},{"6+2*(98*2)"},{"10/2"},{"5-32"},{"4*2"}, {"1-5*(-10+2)"},{"5+1"},{"15-3"},{"6*2/(9-2)"},{"10/2"},{"5-2"},{"4*2"} }; var questionRequest = new QuestionRequest(); questionRequest.Texts.AddRange(questions); using (var call = client.AskQuestion(questionRequest)) { while (call.ResponseStream.MoveNext(tokenSource.Token).Result) { Console.WriteLine($"{call.ResponseStream.Current.Question} = {call.ResponseStream.Current.Answer.ToString()}"); } } } }
AskQuestion() şeklinde sorularımızı gönderip servisimizi çağırıyoruz. Daha sonra dönen ResponseStream.MoveNext() ile tüm servis sonuçlarını dönüş sırasına göre alabiliyoruz. Önce servisi daha sonra da konsol uygulamasını çalıştırdığınızda, tüm taşlar daha net bir şekilde yerine oturacak.
Burada atlatmaya çalıştığım yapılar ve kodlara GitHub’dan ulaşabilirsiniz. Mutlaka göz atın, deneyin derim.
Burada üzerinden geçemedim ama ASP.NET Core gRPC servislerin, istemciler tarafından da “stream” şeklinde veri akışı ile çağırabilecek özellikleri de var. Dağıtık istemcilerin belli yapıda verileri bir akış ile iletilmesi de mümkün. IoT cihazları için çeşitli senaryolar düşünülebilir sanki…
Açıkcası gRPC servislerini .NET Core ortamında geliştirebilmek ve kullanabilmek oldukça güzel. Belli ihtiyaçlar için özellikle avantaj sağlayacak bir yapı.
Ama…
Her çözüm yönteminin büyük “ama”ları da olabiliyor. .NET Core tarafındaki gRPC’nin de büyük sıkıntı oluşturabilecek belli “ama”ları da var. Bunların da farkında olmanın gerekli olduğunu düşünüyorum. Kısaca bunlardan da bahsetmek isterim. Bu uzun yazının en önemli kısmı olabilir belki de…
Grpc.Core vs. grpc-dotnet
gRPC’nin .NET tarafında iki ayrı resmi uygulaması var. Bunlardan biri Grpc.Core, diğeri de grpc-dotnet. Kafaları karıştıralım…
- Grpc.Core: gRPC’nin orijinal C# destekli yapısı. Asıl gRPC, C tabanlı yapısından geliştirilmiş.
- grpc-dotnet: Yeni olarak, baştan yazılmış bir API. .NET Core 3.0’ı temel olarak geliştirilmiş bir API.
Bu ikisi de açık-kaynak bir şekilde geliştirilmekte. Temelinde kodsal anlamda farklar var tabi ki ama kritiklik açısından çok ayrıntılı bakamadım. İlerleyen zamanlarda belki daha net bişeyler karalarım. Ama ASPNET Core tarafındaki gRPC servisler grpc-dotnet tarafına dayanıyor, bunu bilelim…
Azure App Services
Ne yazık ki ASP.NET Core gRPC servisler şu an için Azure App Services ya da IIS tarafında desteklenmiyor. Bununla ilgili gelişmeleri GitHub üzerinden, açık olan “issue” üzerinden takip edebilirsiniz.
ASP.NET Core gRPC servisleri macOS’da çalışmıyor…
Şaka, şaka çalışıyor… Ama biraz kurcalamak lazım. macOS üzerinde geliştirme yapıyorsanız ve ASPNET Core gRPC servisleri geliştirmek istiyorsanız, dikkat etmeniz gereken önemli bir nokta var. Malum ASPNET Core uygulama şablonları default olarak TLS desteği ile geliyor. ASPNET Core gRPC servis şablonları da dolayısıyla. macOS’da, Kestrel tarafında HTTP/2’de TLS ne yazık ki desteklenmiyor. Zaten servisi çalıştırdığınız zaman dannnn diye direkt aşağıdaki gibi bir uyarı ile bunu anlayabiliyoruz.
HTTP/2 over TLS is not supported on macOS due to missing ALPN support.
Bunun için çok tercih edilmese de bir yöntem var. (GitHub linkini paylaştığım, yazıda geçen örnek kodlarda da var) Kestrel’in HTTP/2’yi TLS desteği olmadan kullanmasını, .ConfigureKestrel() metodu ile değiştirebiliriz.
Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { webBuilder.ConfigureKestrel(options => { options.ListenLocalhost(5001, o => o.Protocols = HttpProtocols.Http2); }); } webBuilder.UseStartup<Startup>(); });
Servis tarafında bu değişiklik ile beraber, istemci tarafındaki uygulamada da değişlik gerekecek. Servis de artık TLS desteği olmadığı için, güvenli olmayacak şekilde çağırmamız gerekiyor. Bunun için de istemci tarafında servisi çağırmadan önce aşağıdaki gibi bir değişiklik yapmamız gerekecek.
string address = "https://localhost:5001"; if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); address = "http://localhost:5001"; } var channel = GrpcChannel.ForAddress(address); var client = new Maths.MathsClient(channel);
Geliştirme sırasında olabilecek bir durum olduğu için belki çok da kritik olarak yorumlamak gerekmez ama yine de yazılım geliştirici olarak önemli bir durum. macOS ve Kestrel tarafında umarım kısa süre içerisinde düzelir.
Bunlar gibi başka dikkat etmek gereken “ama”larda var. Direkt benim karşılaştıklarım olduğu için belirtmek istedim. Diğerlerine de buradan ulaşabilirsiniz.
Bir yazının daha sonuna geldik. Bazı arkada kalan özellik ve durumlar ile hayatımıza giren teknolojileri değerlendirmek, ihtiyaçlarımız ile iyi bağdaştırmak gerekli diye düşünüyorum. Umarım bu açıdan faydalı bir yazı olmuştur. Her türlü fikir, düşünce ve sorunuz için ne yapacağınızı biliyorsunuz…