Derinlemesine yazılım eğitimleri için kanalımı takip edebilirsiniz...

gRPC – File Streaming Nasıl Gerçekleştirilir?

Merhaba,

Son zamanlarda gRPC kütüphanesi üzerine derinlemesine incelemelerde bulunmaya çabalamakta ve gerektiği taktirde önemli noktaları bloğumda ilgili konuya odaklı makaleyle yer edindirmeye çalışmaktayım. Hal böyleyken, gRPC kütüphanesinin Http/2 protokolü üzerinden binary formatta veri iletimi sağlamasının getirisi olan hızlı transfer süreçlerinden yola çıkarak bir dosya transfer işleminin nasıl yapıldığını ele almayı ve bunu siz değerli okuyucularımla paylaşmayı uygun görmüş bulunmaktayım.

İçeriğimiz yoğun olarak pratiksel açıdan seyredeceğinden dolayı hiç vakit kaybetmeden direkt olarak operasyona odaklanarak başlayabiliriz.

Proto Dosyası

İlk olarak server ile client arasındaki dosya transferi sürecindeki kontratı sağlayacak olan proto dosyasını tasarlayarak başlayalım.

syntax = "proto3";
option csharp_namespace = "gRPCFileTransferExample";
package file;
import "google/protobuf/empty.proto"; //Geriye dönüş değeri olmayan metotlar için kullanıyoruz.
service FileService {
    rpc FileDownLoad (FileInfo) returns (stream BytesContent);
    rpc FileUpLoad (stream BytesContent) returns(google.protobuf.Empty); //Geriye Empty dönüyor, yani dönüş değeri yok gibi düşünülebilir.
}
message FileInfo{
    string fileName = 1;
    string fileExtension = 2;
}
message BytesContent{
    int64 fileSize = 1;
    bytes buffer = 2;
    int32 readedByte = 3;
    FileInfo info = 4;
}

Yukarıdaki proto içeriğine göz atarsanız eğer, ilk olarak stream edilecek olan dosyayı sembolik olarak temsil edecek olan ‘BytesContent’ message’ına dikkatinizi çekerim. Bu message içerisindeki ‘fileSize’ alanı transfer edilecek olan dosyanın total boyutunu ifade ederken, ‘buffer’ alanı ise transfer sürecinde her bir stream’de ne kadarlık parçanın/tampon taşınacağını ifade etmektedir. Benzer mantıkla ‘readedByte’ alanı ise o anki stream’de okunan byte sayısını ifade etmektedir. ‘info’ isimli alanı inceleyebilmek için diğer message’ımız olan ‘FileInfo’yu incelememiz gerekmektedir. Bu message’da ise transfer edilecek dosyanın adı(fileName) ve uzantısına(fileExtension) dair bilgiler taşınmaktadır. File stream sürecinde transfer edilecek data(ları) temsil edecek olan bu message’lar vasıtasıyla, ‘FileService’ isminde oluşturulacak servisteki ‘FileDownload’ ve ‘FileUpload’ fonksiyonları sayesinde veriler taşınacak ve gerekli operasyonlar sağlanmış olacaktır. Burada ‘FileDownload’da server’dan client’a bir akış söz konusu olacağından dolayı Server Streaming veri iletim tipi benimsenmişken, ‘FileUpload’da ise tam tersi client’dan server’a akış gerçekleştirileceğinden Client Streaming veri iletim tipi kullanılacaktır.

Şimdi proto dosyasını tasarladığımıza göre artık veri iletimi için server ve client uygulamalarını geliştirmeye başlayabiliriz. Bunun için herhangi birinden başlanması yeterli olacaktır. Misal olarak bizler server kanadını geliştirerek yolumuza devam edebiliriz.

Server Uygulamasının Geliştirilmesi

  • FileUpload Operasyonu
    Client’tan server’a gönderilecek olan dosyayı karşılayıp, uygun alana depolayacak olan fonksiyondur. İçerik olarak aşağıdaki gibi geliştirilebilir;

        public class FileUploadService : FileServiceBase
        {
            readonly IWebHostEnvironment _webHostEnvironment;
            public FileUploadService(IWebHostEnvironment webHostEnvironment)
            {
                _webHostEnvironment = webHostEnvironment;
            }
            public override async Task<Empty> FileUpLoad(IAsyncStreamReader<BytesContent> requestStream, ServerCallContext context)
            {
                //Stream'in yapılacağı dizin belirleniyor.
                string path = Path.Combine(_webHostEnvironment.WebRootPath, "files");
                if (!Directory.Exists(path))
                    Directory.CreateDirectory(path);
    
                //Dosyanın stream edileceği hedef FileStream.
                FileStream fileStream = null;
    
                try
                {
                    int count = 0;
    
                    //Yüzdelik hesaplaması için 'chunkSize' değişkeni tanımlanıyor.
                    decimal chunkSize = 0;
    
                    //Gelen stream okunmaktadır.
                    while (await requestStream.MoveNext())
                    {
                        //Stream ilk başladığında(ilk adımda) yapılması gereken öncelikli işlevler gerçekleştiriliyor.
                        if (count++ == 0)
                        {
                            //Stream'de gelen Info nesnesinin FileName özelliğiyle hedef dosyanın adı belirleniyor.
                            fileStream = new FileStream($"{path}/{requestStream.Current.Info.FileName}{requestStream.Current.Info.FileExtension}", FileMode.CreateNew);
    
                            //Gelecek dosya boyutu kadar alan tahsis ediliyor. Bu işlem zorunlu değildir lakin süreçte farklı bir program tarafından diskin doldurulup, işimize engel olmasının önüne geçiyoruz.
                            fileStream.SetLength(requestStream.Current.FileSize);
                        }
    
                        //Buffer, akışta gelen her bir parçanın ta kendisidir. Chunk olarak isimlendirilir.
                        var buffer = requestStream.Current.Buffer.ToByteArray();
    
                        //Akışta gelen chunk'ları hedef FileStream nesnesine yazdırıyoruz. Burada, ikinci parametrede ki '0' değeri ile buffer'dan kaçıncı byte'dan itibaren okunacağı ve yazdırılacağı bildirilmektedir.
                        await fileStream.WriteAsync(buffer, 0, requestStream.Current.ReadedByte);
    
                        //Akışın yüzdelik olarak ne kadarının aktarıldığı hesaplanıyor.
                        //Formülasyon olarak;
                        //Okunan parça sayısı(ReadedByte), chunkSize değişkeninde toplanıyor ve 100 ile çarpılıp sonuç toplam boyuta bölünüyor. Nihai sonuç ise yakın olan tam sayıya yuvarlanıyor ve yüzdelik olarak ne kadarlık aktarım gerçekleştirildiği hesaplanmış oluyor.
                        Console.WriteLine($"{Math.Round(((chunkSize += requestStream.Current.ReadedByte) * 100) / requestStream.Current.FileSize)}%");
                    }
                    Console.WriteLine("Yüklendi...");
    
                }
                catch (Exception ex)
                {
                    //Client'ta stream 'CompleteAsync' edildiği vakit burada olası hata meydana gelebilmektedir. Dolayısıyla tüm bu süreci try catch ile kontrol ediyoruz.
                }
                await fileStream.DisposeAsync();
                fileStream.Close();
                return new Empty();
            }
            .
            .
            .
        }
    

    Kog bloğu içerisinde adım adım açıklama yapılmış olsa dahi, yine de ufak dokunuşlar yapmamızda fayda olacağı kanaatindeyim. Yukarıda görüldüğü üzere server’da File Upload ve Download operasyonlarını üstlenecek olan servis ‘FileUploadService’ olarak tanımlanmıştır(birazdan FileDownload’da da bu serviste çalışmaya devam edeceğiz) 26. satıra göz atarsanız eğer, client’tan gelen stream data okunmakta ve ilgili döngü içerisinde ilk gelen chunk’dan/data’dan/buffer’dan/parçadan transfer edilecek dosyanın ana omurgası oluşturulmaktadır. Burada, gelen buffer’ların toplanacağı FileStream belirlenmekte ve akabinde hedef bilgisayarda ne kadarlık bir alan kaplayacağına dair önceden bir tahsis işlemi gerçekleştirilmektedir. İlk data’dan sonraki tüm buffer’lar da bu işleme gerek kalmayacağından dolayı akış 39. satırdan devam edecektir. Dikkat ederseniz Client Streaming yöntemi ile dosya transferi sağlanmaktadır.

  • FileDownload Operasyonu
    Client’ın server’dan talep edeceği dosyayı gönderecek olan fonksiyondur. İşlevsel olarak aşağıdaki gibi inşa edilebilir;

        public class FileUploadService : FileServiceBase
        {
            readonly IWebHostEnvironment _webHostEnvironment;
            public FileUploadService(IWebHostEnvironment webHostEnvironment)
            {
                _webHostEnvironment = webHostEnvironment;
            }
            public override async Task FileDownLoad(gRPCFileTransferExample.FileInfo request, IServerStreamWriter<BytesContent> responseStream, ServerCallContext context)
            {
                string path = Path.Combine(_webHostEnvironment.WebRootPath, "files");
    
                //Client tarafından download edilmek istenen dosya bilgileri gönderilmiştir. Bu bilgilere karşılık olan dosya bulunmakta ve FileStream olarka işaretlenmektedir.
                using FileStream fileStream = new FileStream($"{path}/{request.FileName}{request.FileExtension}", FileMode.Open, FileAccess.Read);
                
                //Her bir akışta gönderilecek veri parçasını belirliyoruz.
                byte[] buffer = new byte[2048];
                
                //Gönderilecek dosyanın bilgilerini veriyoruz.
                BytesContent content = new BytesContent
                {
                    FileSize = fileStream.Length,
                    Info = new gRPCFileTransferExample.FileInfo { FileName = "video", FileExtension = ".mp4" },
                    ReadedByte = 0
                };
    
                //Her bir buffer, 0. byte'tan itibaren 2048 adet okunmakta ve sonuç 'content.ReadedByte'a atanmaktadır.
                while ((content.ReadedByte = fileStream.Read(buffer, 0, buffer.Length)) > 0)
                {
                    //Okunan buffer'ın stream edilebilmesi için 'message.proto' dosyasındaki 'bytes' türüne dönüştürülüyor.
                    content.Buffer = ByteString.CopyFrom(buffer);
                    //'BytesContent' nesnesi stream olarak gönderiliyor.
                    await responseStream.WriteAsync(content);
                }
                
                fileStream.Close();
            }
        }
    

    ‘FileUpload’ fonksiyonundaki benzer mantıkla gelen istek neticesinde talep edilen data stream edilerek gönderilmektedir. 19. satıra bakarsanız eğer transfer edilecek dosyanın ana omurga bilgileriyle birlikte, 30. satırda o anki buffer/parça bilgileri ‘BytesContent’ nesnesi olarak gönderilmektedir. Birazdan göreceğimiz üzere, client gelen bu ‘BytesContent’ datalarının ilkini tıpkı ‘FileUpload’da olduğu gibi ana omurga olarak işleyecek ve transfer edilen dosyanın sınırlarını belirleyecektir. İlkinin dışındakileri ise adım adım transfer sürecinde parça olarak değerlendirip kullanacaktır. Yani bu işlemi kullanacak olan client tabanı esasında server’ın ‘FileUpload’ operasyonunun birebir muadili olacaktır diyebiliriz. Burada da dikkat ederseniz Server Streaming yöntemiyle veri transferi sağlanmaktadır.

Client Uygulamalarının Geliştirilmesi

Client upload ve download olmak üzere birden fazla operasyonu yürüten tek bir uygulama olabilecekken, bu sorumlulukları ayrı ayrı üstlenen farklı uygulamalar da olabilir. Artık bunun tercihi sizin keyfinize 🙂 daha da ötesi ihtiyaçlarınızın insafına bağlıdır. Her iki durumda da yapılması gereken operasyonlar aşağıdaki gibi seyretmek durumundadır :

  • File Upload Operasyonu
    Server’a dosya yükleyecek/gönderecek olan client’ın işlevselliği aşağıdaki gibi olabilir;

        class Program
        {
            static async Task Main(string[] args)
            {
                var channel = GrpcChannel.ForAddress("https://localhost:5001");
                var client = new FileService.FileServiceClient(channel);
    
                string file = @"C:\Users\ngaka\Desktop\gRPCClientExample\gRPCClientExample\File\Global Değişkenler.mp4";
    
                //Stream yapılacak dosya belirleniyor.
                using FileStream fileStream = new FileStream(file, FileMode.Open);
    
                //Dosyanın tüm bilgileri ediniliyor. Bu nesne stream ile birlikte gönderilecektir.
                var content = new BytesContent
                {
                    FileSize = fileStream.Length,
                    ReadedByte = 0,
                    Info = new gRPCFileTransferExample.FileInfo { FileName = "Global Değişkenler", FileExtension = ".mp4" }
                };
          
                //Stream için server'da ki FileUpload fonksiyonu çağrılıyor.
                var upload = client.FileUpLoad();
    
                //Akışta ne kadar parça gideceği önceden ayarlanıyor. Burada 2048'lik bir alan tahsis edilmektedir. Gönderilecek dosyanın boyutu ne olursa olsun en fazla 2048'lik parça gönderilebileceğinden dolayı bu şekilde ayarlanmıştır.
                byte[] buffer = new byte[2048];
              
                //Her bir buffer, 0. byte'tan itibaren 2048 adet okunmakta ve sonuç 'content.ReadedByte'a atanmaktadır.
                while ((content.ReadedByte = fileStream.Read(buffer, 0, buffer.Length)) > 0)
                {
                    //Okunan buffer'ın stream edilebilmesi için 'message.proto' dosyasındaki 'bytes' türüne dönüştürülüyor.
                    content.Buffer = ByteString.CopyFrom(buffer);
                    //'BytesContent' nesnesi stream olarak gönderiliyor.
                    await upload.RequestStream.WriteAsync(content);
                }
                await upload.RequestStream.CompleteAsync();
    
                fileStream.Close();
            }
        }
    
  • File Download Operasyonu
    Benzer mantıkla server’dan dosya talep eden, bir başka deyişle indirme talebinde bulunan client aşağıdaki gibi tasarlanabilir.

        class Program
        {
            static async Task Main(string[] args)
            {
                var channel = GrpcChannel.ForAddress("https://localhost:5001");
                var client = new FileServiceClient(channel);
    
                //Dosyanın indirileceği dizin tanımlanıyor.
                string downloadPath = @"C:\Users\ngaka\Desktop\gRPCClientExample\gRPCDownloadExample\DownloadFile";
    
                //Server'dan talep edilen dosya bilgileri 'FileInfo' olarak tanımlanıyor.
                var fileInfo = new gRPCFileTransferExample.FileInfo
                {
                    FileExtension = ".mp4",
                    FileName = "Global Değişkenler"
                };
    
                FileStream fileStream = null;
    
                //Server'dan ilgili 'FileInfo' ile talep yapılıyor.
                var request = client.FileDownLoad(fileInfo);
    
                CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
    
                int count = 0;
                decimal chunkSize = 0;
                
                //Talep neticesinde stream olarak gelen dosya parçaları okunmaya başlanıyor.
                while (await request.ResponseStream.MoveNext(cancellationTokenSource.Token))
                {
                    //İlk gelen parçada transfer edilen dosyanın ana hatları belirleniyor.
                    if (count++ == 0)
                    {
                        //Transfer edilen dosyanın server'dan gelen bilgiler eşliğinde belirtilen dizine depolanması için konfigürasyon gerçekleştiriliyor.
                        fileStream = new FileStream(@$"{downloadPath}\{request.ResponseStream.Current.Info.FileName}{request.ResponseStream.Current.Info.FileExtension}", FileMode.CreateNew);
                        
                        //Depolanacak yerde dosya boyutu kadar alan tahsis ediliyor.
                        fileStream.SetLength(request.ResponseStream.Current.FileSize);
                    }
    
                    //'message.proto' dosyasında belirtilen 'bytes' türüne karşılık olarak 'ByteString' türünde gelen buffer'lar byte dizisine dönüştürülüyor.
                    var buffer = request.ResponseStream.Current.Buffer.ToByteArray();
                    
                    //İlgili FileStream'e parçalar yazdırılıyor.
                    await fileStream.WriteAsync(buffer, 0, request.ResponseStream.Current.ReadedByte);
    
                    Console.WriteLine($"{Math.Round(((chunkSize += request.ResponseStream.Current.ReadedByte) * 100) / request.ResponseStream.Current.FileSize)}%");
                }
                Console.WriteLine("Yüklendi...");
    
                await fileStream.DisposeAsync();
                fileStream.Close();
            }
        }
    

Evet… Görüldüğü üzere bir dosya transferi sürecinde nasıl bir proto dosyası tasarlanması gerektiğini, server ve client kanatlarında ne şekilde operasyonların icra edilmesi gerektiğini vs. direkt olarak pratikte incelemiş olduk. Şimdi bu tarz işlevsellikleri kullanan uygulamaların ne şekilde sonuç verdiğini hep beraber gözlemleyelim.

Test Yapalım

File Upload File Download
gRPC - File Streaming Nasıl Gerçekleştirilir? gRPC - File Streaming Nasıl Gerçekleştirilir?

İlgilenenlerin faydalanması dileğiyle…
Sonraki yazılarımda görüşmek üzere…
İyi çalışmalar…

Not : Örnek çalışma projelerini indirmek için aşağıdaki linklere tıklayınız.

gRPCClientExample
gRPCServerExample

Bunlar da hoşunuza gidebilir...

Bir cevap yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

*