Categories: .NET

C# ile Asenkron Socket Programlama

Kimine göre eskide kalmış olsa da, hala birçok yerde aktif olarak kullanılan soket programlamadan bahsedip üzerinde bir örnek gerçekleştirmeye çalışacağım.

Öncelikle soket programlama nedir?

Soketler için istemci (Client) ve sunucu (Server) arasındaki bağlantının sağlanması için olan bir iletişim kanalı diyebiliriz. Yaşam döngüsünü basitçe tarif etmek gerekirse, sunucu önceden belirlenen portu dinler, istemci ise bu porta mesaj gönderir.

İletişim protokolü olarak TCP de, sunucu belirli bir portu dinleyerek gelen istekleri karşılar, UDP protokolünde ise, tek bir soket üzerinden birden çok istemciye veriyi iletebilir.

Unutulmamalıdır ki UDP protokolünü kullanmaya karar vermiş biri, meydana gelebilecek paket kayıplarını da göze alıp, bunları iyi handle etmesi gerekmektedir.

Bunun en büyük örneği VOIP sistemlerinde ses paketlerinin zaman zaman kayıplara uğramasıdır. Kısaca “fire and forget” mantığında çalışmaktadır diyebiliriz.

Nerelerde kullanılıyor?

Bu makaleyi yazma sebebimden yola çıkarak, öncelikle gerçek bir senaryodan örnek vermek istiyorum.

Çalışıyor olduğum firmada, oldukça yoğun servis verileri ile çalışmaktayız. IIS’de konumlanan tek bir uygulama içerisinden, yaklaşık 30+ farklı servis üzerinden verilerin eş zamanlı olarak çekildiğini ve işlendiğini düşünebilirsiniz. Çekilen bu verilerin boyut’ları ise oldukça büyük bir durumda.

Domain içerisine her bir yeni servis eklendiğinde ise, IIS üzerindeki uygulama, iyice hantallaşmaya ve sunucuyu oldukça yormaya başlamıştı. Bu gibi sebeplerden yola çıkarak, ana uygulama domain’i içerisinden servis’leri ayırmaya karar verdik. Ayrılan bu servisleri ise, “n” adet yeni oluşturduğumuz servis makinelerine deployment işlemlerini gerçekleştirdik. Ana uygulamamız ile servis makineleri arasındaki iletişimi ise, gerek veri boyutlarının büyüklüğünden gerekse de daha hızlı bir iletişim sağlayabilmek için soket üzerinden yönetmeye karar verdik.

Böylece IIS üzerinde konumlanan ana uygulamamız üzerindeki yükü oldukça azaltmış ve daha esnek bir yapı sağlamış olduk.

Bir başka örnek vermek gerekirse, gerçek zamanlı uygulamaları düşünebiliriz. Örneğin chat veya bazı pos cihazlarını örnek olarak gösterebiliriz.

Bir örnek gerçekleştirelim

Öncelikle soket aracılığı ile aktarıcak olduğumuz nesnelerimizi, DataTransferObjects isminde bir proje içerisinde tanımlayalım. Öncelikle ExampleDTO class’ını aşağıdaki gibi oluşturalım.

using System;

namespace ExampleDataTransferObjects
{
    /// <summary>
    /// Serialize edebilmek için Serializable attributü ile işaretliyoruz.
    /// </summary>
    [Serializable]
    public class ExampleDTO
    {
        public string Status { get; set; }
        public string Message { get; set; }
    }
}

Oluşturduktan sonra kodlamaya sunucu ile devam edelim.

Solution içerisine yeni bir console application projesi ekleyip ExampleServer adını verelim. İçerisinde ise Sockets isminde bir klasör oluşturup, “Client” class’ını aşağıdaki gibi ekleyelim.

using ExampleDataTransferObjects;
using System;
using System.IO;
using System.Net.Sockets;
using System.Runtime.Serialization.Formatters.Binary;

namespace ExampleServer.Sockets
{
    public delegate void OnExampleDTOReceived(ExampleDTO eDTO);

    public class Client
    {
        #region Variables
        public OnExampleDTOReceived _OnExampleDTOReceived;
        Socket _Socket;

        // Socket işlemleri sırasında oluşabilecek errorları bu enum ile handle edebiliriz.
        SocketError socketError;
        byte[] tempBuffer = new byte[1024]; // 1024 boyutunda temp bir buffer, gelen verinin boyutu kadarıyla bunu receive kısmında handle edeceğiz.
        #endregion

        #region Constructor
        public Client(Socket socket)
        {
            _Socket = socket;
        }
        #endregion

        #region Public Methods
        public void Start()
        {
            // Socket üzerinden data dinlemeye başlıyoruz.
            _Socket.BeginReceive(tempBuffer, 0, tempBuffer.Length, SocketFlags.None, OnBeginReceiveCallback, null);
        }
        #endregion

        #region Private Methods
        void OnBeginReceiveCallback(IAsyncResult asyncResult)
        {
            // Almayı bitiriyoruz ve gelen byte array'in boyutunu vermektedir.
            int receivedDataLength = _Socket.EndReceive(asyncResult, out socketError);

            if (receivedDataLength <= 0 && socketError != SocketError.Success)
            {
                // Gelen byte array verisi boş ise bağlantı kopmuş demektir. Burayı istediğiniz gibi handle edebilirsiniz.
                return;
            }

            // Gelen byte array boyutunda yeni bir byte array oluşturuyoruz.
            byte[] resizedBuffer = new byte[receivedDataLength];

            Array.Copy(tempBuffer, 0, resizedBuffer, 0, resizedBuffer.Length);

            // Gelen datayı burada ele alacağız.
            HandleReceivedData(resizedBuffer);

            // Tekrardan socket üzerinden data dinlemeye başlıyoruz.
            // Start();

            // Socket üzerinden data dinlemeye başlıyoruz.
            _Socket.BeginReceive(tempBuffer, 0, tempBuffer.Length, SocketFlags.None, OnBeginReceiveCallback, null);
        }

        /// <summary>
        /// Gelen datayı handle edeceğimiz nokta.
        /// </summary>
        /// <param name="resizedBuffer"></param>
        void HandleReceivedData(byte[] resizedBuffer)
        {
            if (_OnExampleDTOReceived != null)
            {
                using (var ms = new MemoryStream(resizedBuffer))
                {
                    // BinaryFormatter aracılığı ile object tipimize geri deserialize işlemi gerçekleştiriyoruz ve ilgili delegate'e parametre olarak geçiyoruz.
                    ExampleDTO exampleDTO = new BinaryFormatter().Deserialize(ms) as ExampleDTO;

                    _OnExampleDTOReceived(exampleDTO);
                }
            }
        }
        #endregion
    }
}

Sunucu için istemci işlemlerini gerçekleştireceğimiz sınıfı ekledikten sonra ise “Sockets” klasörü içerisine asıl dinleme işlemini yapacağımız class olan “Listener” class’ını ekleyelim ve aşağıdaki gibi kodlayalım.

using ExampleDataTransferObjects;
using System;
using System.Net;
using System.Net.Sockets;

namespace ExampleServer.Sockets
{
    public class Listener
    {
        #region Variables
        Socket _Socket;
        int _Port;
        int _MaxConnectionQueue;
        #endregion

        #region Constructor
        public Listener(int port, int maxConnectionQueue)
        {
            _Port = port;
            _MaxConnectionQueue = maxConnectionQueue;

            // Socket'i tanımlıyoruz IPv4, socket tipimiz stream olacak ve TCP Protokolü ile haberleşeceğiz. 
            // TCP Protokolünde server belirlenen portu dinler ve gelen istekleri karşılar oysaki UDP Protokolünde tek bir socket üzerinden birden çok client'a ulaşmak mümkündür.
            _Socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        }
        #endregion

        #region Public Methods
        public void Start()
        {
            IPEndPoint ipEndPoint = new IPEndPoint(IPAddress.Any, _Port);

            // Socket'e herhangi bir yerden ve belirttiğimiz porttan gelecek olan bağlantıları belirtmeliyiz.
            _Socket.Bind(ipEndPoint);

            // Socketten gelecek olan bağlantıları dinlemeye başlıyoruz ve maksimum dinleyeceği bağlantıyı belirtiyoruz.
            _Socket.Listen(_MaxConnectionQueue);

            // BeginAccept ile asenkron olarak gelen bağlantıları kabul ediyoruz.
            _Socket.BeginAccept(OnBeginAccept, _Socket);
        }
        #endregion

        #region Private Methods
        void OnBeginAccept(IAsyncResult asyncResult)
        {
            Socket socket = _Socket.EndAccept(asyncResult);
            Client client = new Client(socket);

            // Client tarafından gönderilen datamızı işleyeceğimiz kısım.
            client._OnExampleDTOReceived += new Sockets.OnExampleDTOReceived(OnExampleDTOReceived);
            client.Start();

            // Tekrardan dinlemeye devam diyoruz.
            _Socket.BeginAccept(OnBeginAccept, null);
        }

        void OnExampleDTOReceived(ExampleDTO exampleDTO)
        {
            // Client tarafından gelen data, istediğiniz gibi burada handle edebilirsiniz senaryonuza göre.
            Console.WriteLine(string.Format("Status: {0}", exampleDTO.Status));
            Console.WriteLine(string.Format("Message: {0}", exampleDTO.Message));
        }
        #endregion
    }
}

Dinleme işlemini yapacağımız Listener class’ını hazırladığımıza göre, şimdi console uygulamamızın “Program” class’ının Main method’u içerisinde dinleme işlemine başlayabiliriz.

using ExampleServer.Sockets;
using System;

namespace ExampleServer
{
    class Program
    {
        static void Main(string[] args)
        {
            int port = 5555;
            Console.WriteLine(string.Format("Server Başlatıldı. Port: {0}", port));
            Console.WriteLine("-----------------------------");

            Listener listener = new Listener(port, 50);

            listener.Start();

            Console.ReadLine();
        }
    }
}

Böylelikle 5555 port’u üzerinden gelecek olan bağlantıları dinleyip, işleyecek bir sunucuya sahip olduk. Şimdi istemciyi kodlamaya başlayabiliriz.

Solution üzerine ExampleClient isminde bir console application projesi ekleyelim. Ardından içerisinde Sockets isimli bir klasör oluşturup aşağıdaki gibi bu klasör içerisinde ExampleSocket isimli bir class tanımlayalım.

using ExampleDataTransferObjects;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Runtime.Serialization.Formatters.Binary;

namespace ExampleClient.Sockets
{
    public class ExampleSocket
    {
        #region Variables
        Socket _Socket;
        IPEndPoint _IPEndPoint;

        // Socket işlemleri sırasında oluşabilecek errorları bu enum ile handle edebiliriz.
        SocketError socketError;
        byte[] tempBuffer = new byte[1024];
        #endregion

        #region Constructor
        public ExampleSocket(IPEndPoint ipEndPoint)
        {
            _IPEndPoint = ipEndPoint;

            // Socket'i tanımlıyoruz IPv4, socket tipimiz stream olacak ve TCP Protokolü ile haberleşeceğiz. 
            // TCP Protokolünde server belirlenen portu dinler ve gelen istekleri karşılar oysaki UDP Protokolünde tek bir socket üzerinden birden çok client'a ulaşmak mümkündür.
            _Socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        }
        #endregion

        #region Public Methods
        public void Start()
        {
            // BeginConnect ile asenkron olarak bir bağlantı başlatıyoruz.
            _Socket.BeginConnect(_IPEndPoint, OnBeginConnect, null);
        }

        public void SendData(ExampleDTO exampleDTO)
        {
            using (var ms = new MemoryStream())
            {
                // İlgili object'imizi binary'e serialize ediyoruz.
                new BinaryFormatter().Serialize(ms, exampleDTO);
                IList<ArraySegment<byte>> data = new List<ArraySegment<byte>>();

                data.Add(new ArraySegment<byte>(ms.ToArray()));

                // Gönderme işlemine başlıyoruz.
                _Socket.BeginSend(data, SocketFlags.None, out socketError, (asyncResult) =>
                {
                    // Gönderme işlemini bitiriyoruz.
                    int length = _Socket.EndSend(asyncResult, out socketError);

                    if (length <= 0 || socketError != SocketError.Success)
                    {
                        Console.WriteLine("Server bağlantısı koptu!");
                        return;
                    }
                }, null);

                if (socketError != SocketError.Success)
                    Console.WriteLine("Server bağlantısı koptu!");
            }
        }
        #endregion

        #region Private Methods
        void OnBeginConnect(IAsyncResult asyncResult)
        {
            try
            {
                // Bağlanma işlemini bitiriyoruz.
                _Socket.EndConnect(asyncResult);

                // Bağlandığımız socket üzerinden datayı dinlemeye başlıyoruz.
                _Socket.BeginReceive(tempBuffer, 0, tempBuffer.Length, SocketFlags.None, OnBeginReceive, null);
            }
            catch (SocketException)
            {
                // Servera bağlanamama durumlarında bize SocketException fırlatıcaktır. Hataları burada handle edebilirsiniz.
                Console.WriteLine("Servera bağlanılamıyor!");
            }
        }

        void OnBeginReceive(IAsyncResult asyncResult)
        {
            // Almayı bitiriyoruz ve geriye gelen byte array'in boyutunu vermektedir.
            int receivedDataLength = _Socket.EndReceive(asyncResult, out socketError);

            if (receivedDataLength <= 0 || socketError != SocketError.Success)
            {
                // Gelen byte array verisi boş ise bağlantı kopmuş demektir. Burayı istediğiniz gibi handle edebilirsiniz.
                Console.WriteLine("Server bağlantısı koptu!");
                return;
            }

            // Tekrardan socket üzerinden datayı dinlemeye başlıyoruz.
            _Socket.BeginReceive(tempBuffer, 0, tempBuffer.Length, SocketFlags.None, OnBeginReceive, null);
        }
        #endregion
    }
}

İstemcimiz için soketi hazırladığımıza göre, şimdi console uygulamamızın Program class’ının Main method’u içerisinde soket’e bağlanma işlemlerini gerçekleştirebiliriz.

using ExampleClient.Sockets;
using ExampleDataTransferObjects;
using System;
using System.Net;
using System.Linq;

namespace ExampleClient
{
    class Program
    {
        static void Main(string[] args)
        {
            int port = 5555;
            Console.WriteLine(string.Format("Client Başlatıldı. Port: {0}", port));
            Console.WriteLine("-----------------------------");

            ExampleSocket exampleSocket = new ExampleSocket(new IPEndPoint(IPAddress.Parse("127.0.0.1"), port));
            exampleSocket.Start();

            Console.WriteLine("Göndermek için \"G\", basınız...");

            int count = 1;
            while (Console.ReadLine().ToUpper() == "G")
            {
                ExampleDTO exampleDTO = new ExampleDTO()
                {
                    Status = string.Format("{0}. Alındı", count),
                    Message = string.Format("{0} ip numaralı client üzerinden geliyorum!", GetLocalIPAddress())
                };

                exampleSocket.SendData(exampleDTO);
                count++;
            }

            Console.ReadLine();
        }

        static string GetLocalIPAddress()
        {
            string localIP = Dns.GetHostEntry(Dns.GetHostName()).AddressList.Where(a => a.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork).FirstOrDefault().ToString();

            return localIP;
        }
    }
}

İstemcimizde sunucuya bağlanmak için hazır durumda.Tek yapmamız gereken göndermek için G tuşuna basmak.

 

ExampleDataTransferObjects kütüphanesini istemci ve sunucu tarafında referans olarak eklemeyi unutmayınız.

Kaynak kodu ekte bulabilirsiniz.

SocketProgrammingExample

 

Gökhan Gökalp

View Comments

  • Selamlar.
    Bu yapıyı kullanarak server ve client arasında büyük veriler göndermek problem yaşatır mı? ayrıca ağı dinleyen programlar için ne kadar güvenli bu yöntem?

    • Merhaba, limit olarak herhangi bir limit bulunmamakta sadece buffer'ın aşılmamasına dikkat etmeniz gerekmektedir. https://msdn.microsoft.com/en-us/library/ms145160.aspx adresindeki notlar'ın bulunduğu kısımda okuyabilirsiniz. "You must ensure that the size of your buffer does not exceed the maximum packet size of the underlying service provider." Ayrıca arp poisoning'e gelince herhangi biri server ağınıza ulaşıp o ağ üzerinde sniff işlemi yaparsa elbette dinlenebilir. Buda sizin mimari olarak hatanızı göstermektedir.

  • Hocam merhaba, online oyun için socket oluşturma çabası içerisindeyiz :) Birde bunun multi-thread örneğini yayınlayabilir misiniz?

    • Merhaba, maksimum client sayısına ortalama bir şey söylemek yanlış olur. Concurrent olarak gerçekleştireceğiz işlemlere, bu uygulamayı host edecek olan server'ın kapasitesine ve aynı zamanda network'e bağlı olan bir durum söz konusudur. Bu tarz uygulamalarda daha ölçeklenebilir bir yapı elde edebilmek için, load balancing tarzı işlemler ile yük maliyetini farklı server'lara dağıtabilirsiniz.

      • Hocam client Yazdığınız sınıf server görevi yapıyor niçin clientimiz sever işlemi yapıp bağlantıları dinliyor

        • Evet yazdığımız proje namespace'inde de olduğu gibi Server. Listener haricinde Client isminde bir class oluşturmamın sebebi ise, Listener içerisinde, client işlemlerini gerçekleştirmek. Client tarafından gönderilen datayı, işleyecek olan sınıf gibi düşünebilirsiniz, temsili. İsimlendirmeye takılmayın, siz istediğiniz gibi kullanabilirsiniz. :)

  • Merhaba,

    Uzun zamandır böyle bir proje arıyordum. Çok teşekkürler
    Şimdi 224 bilgisayar için uzaktan yönetilebilir bir program yazabilirim.

    Paylaşımlarını takip ediyor olucam Sağolasın

  • Gökhan kardeşim selam,

    Anlattığın konu o kadar güzel olmuş ki inan uzun zamandır böyle bir örnek arıyordum.
    Senden bir ricam olacak server kısmını windows form olarak tasarlar sak nasıl yapmamız gerekiyor.
    Amaç 224 bilgisayarda ayrı ayrı server çalıştıracağım client makineden gelen komuta göre windows form içinde yapması gerekenleri yazacağım fakat form tarafını çözemedim. Örnek mail atabilir sen sevinirim teşekkürer
    Tuncay G.

  • Merhaba Gökhan Abi,

    Anlatım çok güzel olmuş. 1 yıldır c# ile uğraşıyorum. bu projeyi windows olarak nasıl yapabilirim.
    Hocamız bir proje yapmamızı istedi bende havalı proje yapmak istiyorum Arduino ve c# ile uzaktan led kontrol uygulaması düşünüyorum. Labaratuvardaki iki bilgisayar arası soket ile bağlanıp bir bilgisayara bağlı olan arduino com üzerinden aldığı veriye göre led yakacak yada kapatacak. Arduino için bir abim yardımcı olacak.

    size simdiden teşekkürlerimi iletiyorum iyi çalışmalar

  • Gökhan hocam, çok güzel bir konuya değinmişsiniz , emeğinize sağlık. Bir sorum olacak. Uygulama çok güzel ama Server üzerinden istediğim client a nasıl mesaj gönderebilirim. Ya da aynı anda bağlı client lara nasıl toplu mesaj gönderebilirim.

  • Merhaba,
    Benim için faydalı bir paylaşım oldu. Bir sorum olacak. SendData metodunun geri dönüşü yok. SendData ile gönderdiğim veriyi sunucu tarafından işlendikten sonra geri almak istiyorum. Bunu nasıl yapabilirim?

Recent Posts

Containerized Uygulamaların Supply Chain’ini Güvence Altına Alarak Güvenlik Risklerini Azaltma (Güvenlik Taraması, SBOM’lar, Artifact’lerin İmzalanması ve Doğrulanması) – Bölüm 1

{:tr}Bildiğimiz gibi modern yazılım geliştirme ortamında containerization'ın benimsenmesi, uygulamaların oluşturulma ve dağıtılma şekillerini oldukça değiştirdi.…

8 ay ago

Identity & Access Management İşlemlerini Azure AD B2C ile .NET Ortamında Gerçekleştirmek

{:tr}Bildiğimiz gibi bir ürün geliştirirken olabildiğince farklı cloud çözümlerinden faydalanmak, harcanacak zaman ve karmaşıklığın yanı…

1 yıl ago

Azure Service Bus Kullanarak Microservice’lerde Event’ler Nasıl Sıralanır (FIFO Consumers)

{:tr}Bazen bazı senaryolar vardır karmaşıklığını veya eksi yanlarını bildiğimiz halde implemente etmekten kaçamadığımız veya implemente…

2 yıl ago

.NET Microservice’lerinde Outbox Pattern’ı ile Eventual Consistency için Atomicity Sağlama

{:tr}Bildiğimiz gibi microservice architecture'ına adapte olmanın bir çok artı noktası olduğu gibi, maalesef getirdiği bazı…

2 yıl ago

Dapr ve .NET Kullanarak Minimum Efor ile Microservice’ler Geliştirmek – 02 (Azure Container Apps)

{:tr}Bir önceki makale serisinde Dapr projesinden ve faydalarından bahsedip, local ortamda self-hosted mode olarak .NET…

2 yıl ago