gRPC Nedir? Protocol Buffer, Stream ve Dahası

grpc-nedir

gRPC açık kaynaklı ve uzak prosedür çağrılarıyla istemci ile sunucu arasında iletişim kurulmasını sağlayan bir framework olmakla birlikte sağladığı multi-language desteği, HTTP/2, REST’e göre x8 kat daha hızlı olması, binary serileştirmesi gibi özellikleriyle öne çıkmaktadır. İstemci ile sunucu arasında gerçekleşecek bu iletişim kontratlara dayanmaktadır. Bu kontratlar Protocol Buffer olarak adlandırılmakta ve bu kontratlar .proto uzantısına sahip olmaktadır. Genellikle sınırlı bant genişliği ve hafızaya sahip IoT cihazlarıyla iletişimde ve mikro servislerde servisler arası iletişimde tercih edilirler.

Konuyu pekiştirmek adına ürün yönetim işlemlerini gerçekleştirecek basit bir sistem kuracağız. Buna göre hem ürünleri yayınlayacak bir sunucu servisi ve bu servisi consume edecek bir istemci servise ihtiyacımız olacak.

Server (Sunucu) Projesi

Proje dosyalarını oluşturmaya ProductGrpc isimli ve ASP.NET Core Empty tipindeki servisi oluşturarak başlayalım. Bu proje senaryoya göre istemci değil sunucu rolünde olacaktır.

dotnet new web --name ProductGrpc

appsettings.json dosyasına Kestrel Http/2 konfigürasyonunu ekleyelim.

"Kestrel": {
  "EndpointDefaults": {
    "Protocols": "Http2"
  }
}

Hemen gerekli paketleri kuralım. Proje, sunucu rolünde olacağı için Grpc.AspNetCore isimli paketi kurmamız gerekmektedir.

dotnet add package Grpc.AspNetCore

Paketi kurduktan sonra kontratımızı yazmaya başlayalım. Bunun için projemize Protos isimli bir klasör açarak product.proto isminde bir dosya oluşturalım ve içini aşağıdaki gibi dolduralım.

syntax = "proto3"; // versiyon

option csharp_namespace = "ProductGrpc.Protos"; // opsiyonel namespace

import "google/protobuf/empty.proto"; // well-known type
import "google/protobuf/timestamp.proto"; // well-known type

// servis sözleşmesi
service ProductProtoService {
    rpc GetProduct (GetProductRequest) returns (ProductModel);
    rpc GetProducts (google.protobuf.Empty) returns (stream ProductModel);

    rpc InsertProduct (InsertProductRequest) returns (ProductModel);
    rpc BulkInsertProduct (stream ProductModel) returns (BulkInsertProductResponse);
    rpc UpdateProduct (UpdateProductRequest) returns (ProductModel);
    rpc RemoveProduct (RemoveProductRequest) returns (RemoveProductResponse);
}

// servis rpc'lerinde kullanılacak tip tanımlamaları
message GetProductRequest {
    int32 id = 1;
}

enum ProductStatus {
    None = 0;
    Low = 1;
    InStock = 2;
}

message ProductModel {
    int32 id = 1;
    string name = 2;
    string detail = 3;
    float price = 4;
    ProductStatus status = 5;
    google.protobuf.Timestamp createdOn = 6;
}

message InsertProductRequest {
    ProductModel product = 1;
}

message BulkInsertProductResponse {
    bool success = 1;
    int32 inserted = 2;
}

message UpdateProductRequest {
    ProductModel product = 1;
}

message RemoveProductRequest {
    int32 id = 1;
}

message RemoveProductResponse {
    bool success = 1;
}

Koda baktığınızda ProductProtoService isminde bir arayüz tanımlandığını ve içerisinde 6 adet rpc’nin (Remote Prosedure Call) aldıkları parametre ve geri dönüş tipleriyle birlikte tanımlandığını görebilirsiniz.

Oluşturduğumuz Proto Buffer dosyasını ProductGrpc.csproj dosyamıza ekleyelim ve projemizi derleyelim, böylece ihtiyacımız olan sınıflar generate edilecektir. Burada dikkat edilmesi gereken nokta servisin istemci olarak mı sunucu olarak mı kullanılacağının GrpcServices attribute’ü içerisinde bildirilmesidir.

<ItemGroup>
   <Protobuf Include="Protos/product.proto" GrpcServices="Server" />
</ItemGroup>

Proje derlendikten sonra obj/net5.0/Protos dizininde oluşturulan gRPC servisine göz atabilirsiniz. Bu servis oluşturulurken hazırladığımız product.proto dosyasından beslenmiştir.

Şimdi kullanacağımız model ve mock-datalarımızı oluşturalım. Bunun için Models klasörü altına Product.cs ve Data klasörü altına Mock.cs dosyalarımızı aşağıdaki gibi oluşturalım.

namespace ProductGrpc.Models
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Detail { get; set; }
        public float Price { get; set; }
        public ProductStatus Status { get; set; }
        public DateTime CreatedOn { get; set; }
    }

    public enum ProductStatus
    {
        None = 0,
        Low = 1,
        InStock = 2
    }
}
namespace ProductGrpc.Data
{
    public class Mock
    {
        public static List<Product> Data = new List<Product>
        {
            new Product { Id = 1, Name = "Telefon", Detail = "Telefon Açıklaması", Price = 219.99f, Status = ProductStatus.Low, CreatedOn = DateTime.UtcNow },
            new Product { Id = 2, Name = "Bilgisayar", Detail = "Bilgisayar Açıklaması", Price = 529.99f, Status = ProductStatus.InStock, CreatedOn = DateTime.UtcNow },
            new Product { Id = 3, Name = "Kitap", Detail = "Kitap Açıklaması", Price = 19.99f, Status = ProductStatus.None, CreatedOn = DateTime.UtcNow },
            new Product { Id = 4, Name = "Müzik", Detail = "Müzik Açıklaması", Price = 9.99f, Status = ProductStatus.InStock, CreatedOn = DateTime.UtcNow },
        };
    }
}

Generate edilmiş servisi inherit ederek methodlarımızın içerisini doldurmaya başlayalım, bunun için Services klasörü altına ProductService.cs dosyasını oluşturalım ve ProductProtoService.ProductProtoServiceBase sınıfından türetelim. Bu isim oluşturduğumuz proto dosyasında servise verdiğimiz isme göre değişmektedir.

namespace ProductGrpc.Services
{
    public class ProductService : ProductProtoService.ProductProtoServiceBase
    {
        // Helper Methods
        private ProductModel GetFromProduct(Product product)
        {
            return new ProductModel
            {
                Id = product.Id,
                Name = product.Name,
                Detail = product.Detail,
                Price = product.Price,
                Status = (ProductGrpc.Protos.ProductStatus)product.Status,
                CreatedOn = Timestamp.FromDateTime(product.CreatedOn),
            };
        }
        private Product GetFromModel(ProductModel model)
        {
            return new Product
            {
                Id = model.Id,
                Name = model.Name,
                Detail = model.Detail,
                Price = model.Price,
                Status = (ProductGrpc.Models.ProductStatus)model.Status,
                CreatedOn = model.CreatedOn.ToDateTime()
            };
        }

        public override Task<ProductModel> GetProduct(GetProductRequest request, ServerCallContext context)
        {
            var product = Mock.Data.FirstOrDefault(f => f.Id == request.Id);
            if (product == null)
            {
                throw new RpcException(new Status(StatusCode.NotFound, $"Product with Id: {request.Id} is not found."));
            }

            return Task.FromResult(GetFromProduct(product));
        }

        public override async Task GetProducts(Empty request, IServerStreamWriter<ProductModel> responseStream, ServerCallContext context)
        {
            foreach (var product in Mock.Data)
            {
                await responseStream.WriteAsync(GetFromProduct(product));
            }
        }

        public override Task<ProductModel> InsertProduct(InsertProductRequest request, ServerCallContext context)
        {
            var product = GetFromModel(request.Product);
            Mock.Data.Add(product);
            return Task.FromResult(GetFromProduct(product));
        }

        public override async Task<BulkInsertProductResponse> BulkInsertProduct(IAsyncStreamReader<ProductModel> requestStream, ServerCallContext context)
        {
            var inserted = 0;
            while (await requestStream.MoveNext())
            {
                var product = GetFromModel(requestStream.Current);
                Mock.Data.Add(product);
                inserted++;
            }
            return new BulkInsertProductResponse
            {
                Success = inserted > 0,
                Inserted = inserted
            };
        }

        public override Task<ProductModel> UpdateProduct(UpdateProductRequest request, ServerCallContext context)
        {
            var product = Mock.Data.FirstOrDefault(f => f.Id == request.Product.Id);
            if (product == null)
            {
                throw new RpcException(new Status(StatusCode.NotFound, $"Product with Id: {product.Id} is not found."));
            }

            product.Name = request.Product.Name;
            product.Detail = request.Product.Detail;
            product.Price = request.Product.Price;
            product.Status = (ProductGrpc.Models.ProductStatus)request.Product.Status;
            product.CreatedOn = request.Product.CreatedOn.ToDateTime();

            return Task.FromResult(GetFromProduct(product));
        }

        public override Task<RemoveProductResponse> RemoveProduct(RemoveProductRequest request, ServerCallContext context)
        {
            var product = Mock.Data.FirstOrDefault(f => f.Id == request.Id);
            if (product == null)
            {
                throw new RpcException(new Status(StatusCode.NotFound, $"Product with Id: {request.Id} is not found."));
            }

            Mock.Data.Remove(product);

            return Task.FromResult(new RemoveProductResponse { Success = true });
        }
    }
}

Oluşturulan servis ile Startup.cs dosyasında Grpc’yi konfigüre edelim.

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddGrpc(options =>
        {
            options.EnableDetailedErrors = true;
        });
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapGrpcService<ProductService>();
        });
    }
}

Client (İstemci) Projesi

Oluşturduğumuz sunucu projesini consume edecek istemci projesini yine aynı yöntemle ProductGrpcClient isminde ve Worker Service tipinde oluşturalım.

dotnet new worker --name ProductGrpcClient

Projeyi oluşturduktan sonra Protos isimli bir klasör açarak ProductGrpc projesindeki product.proto dosyasını içerisine kopyalayıp aşağıdaki değişikliği yapalım.

option csharp_namespace = "ProductGrpcClient.Protos";

ProductGrpcClient.csproj dosyası içerisine .proto dosyamızı ekleyelim. Burada GrpcServices attribute’ünü Client olarak işaretlediğimize dikkat edin.

<ItemGroup>
  <Protobuf Include="Protos/product.proto" GrpcServices="Client" />
</ItemGroup>

Bu işlemden sonra sunucu projemizi iki yoldan consume edebiliriz. İlkine göz atalım. Bunun için öncelikle Google.Protobuf, Grpc.Net.Client ve Grpc.Tools paketlerini kurarak işleme devam edelim.

using var channel = GrpcChannel.ForAddress("https://localhost:5001");
var client = new ProductProtoService.ProductProtoServiceClient(channel);

var product = await client.GetProductAsync(new GetProductRequest { Id = 1 });

Bu yolla rpc fonksiyonlarımızı artık çağırabiliriz. Bir diğer yönteme göz atalım, bunun için ise Grpc.AspNetCore isimli paketi kurmamız yeterli. Ardından ConfigureServices methodu içerisinden servisimizi client olarak işaretleyelim.

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureServices((hostContext, services) =>
        {
            services.AddHostedService<Worker>();

            services.AddGrpcClient<ProductProtoService.ProductProtoServiceClient>(options =>
            {
                options.Address = new Uri("https://localhost:5001");
            });

            services.AddScoped<ProductService>();
        });

Daha sonra consume edeceğimiz client servisindeki methodları işleyeceğimiz Services klasörü altına ProductService.cs isimli sınıfımızı oluşturalım.

namespace ProductGrpcClient.Services
{
    public class ProductService
    {
        private readonly ProductProtoService.ProductProtoServiceClient _productProtoService;

        public ProductService(ProductProtoService.ProductProtoServiceClient productProtoService)
        {
            _productProtoService = productProtoService;
        }

        public async Task<ProductModel> GetProduct(int Id)
        {
            return await _productProtoService.GetProductAsync(new GetProductRequest { Id = Id });
        }

        public async Task<List<ProductModel>> GetProducts()
        {
            var products = new List<ProductModel>();

            using var productsData = _productProtoService.GetProducts(new Empty());

            await foreach (var product in productsData.ResponseStream.ReadAllAsync())
            {
                products.Add(product);
            }

            return products;
        }

        public async Task<ProductModel> InsertProduct(InsertProductRequest product)
        {
            return await _productProtoService.InsertProductAsync(product);
        }

        public async Task<ProductModel> UpdateProduct(UpdateProductRequest product)
        {
            return await _productProtoService.UpdateProductAsync(product);
        }

        public async Task<RemoveProductResponse> RemoveProduct(int Id)
        {
            return await _productProtoService.RemoveProductAsync(new RemoveProductRequest { Id = Id });
        }

        public async Task<BulkInsertProductResponse> BulkInsertProduct(List<ProductModel> products)
        {
            using var bulkInsert = _productProtoService.BulkInsertProduct();

            foreach (var product in products)
            {
                await bulkInsert.RequestStream.WriteAsync(product);
            }

            await bulkInsert.RequestStream.CompleteAsync();

            return await bulkInsert;
        }
    }
}

Yazmış olduğumuz servis methodlarını kullandığımız örneğe göz atalım.

namespace ProductGrpcClient
{
    public class Worker : BackgroundService
    {
        private readonly ILogger<Worker> _logger;
        private readonly ProductService _productService;

        public Worker(ILogger<Worker> logger, ProductService productService)
        {
            _logger = logger;
            _productService = productService;
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
            {
                // The following statement allows you to call insecure services. To be used only in development environments.
                AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);
            }

            while (!stoppingToken.IsCancellationRequested)
            {
                // With metdod 1 (Requires Google.Protobuf, Grpc.Net.Client, Grpc.Tools)
                // using var channel = GrpcChannel.ForAddress("http://localhost:5000");
                // var client = new ProductProtoService.ProductProtoServiceClient(channel);
                // var product = await client.GetProductAsync(new GetProductRequest { Id = 1 });

                // With method 2 (Required Grpc.AspNetCore packages and .AddGrpcClient())
                try
                {
                    var product = await _productService.GetProduct(10);
                    _logger.LogInformation($"GetProduct Response: {product}");
                }
                catch (RpcException ex)
                {
                    if (ex.StatusCode == StatusCode.NotFound)
                    {
                        var product = await _productService.GetProduct(1);
                        _logger.LogInformation($"GetProduct Response: {product}");
                    }
                    else
                    {
                        throw;
                    }
                }

                var products = await _productService.GetProducts();
                _logger.LogInformation($"GetProducts Response: {products}");

                var insertProduct = await _productService.InsertProduct(new InsertProductRequest
                {
                    Product = new ProductModel
                    {
                        Id = 5,
                        Name = "Arabax",
                        Detail = "Arabax Açıklaması",
                        Price = 1999.99f,
                        Status = ProductStatus.InStock,
                        CreatedOn = Timestamp.FromDateTime(DateTime.UtcNow)
                    }
                });
                _logger.LogInformation($"InsertProduct Response: {insertProduct}");

                var updateProduct = await _productService.UpdateProduct(new UpdateProductRequest
                {
                    Product = new ProductModel
                    {
                        Id = 5,
                        Name = "Araba",
                        Detail = "Araba Açıklaması",
                        Price = 1990f,
                        Status = ProductStatus.Low,
                        CreatedOn = Timestamp.FromDateTime(DateTime.UtcNow)
                    }
                });
                _logger.LogInformation($"UpdateProduct Response: {updateProduct}");

                var removeProduct = await _productService.RemoveProduct(5);
                _logger.LogInformation($"RemoveProduct Response: {removeProduct}");

                var bulkInsert = await _productService.BulkInsertProduct(new List<ProductModel>
                {
                    new ProductModel {Id = 5, Name = "Araba", Detail = "Araba Açıklaması", Price = 1990, Status = ProductStatus.InStock, CreatedOn = Timestamp.FromDateTime(DateTime.UtcNow)},
                    new ProductModel {Id = 6, Name = "Ev", Detail = "Ev Açıklaması", Price = 11990, Status = ProductStatus.InStock, CreatedOn = Timestamp.FromDateTime(DateTime.UtcNow)},
                });
                _logger.LogInformation($"BulkInsertProduct Response: {bulkInsert}");


                await Task.Delay(5000, stoppingToken);
            }
        }
    }
}

Bonus: gRPCurl

gRPCurl, gRPC sunucularına komut satırından erişmek için kullanılan bir tooldur. Bu bağlantıyı kullanarak kurulum yapıldıktan sonra Grpc.AspNetCore.Server.Reflection paketini kurup bu özelliği kullanacağımızı Startup.cs dosyasında bildirdikten sonra aktif hale getiriyoruz.

namespace ProductGrpc
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddGrpcReflection();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {

            app.UseEndpoints(endpoints =>
            {
                if (env.IsDevelopment())
                {
                    endpoints.MapGrpcReflectionService();
                }
            });
        }
    }
}

Aşağıdaki komutları kullanarak komut satırı üzerinden, çalışan bir gRPC sunucusuna ulaşarak ilgilendiğiniz kaynaklara ulaşabilirsiniz.

grpcurl -plaintext localhost:5001 describe

grpcurl -plaintext localhost:5001 describe greet.HelloRequest

grpcurl -d {\"name\":\"World\"} -plaintext localhost:5001 greet.Greeter/SayHello

Bonus: Logging

gRPC servisi ve istemcisi built-in log altyapısını kullanılır, loglar beklenmeyen durumların debug edilmesinde işlerimizi kolaylaştırır. Loglamayı aktif etmek için iki yol izleyebiliriz, bunlardan ilki appsettings.json içerisine aşağıdaki kod, log düzeğiyle birlikte eklenmelidir.

{
  "Logging": {
    "LogLevel": {
      "Grpc": "Debug"
    }
  }
}

Bir diğer yöntemdeyse, Program.cs altından ConfigureLogging methodunu çağırarak aktif hale gelmektedir.

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureLogging(logging =>
        {
             logging.AddFilter("Grpc", LogLevel.Debug);
        })
        .ConfigureServices((hostContext, services) =>
        {
            services.AddHostedService<Worker>();

            services.AddGrpcClient<ProductProtoService.ProductProtoServiceClient>(options =>
            {
                options.Address = new Uri("https://localhost:5001");
            });

            services.AddScoped<ProductService>();
        });

Kaynak dosyasına buradan ulaşabilirsiniz.

1 Response

  1. Mahsun dedi ki:

    Mükemmel bir kaynak

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir