Makale Özeti

Herkese merhabalar. Bu makale birkaç makalelik bir dizinin ilk makalesidir. Bu makaleler sonunda IRC tarzı basit bir Chat sistemi geliştireceğiz (MIRC kullanmış olanlar IRC'nin ne demek olduğunu daha iyi anlayacaktır). Sunucu/İstemci mimarisine göre geliştirilecek olan sistemde sunucu ile istemci arasındaki iletişim TCP protokolü üstünden gerçekleşecek, sunucuya birçok istemci aynı anda bağlanacak ve istemciler gerek toplu gerekse özel olarak birbirlerine mesaj atabileceklerdir. Makaleler boyunca soket programlama ve multi-thread programlama teknikleri açıklanacak ve bunlardan yararlanılacaktır.

Makale

Yukarıdaki şekilde, oluşturulacak sistemin genel şeması vardır (IP Adresi ve PORT değerleri gelişigüzel seçilmiştir). Sistem katmanlı bir mimaride tasarlanacaktır. Şekilde Chat Sunucu ve Chat İstemci modülleri chat mantığının gerçekleştirilmesinden sorumluyken, ASMES ('Asenkron Soketli Mesajlaşma Sistemi' anlamına gelir) İstemci ve Sunucu kütüphaneleri ise TCP protokolü ile mesajların sunucu/istemci arasında internet üzerinden aktarımından sorumlu olacaktır.

Makalelerin nihai amacı multi-thread bir soket sunucu ve istemci çifti geliştirmeyi açıklamak ve genel bir soket kütüphanesi geliştirmektir. Dolayısıyla bu projeyle beraber, başka projelerde de TCP soket altyapısı olarak kullanılabilecek ASMES adlı genel bir kütüphane de geliştirmiş olacağız.

Bu ilk makalede TCP soket programcılığına giriş yapılacak ve projemizden bağımsız olarak, basit bir sunucu/istemci modelinde iletişimin nasıl sağlandığı ayrıntılı, teorik ve pratik olarak incelenecektir.

TCP nedir?

TCP/IP protokolü aslında TCP, IP, ARP, UDP, HTTP ve FTP gibi onlarca protokolü bünyesinde bulunduran katmanlı bir protokol kümesidir. IP ve TCP protokolleri bunların en önemlileri olduğu için TCP/IP ismi verilmiştir. Bizim kullanacağımız TCP (Transmission Control Protocol) protokolü bu katmanlı yapının Taşıma katmanında bulunarak, uygulamalar için güvenlilir (reliable), bağlantılı (connected), sıralı (ordered) bir veri akışı (data stream) sağlar. Bu kavramların her biri bilgisayar alanında önemlidir ve TCP'yi UDP'den ayıran temel özelliklerdir. UDP, taşıma katmanında bulunan diğer protokoldür. UDP (User Datagram Protocol) bağlantısız çalışır (gönderilen UDP paketleri birbirinden bağımsızdır), güvenli değildir (paket kaybı yaşanabilir), sıralı değildir (gönderildiği sırada karşı tarafa ulaşmayabilir). UDP'den TCP'ye göre en büyük avantajı daha hızlı olmasıdır. Ancak bir chat sistemi geliştireceksek mesajların kaybolmasını ya da yanlış sırada iletilmesini istemeyiz. Bu yüzden TCP kullanıyoruz. .NET framework gerekli tüm kütüphaneleri bize sağlayarak kolayca TCP protokolünü kullanmamıza yardımcı olur.

TCP protokolünde bir uygulamayı tanımlayan (adresleyen) iki bileşen vardır: IP ve PORT. IP, uygulamanın çalıştığı makinenin IP adresi, PORT ise bu makine üzerinde bu uygulamaya tahsis edilmiş bir sanal iletişim kanalıdır. Her uygulama bir port ile ilişkilendirilir. Böylece o makineye gelen verilerin hangi uygulamaya teslim edileceği bu port bilgisine bakılarak gerçekleşir. Port aslında 0 ile 65535 arasındaki bir sayıdan başka bir şey değildir. Bazı uygulamalar için bu port değeri sabittir. Örneğin FTP uygulamaları 21, HTTP server uygulamaları 80 numaralı portu kulanır (dinler). 0-1023 arasındaki portlar özel kullanıma ayrılmış portlardır. Bunların tam listesi internette kolayca bulunabilir. Kendi yazacağımız uygulamalarda bu ayrılmış portları kullanmayız.

Sunucu/İstemci (Server/Client) modeli nedir?

Sunucu/İstemci modeli, bilgisayarlar arasında veri iletişimini sağlamak amacıyla geliştirilmiş bir iletişim yöntemidir. Bu yöntemde en basit haliyle, bir makine üzerinde çalışan bir sunucu uygulama, kendisine bağlanan aynı ya da farklı makinelerdeki istemci uygulamalara hizmet verir. Bu hizmet, web mantığında oldu gibi tek yönlü Talep/Cevap (Request/Response) şeklinde olabilir. TCP kullanarak geliştirilen bir sunucu/istemci modelinde ise sunucu ile istemci arasında aşağıdaki şekildeki gibi çift taraflı bir veri akışı vardır. Sunucu veya istemci her an birbirlerine veri gönderip alabilirler. Veriler byte akışı olarak iletilir. Sunucu, kendisine bağlanan her istemci için ayrı bir soket bağlantısı kurar.

.NET TCP soket programcılığı

.NET framework kullanılarak TCP sunucu/istemci uygulamaları geliştirmek için temelde System.Net ve System.Net.Sockets namespace'lerinden faydalanılır. Ayrıca veri akışını sağlamak için akış nesnelerini içeren System.IO namescape'inde tanımlanan sınıflardan yararlanılır.

Artık bu genel bilgilerden sonra Sunucu/İstemci modelinin tasarlanmasına ve gerçeklenmesine geçebiliriz.

Sunucu Tarafı

Öncelikle kodlarımızı çalıştırabilmek için Visual Studio.NET'de yeni bir console application oluşturalım. Sunucu tarafındaki uygulama iki temel görevi yerine getirir:

1) Kendisine bağlantı kurmak isteyen istemcileri dinleme için bir port açar ve istemcilerin bağlantı isteklerini gerçekleştirir.

2) Bağlantı sağlanan istemcilerle veri alışverişi gerçekleştirir.

Buradan anlaşıldığı gibi sunucu, istemcilerin bağlantı isteklerini karşılamak için bir port'u sürekli dinlemek durumundadır. .NET'de bir portu dinlemeye başlamak için aşağıdaki gibi bir kod yazılır:

   13  TcpListener dinleyiciSoket = new TcpListener(System.Net.IPAddress.Any, 10048);

   14  dinleyiciSoket.Start();

İlk satırda gelen bağlantı isteklerini dinlemek için TcpListener sınıfından bir nesne oluşturuluyor. Dinlenen IP adresi olarak o makine üzerindeki tüm network kartları seçilmiş (bu parametre şimdilik önemli değil), dinleme portu olarak da 10048 tercih edilmiş. İkinci satırda da dinleme işlemi başlatılıyor. Dinlenen port'a yapılan istekleri kabul etmek içinse TcpListener sınıfının AcceptSocket metodu kullanılır. Bu method sunucuya bir istemci bağlanana kadar bekler, bağlanma isteği geldiğinde istemciyle bir TCP bağlantısı kurar ve istemciyle veri iletişimini gerçekleştirmek için bir Socket nesnesi döner. İstemci ile gerçekleştirilecek tüm veri iletişimi için bu Socket nesnesi kullanılır. Neticede kodumuz aşağıdaki hale gelmiş oldu:

   12  //Portu'u Dinlemeyi başlat

   13  TcpListener dinleyiciSoket = new TcpListener(System.Net.IPAddress.Any, 10048);

   14  dinleyiciSoket.Start();

   15  //Bağlanan bir istemciyi kabul et ve bağlantıyı oluştur

   16  Socket istemciSoketi = dinleyiciSoket.AcceptSocket();

Bu programı çalıştırdığımızda boş bir console ekranı açılır (Çok küçük ihtmal olasa da, eğer 10048 portu başka bir ugulama tarafından kullanılıyorsa bir exception alabilirsiniz. Bu durumda port'u değiştrip yeniden deneyin). Gerçekten dinleme işleminin yapıldığını ispatlamak için Başlat'dan çalıştır'a cmd yazıp komut satırını açtıktan sonra komut satırına şu komutu yazıp çalıştıralım:

netstat -ano

Bu komut o anda o makine üzerinde açık olan tüm portların listesini verecektir.

Görüldüğü gibi 10048 numaralı bir TCP portu LISTENING (dinleme) durumundadır. Çalıştırdığımız programı kapatıp yeniden bu komutu kullandığımızda bu satırın olmadığını görülür.

Şimdiye kadar bir TCP portu açıp dinledik ve bağlantıları kabul ettik. Şimdi kabul ettiğimiz bir bağlantıyla nasıl veri iletişimi gerçekleştireceğimizi gösterelim. Veri iletişimi için Socket sınıfının Receive (almak için) ve Send (göndermek için) fonksyonları vardır. Ancak System.IO namespace'inde tanımlanan akış sınıfları veri gönderme/alma işlemlerini kolaylaştırır. Biz makalemizde NetworkStream, BinaryReader ve BinaryWriter sınıflarından faydalanacağız. System.IO namespace'ini kodumuza dahil ettikten sonra aşağıdaki kodları yazabiliriz.

   19  NetworkStream agAkisi = new NetworkStream(istemciSoketi);

   20  BinaryReader binaryOkuyucu = new BinaryReader(agAkisi);

   21  BinaryWriter binaryYazici = new BinaryWriter(agAkisi);

Öncelikle Socket nesnemizle ilişkili bir ağ akış nesnesi oluşturuyoruz. Daha sonra bu akışa yazmayı kolaylaştıran BinaryReader ve BinaryWriter sınıflarından birer nesne oluşturuyoruz. Artık veri almak için binaryOkuyucu, veri göndermek içinse binaryYazici nesnelerini kullanabiliriz. Şimdi basit bir örnek yapalım. Örneğimizde sunucu uygulamamız soket'den bir string metin okusun. Bu metin 'selam' ise istemciye 'sana da selam' metnini gönderelim, değilse 'selam vermeyenle konusmam' metnini gönderelim. En son da soketleri kapatalım. Kodumuzun son hali aşağıdaki gibi oldu.

    1 using System;

    2 using System.IO;

    3 using System.Net;

    4 using System.Net.Sockets;

    5 

    6 namespace SunucuUygulamasi

    7 {

    8     class Program

    9     {

   10         static void Main(string[] args)

   11         {

   12             //Portu'u Dinlemeyi başlat

   13             TcpListener dinleyiciSoket = new TcpListener(System.Net.IPAddress.Any, 10048);

   14             dinleyiciSoket.Start();

   15             //Bağlanan bir istemciyi kabul et ve bağlantıyı oluştur

   16             Socket istemciSoketi = dinleyiciSoket.AcceptSocket();

   17             //Akış nesnelerini oluştur

   18             NetworkStream agAkisi = new NetworkStream(istemciSoketi);

   19             BinaryReader binaryOkuyucu = new BinaryReader(agAkisi);

   20             BinaryWriter binaryYazici = new BinaryWriter(agAkisi);

   21             //Bir string değer oku

   22             string alinanMetin = binaryOkuyucu.ReadString();

   23             //okunan string'e göre bir işlem yap

   24             if (alinanMetin == "selam")

   25             {

   26                 binaryYazici.Write("sana da selam");

   27             }

   28             else

   29             {

   30                 binaryYazici.Write("selam vermeyenle konusmam");

   31             }

   32             //soketleri kapat

   33             istemciSoketi.Close();

   34             dinleyiciSoket.Stop();

   35         }

   36     }

   37 }

BinaryReader ve BinaryWriter sınıfları .NET içerisinde tanımlı temel değişken tiplerinde (Byte, String, Integer.. v.s.) değer okuyup yazabilir. Şimdilik biz basit olması açısından string bir değer alıp gönderdik. Kodumuzu çalıştırdığımızda program aşağıdaki satırda bekler:

   16             Socket istemciSoketi = dinleyiciSoket.AcceptSocket();

Bir istemci, sunucuya bağlanana kadar da kodun çalışması devam etmez. Çünkü AcceptSocket fonksyonu bloklamalı bir fonksyondur.

Bu örneği çalıştırıp sonucunu görebilmek için bir de istemci uygulamaya ihtiyaç vardır. Aksi halde uygulamamızı test edemeyiz. Bu yüzden önce istemci tarafını anlatıp gerekli kodları yazalım, sonra iki uygulamayı da test edelim.

İstemci tarafı

Sunucu tarafında yaptıklarımıza bakacak olursak istemcinin de genel olarak iki işlem gerçekleştirdiği anlaşılır:

1) Sunucuya bağlantı isteği göndermek

2) Bağlantı kurulduktan sonra veri alışverişi gerçekleştirmek

İstemci uygulamamızı geliştirmek için yine Visual Studio.NET'de bir console uygulaması oluşturalım. Bu sefer tüm kodları bir arada verip açıklamaları ardından yapalım.

    1 using System;

    2 using System.Net;

    3 using System.Net.Sockets;

    4 using System.IO;

    5 

    6 namespace IstemciUygulamasi

    7 {

    8     class Program

    9     {

   10         static void Main(string[] args)

   11         {

   12             //Soket bağlantısını oluştur

   13             Socket istemciBaglantisi = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

   14             //Bağlantıyı gerçekleştir

   15             istemciBaglantisi.Connect(IPAddress.Parse("127.0.0.1"), 10048);

   16             //vberi iletişimi için akış nesnelerini oluştur

   17             NetworkStream agAkisi = new NetworkStream(istemciBaglantisi);

   18             BinaryReader binaryOkuyucu = new BinaryReader(agAkisi);

   19             BinaryWriter binaryYazici = new BinaryWriter(agAkisi);

   20             //sunucuya bir string metin yolla

   21             binaryYazici.Write("selam");

   22             //Sunucudan bir string metin oku

   23             string alinanMetin = binaryOkuyucu.ReadString();

   24             //alınan metini ekranda göster

   25             Console.WriteLine(alinanMetin);

   26             //Bağlantıyı sonlandır

   27             istemciBaglantisi.Close();

   28             //Enter'a basılana kadar bekle

   29             Console.ReadLine();

   30         }

   31     }

   32 }

Yukarıdaki kodda zaten her satırda gerekli açıklamalar var. Öncelikle sunucuyla iletişim için bir Socket nesnesi oluşturuluyor, daha sonra "127.0.0.1" IP adresli makinanın 10048 numaralı portunu dinleyen programa bağlanmaya çalışılıyor. "127.0.0.1" o anda üzerinde çalışılan makinanın kendisini temsil eder ve LoopBack adres olarak bilinir. Sunucu uygulama yerel bilgisayarımızda çalıştığı sürece bu şekilde bir IP adresi kullanabiliriz, başka makinede çalıştığında o makinenin gerçek IP adresi buraya yazılır. Dikkat ederseniz 10048 portu da sunucu uygulamasında yazdığımız port ile aynıdır. Bu aşamada programı çalıştırdığınızda eğer sunucu uygulama çalışır durumda değilse, istemci programınız bir exception fırlatacaktır. Çünkü bağlanılmaya çalışılan uygulama (yani 10048 portunu dinleyen herhangi bir program) bulunamamıştır. Kodu test etmek için önce sunucuyu ardından istemciyi çalıştırmak gerekir.

Bağlantı sağlandıktan sonra yine akış nesneleri oluşturulup karşı tarafa bir string gönderiliyor. Daha sonra bir string okunuyor ve ekranda gösteriliyor.

Önce sunucu programı, ardından istemci programı çalıştırdığınızda iki olay meydana gelir. Öncelikle istemci penceresi aşağıdaki şekildeki gibi olacaktır:

Program kodu içerisinde geçen 'selam' yerine başka bir metin girildiğinde ise sunucu 'selam vermeyenle konusmam' diyecektir.

İstemciyi çalıştırdığınızda ikinci gerçekleşen olay da sunucu programın kapanmasıdır. Hatırlarsanız sunucu uygulamamız çalıştığında AcceptSocket metodunda kalıyordu. Bir istemci bağlandığında bu satır geçilir ve sunucu program çalışmasına devam eder ve en sonunda da kapanır.

Şimdi tekrar sunucu programa dönüp biraz değişiklik yapalım ki olayı daha net görelim. Sunucu programının son hali şöyle olsun:

    1 using System;

    2 using System.IO;

    3 using System.Net;

    4 using System.Net.Sockets;

    5 

    6 namespace SunucuUygulamasi

    7 {

    8     class Program

    9     {

   10         static void Main(string[] args)

   11         {

   12             //Portu'u Dinlemeyi başlat

   13             TcpListener dinleyiciSoket = new TcpListener(System.Net.IPAddress.Any, 10048);

   14             dinleyiciSoket.Start();

   15             //Bağlanan bir istemciyi kabul et ve bağlantıyı oluştur

   16             Console.WriteLine("Bir istemcinin bağlanmasını bekliyor..."); //***

   17             Socket istemciSoketi = dinleyiciSoket.AcceptSocket();

   18             //Akış nesnelerini oluştur

   19             NetworkStream agAkisi = new NetworkStream(istemciSoketi);

   20             BinaryReader binaryOkuyucu = new BinaryReader(agAkisi);

   21             BinaryWriter binaryYazici = new BinaryWriter(agAkisi);

   22             //Bir string değer oku

   23             string alinanMetin = binaryOkuyucu.ReadString();

   24             //Okunan string'i ekrana yaz //***

   25             Console.WriteLine("istemci şunu yazdı: " + alinanMetin); //***

   26             //okunan string'e göre bir işlem yap

   27             if (alinanMetin == "selam")

   28             {

   29                 binaryYazici.Write("sana da selam");

   30             }

   31             else

   32             {

   33                 binaryYazici.Write("selam vermeyenle konusmam");

   34             }

   35             //soketleri kapat

   36             istemciSoketi.Close();

   37             dinleyiciSoket.Stop();

   38             //Bir tuşa basana kadar bekle //***

   39             Console.WriteLine("programdan cikmak icin bir tusa basiniz..."); //***

   40             Console.ReadKey(); //***

   41         }

   42     }

   43 }

Sonuna *** konulan satırlar yeni eklendi. Tek yapılan şey, olan bazı olayları ekrana yazdırmaktır. Şimdi sırayla sunucu ve istemciyi çalıştırdığınızda aşağıdaki gibi iki ekranla karşılaşırsınız.

TCP Veri alışverişi

Önceki örnekte doğrudan string gönderip almamıza rağmen, TCP protokolünde sunucu ile istemci arasındaki veri iletişiminin temelde 'sıralı byte akışı' şeklinde olduğunu belirttik. Bu şekilde bir akışı yazılımsal olarak yönetmek çok basit değildir. Öncelikle gönderdiğiniz verileri birbirinden ayırmanız için kendi yazılımınıza özgü başlangıç/bitiş byte'ları göndermeniz ve karşı tarafta bu byte'lara bakarak mesajları birbirinden ayırmanız (ya da benzer başka bir mantık kullanmanız) gerekir. Örneğin 'selam' ve 'naber' metinlerini sunucuya yollamak istediğimizi düşünelim. Veri iletişimi aşağıdaki şekildeki gibi gerçekleşir.

Dikkat edilirse burada bir problem ortaya çıkar; sunucu selam ve naber mesajlarını birbirinden ayıramaz. Ayrıca sunucu mesajı alırken sırayla 's','e','l','a'... şeklinde byte'ları alır. Bunları birleştirip bir metine dönüştürmesi gerektiği için metnin nerde başlayıp nerde bittiğini bilmesi gerekiyor. Örneğin mesajı aşağıdaki biçimde gönderdiğimizde bu sorunlar çözülmüş olur:

<selam><naber>

Çok iyi bir yöntem olmasa da bu yöntem aslında işimizi görüyor. Sunucu < işaretini aldığı zaman bir metin okumaya başlayacağını anlar ve > işaretini görene kadar aldığı tüm byte'ları (harfleri) birleştirir. Bir başka < işareti gördüğünde ikinci bir mesaj aldığını anlar. Bu yöntemin en büyük dezavantajı, mesaj metinleri içerisinde < veya > işareti geçtiğinde sunucunun kafası karışır ve mesajları yanlış yorumlar. Bunu engellemek için ya bu işaretleri özel bir metne dönüştürüp yollamalıyız ya da bu işaretleri mesajlarda kullanmamalıyız. Basit olması açısından ikinci çözümü seçiyoruz. Chat yazılımımızda bu işaretler kullanılamayacak.

Yukarıdaki örnek uygulamada aslında (BinaryReader sınıfının) ReadString metodu ve (BinaryWriter sınıfının) Write methodu tüm bu durumları bertaraf ederek güvenli string iletişimi sağlıyor. Ancak biz projemizde belli bir aşamaya kadar bu işlemleri manuel yapacağız. Şimdi yukarıdaki < ve > mantığına göre TCP üzerinden bir string gönderen fonksyonu inceleyelim:

   32 public bool MesajYolla(string mesaj)

   33 {

   34     try

   35     {

   36         //Mesajı byte dizisine çevirelim

   37         byte[] bMesaj = System.Text.Encoding.ASCII.GetBytes(mesaj);

   38         //Karşı tarafa gönderilecek byte dizisini oluşturalım

   39         byte[] b = new byte[bMesaj.Length + 2];

   40         Array.Copy(bMesaj, 0, b, 1, bMesaj.Length);

   41         b[0] = 60; // <

   42         b[b.Length - 1] = 62; // >

   43         //Mesajı sokete yazalım

   44         binaryYazici.Write(b); // BinaryWriter nesnesi

   45         agAkisi.Flush(); // NetworkStream nesnesi

   46         return true;

   47     }

   48     catch (Exception)

   49     {

   50         return false;

   51     }

   52 }

Hedefe 'selam' kelimesini göndermek istediğimizde aslında <selam> göndermemiz gerektiğini biliyoruz. Yukarıdaki kodda önce mesaj string'i byte dizisine çevriliyor, sonra bu dizinin boyutundan 2 fazla bir dizi oluşturuluyor ve başına < (ASCII kodu 60), sonuna > (ASCII kodu 62) arasına da mesaj'ı oluşturan byte dizisi yazılıyor. Son olarak da oluşturulan byte dizisi hedefe gönderiliyor. Write fonksyonu tamponlu olduğu için (mesajı derhal göndermek yerine optimizasyon açısından bir süre bekletebilir) Flush metodunu çağırarak, o ana kadar tampona yazılmış verileri gönderiyoruz.

Yukarıdaki fonksyonun gönderdiği byte stream'i alıp, içerisinden string'leri ayırt eden kodumuz ise aşağıdaki gibi olacaktır.

   34 //Başlangıç Byte'ını oku

   35 byte b = binaryOkuyucu.ReadByte();

   36 if (b == 60) // <

   37 {

   38     //Mesajı oku

   39     List<byte> bList = new List<byte>();

   40     while ((b = binaryOkuyucu.ReadByte()) != 62) // >

   41     {

   42         bList.Add(b);

   43     }

   44     string mesaj = System.Text.Encoding.ASCII.GetString(bList.ToArray());

   45 }

Görüldüğü gibi bir byte okunuyor, eğer bu < ise, > işaretini alana kadar alınan bütün byte'lar bir listeye atılıyor, daha sonra bu liste byte dizisine, en son da string'e çeviriliyor.

Sonuç

Kodlamalar ve byte dizisi ile string arasındaki dönüşümler ASCII standart'ına göre yapıldığı için Türkçe karakterleri gönderip alırken problem yaşanır. Neticede bu yöntem hem zor hem de çok kullanışlı değildir ancak TCP soket programcılığını en temelinde gerçekleştirmek istediğimiz için bu şekilde yapacağız. Daha ilerde çok daha basit, güvenli ve kullanışlı yöntemlerle veri iletişimini göstereceğiz.

Makale içinde geçen bilmediğiniz terimleri ya da tasarım metodlarını başta wikipedia.org adresinde olmak üzere arayıp, dökümanları inceleyebilirsiniz. Bir işin teorisini de çok iyi bilince gerçekten o konuya hakim olabilir ve kendi yöntemlerinizi geliştirebilirsiniz. Sadece örnekler üzerinden öğrenmek sizi her zaman sınırlar, örnekteki yöntemlerin dışına çıkmanız zor olur.

Bir sonraki makalemizde daha hızlı ilerleyerek önce sürekli açık kalabilen ve sırayla tüm istemcilere cevap verebilen bir sunucu, daha sonra da birçok istemciye eşzamanlı cevap verebilen bir sunucu tasarlayıp gerçekleştireceğiz. Son yazdığımız gönderme ve alma kodlarını yazarak programınızı değiştirmeye çalışmayın, gelecek makalede bu kodları yeniden ele alıp kullanacağız.

Halil İbrahim Kalkan
halilibrahimkalkan@yahoo.com