Makale Özeti

Bu makalede TCP soket programlamanın ayrıntılarına girerek çok katmanlı (mutli tier/layer), çok thread'li (mutli thread) bir sunucu/istemci mimarisi tasarlamayı açıklayacağız. Neticede herhangi bir uygulamadan bağımsız ve her uygulamaya kolayca entegre olabilecek ASMES adında TCP soketleri üzerinden iletişim yapan bir kütüphane geliştirmiş olacağız.

Makale

Bu makale 'TCP Socket Programcılığına Giriş' isimli birinci makalemin devamıdır. Birinci makalede TCP protokolü ve .NET framewrok kullanacak basitçe TCP üzerinden veri alışverişi açıklanmıştı. Eğer soket programlama konusunda hiç tecrübeniz yoksa öncelikle birinci makalemi okumalısınız.

Neden Multi-Thread?

Thread, bir program içerisinde farklı kod kısımlarını aynı zamanda çalıştırılmasının bir yoludur [1]. Bu şekilde birden çok işlem aynı zamanda yapılarak özellikle Giriş/Çıkış (I/O) ve Hesaplama (CPU) işlemlerinin karışık kullanıldığı durumlarda performans ciddi bir şekilde artırılır. Birden çok thread çalıştığı durumda thread'ler değişkenleri ve diğer nesneleri ortak kullanabilir, bu şekilde threadler arasında veri alışverişi gerçekleştirilir.

Bir .NET projesi başlangıçta tek bir thread'den oluşur. Bu yüzden bir kod parçası çalışırken diğer kodlar kendi sıralarının gelmesini bekler. Örneğin bir önceki makaleden hatırlarsanız TCPListener sınfının AcceptSocket metodu bloklamalı bir fonksyon olduğu için program o satırda kalıyor, devam etmiyordu. Ancak biz istiyoruz ki bir istemcinin bağlanmasını beklerken aynı anda başka bir istemciyle veri alışverişi yapabilelim, hatta birçok istemciyle aynı anda veri alışverişi yapabilelim.

Soket programlama (hatta genel olarak herhangi bir I/O işlemi) yaptığımızda genelde bloklamalı fonksyonlarla karşı karşıya kalırız. TCPListener sınfının AcceptSocket fonksyonu, BinaryReader sınıfının ReadByte fonksyonu bloklamalıdır. AcceptSocket fonksyonu yeni bir istemci bağlanmadığı sürece çalışmaya devam etmez, ReadByte fonksyonu ise ilgili istemciden bir byte veri gelmediği sürece çalışmaya devam etmez.

Yukarıda anlatılanlar çerçevesinde bir sunucu programında birden çok istemciye aynı anda hizmet verebilmek için programın multithread yazılmasının kaçınılmaz olduğu görülür. Bu makalede geliştirilecek sistemde yeni gelen istemcileri kabul etmek için bir thread, her istemciyle veri alışverişi yapmak için de ayrı bir thread kullanılacak. Böylece aynı anda 5 istemci sunucuya bağlı ise sunucu programımızda 1+5=6 thread (bir de başlangıçtaki ana thread olduğu için aslında 7 thread) eşzamanlı çalışacaktır.

Multithread programlama kendi başına bir kitap olabilecek kadar geniş ve çok fazla teknik içeren bir konudur. Bu makalede sadece gerektiği kadar gösterilecek ve detayına girilmeyecektir. Yani multithread programlama deneyiminiz olmasa da makaleyi anlayabilirsiniz, ancak kendinizi geliştirmeniz ve konuya daha iyi hakim olabilmeniz için öğrenmeniz faydalı olur.

Neden Çok Katmanlı mimari?

Çok Katmanlı mimari (Multi Layer Architecture), modüler programlanın en iyi örneklerindendir. Bu yöntemde aşağıdaki şekildeki gibi birbirinin üzerinde çalışan katmanlar sadece kendilerinin bir üstündeki ve bir altındaki katmanla iletişim halindedirler.

Yukarıdaki şekilde Katman 1'deki kodlar sadece Katman 2 ile iletişim kurar (yani Katman 2'deki fonksyonları çağırabilir veya katman 2 tarafından kendi fonksyonları çağrılabilir). Böylece Katmanlar birbirinden soyutlanmış olur. Genelde üstteki katman alttaki katmana bağımlıdır ancak alttaki katman üstteki katmandan bağımsızdır, üstteki katmanı tanımaz. Üstten alta iletişim interface ya da doğrudan belirli bir sınıfın fonksyonunu çağrırarak olurken alttan üste doğru iletişim interface ya da event'lar ile olur (yani alttaki katmanı kullanan üst katman ya belli bir interface'i uygular (implements) ya da belli olaylara kaydolur).

Bu modelle soyutlama ve basitleştirme de sağlanabilir. Örneğin katman 1 katman 2'nin bir fonksyonunu çağırdığında katman 2 katman 3 üzerinde birden fazla işlem yapıyor olabilir. Bu şekilde katman 1'in doğrudan katman 3'ü kullanması daha zorken, katman 2 vasıtasıyla bir basitleştirme ve soyutlama yapılmış olur.

Internet dünyasındaki en popüler olan iletişim modeli 'OSI referans modeli' 7 katmanlı bir yapıda tasarlanmıştır[2]. TCP/IP protokolü tam olarak OSI modeline uymasa da, daha basit olan Dod[3] modelinde katmanlı bir protokol kümesidir (TCP protokolünün bu katmanlı yapının taşıma katmanında bulunduğunu daha önceki makalede anlatmıştık).

Neticede günümüzde neredeyse tüm güçlü yazılımlar ve yazılım teknikleri katmanlı olarak tasarlanmaktadır ve gerçekleştirilmektedir. Biz de tasarlayacağımız ASMES kütüphanesini katmanlı yapıya uygun olacak şekilde tasarlayacağız. Böylece bu katman üzerine geliştireceğimiz chat projemiz TCP soketlerini doğrudan hiç kullanmadan, sadece ASMES kütüphanesini kullanarak geliştirilecektir.

ASMES Kütüphanesi

ASMES bu projeye tamamen kendi verdiğim bir isimdir. Yazılım dünyasında geliştirilen projelere genelde bu şekilde kısaltma isimler verildiği için ben de bu geliştirilecek kütüphaneye 'Asenkron Soketli Mesajlaşma Sistemi'nin kısaltması olan ASMES ismini verdim. ASMES, herhangi bir uygulama için TCP soketleri kullanarak iletişim altyapısını sağlayabilecek bir kütüphanedir. Aşağıdaki şekil bir önceki makalede de olan, ASMES'in katmanlı yapıda nerede bulunduğunu gösteriyor.

Şekilde görüldüğü gibi sunucu ve istemcide çalışan mantık (bir önceki makaleden hatırlayacağınız gibi) birbirinden farklı olduğu için sunucuda ve istemcide ayrı katmanlar çalışır. Aslında projemizi tek bir kütüphane olarak tasarladık ancak sunucuda ve istemcide kullanılacak sınıflar tabii ki farklıdır. Yukarıdaki şekil de bunu ifade ediyor.

Bu makalede öncelikle sunucu tarafındaki kodların, daha sonra istemci tarafındaki kodlarının nasıl geliştirildiği anlatılacak, ardından bu kütüphaneyi kullanan çok basit bir örnek yapılacak. Bir sonraki makalede ise bu kütüphanenin üstüne daha gelişmiş bir chat (IRC) sistemi geliştireceğiz.

ASMES kütüphanesinde sunucu ile istemci arasında gönderilen mesajlar ASCII standartında string metinlerden ibaret olacaktır. Makaleyi okurken kodları bakarak yazmak yerine kodların tamamını indirip bir yandan inceleyip bir yandan makaleyi baştan sonra dikkatle okursanız daha iyi anlaşılacaktır çünkü bu makalede kodların tamamı yerine en önemli kısımları açıklanacaktır.

Sunucu tarafı

Sunucu tarafında çalışan 'ASMES Sunucu katmanı' aşağıdaki görevleri yerine getirsin istiyoruz:

1) Sunucu Baslat fonksyonu ile başlatatılsın ve istediğimizde Durdur fonksyonu ile durdurulsun.
2) Sunucuya bir istemci bağlandığında YeniIstemciBaglandi adında bir olayı tetikleyerek bize haber versin.
3) Sunucuya bağlı bir istemci, bağlantıyı kapattığında IstemciBaglantisiKapatildi adında bir olay tetikleyerek bize haber versin.
4) Sunucuya bağlı bir istemci sunucuya bir mesaj gönderdiğinde IstemcidenYeniMesajAlindi adında bir olay tetikleyerek bize haber versin.
5) Sunucuya bağlı bir istemciye bir mesaj göndermek için MesajYolla adında bir fonksyon olsun.
6) Sunucuya bağlı olan istemcileri birbirinden ayırt etmek için bir yöntem sağlasın. Böylece bir istemciye mesaj göndermek istediğimizde, istediğimiz istemciyi seçebilelim veya bir istemciden mesaj geldiğinde hangi istemciden geldiğini anlayabilelim.

Yukarıdaki maddeler aslında yukarıdaki şekildeki 'Chat Sunucusu' katmanı ile 'ASMES Sunucu kütüphanesi' katmanı arasında nasıl bir iletişim sağlanacağını gösteriyor. Bizim bu makalede amacımız ise ASMES Sunucu Kütüphanesi katmanının TCP protokolü üzerinden istemcilerle nasıl bir iletişim sağlayacağını göstermek. Bunu göz önünde bulundurarak aşağıdaki şekli inceleyelim:

'Neden mutli-thread' kısmında anlatıldığı gibi ASMESSunucusu içerisinde her istemci için bir thread ve yeni gelen istemci bağlantılarını dinlemek için de ayrı bir thread çalışacak. Yazılımsal olarak her bir istemciyi temsil eden Istemci adında bir sınıf ve bağlantıları dinleyen BaglantiDinleyici adında bir sınıf vardır. BaglantiDinleyici nesnesi bir bağlantı sağladığında bu yeni istemci için bir Istemci nesnesi oluştulup yeni bir thread başlatılır. Bu iki sınıf ASMESSunucusu sınıfının altsınıfları olacak. Böylece ASMES sunucu katmanını da kendi içerisinde modüler tasarlamak istiyoruz. BaglantiDinleyici sınıfı, ASAMSunucusu sınıfı ve Istemci sınıfları aralarında nesne referanslarına yapılan fonksyon çağrıları ile iletişim kurarlar. En basitinden başlamak için öncelikle BaglantiDinleyici sınıfını inceleyelim.

Bağlantıları dinlemek ve kabul etmek

ASAMSunucusu sınıfı bir adet BaglantiDinleyici nesnesi içerir. BaglantiDinleyici sınıfı ayrı bir thread olarak çalışıp aşağıdaki görevleri yapabilmelidir:

1) Baslat ve Durdur fonksyonları sağlayarak bağlantı dinleme işleminin başlatılıp durdurulmasını sağlamak.
2) Dinleme işlemi sürdüğü sürece yeni gelen istemci bağlantılarını kabul edip, bağlantıyla açılan Socket nesnelerini ASAMSunucusu sınıfına iletmek.

BaglantiDinleyici sınıfını incelediğimizde kurucu fonksyon aşağıdaki gibidir:

  218             public BaglantiDinleyici(ASMESSunucusu sunucu, int port)

  219             {

  220                 this.sunucu = sunucu;

  221                 this.port = port;

  222             }

Sunucu parametresi, ASMESSunucusu sınıfı ile iletişim kurmak için bir referanstır ve BaglantiDinleyici oluşturulurken parametre olarak geçilir. Daha sonra yeni bağlantı geldiğinde bu referans üzerinden istemci bağlantısı sunucuya iletilir. Port parametresi ise dinlenecek TCP port numarasıdır. Dinleme işlemini başlatmak için Baslat fonksyonu kullanılır:

  228             public bool Baslat()

  229             {

  230                 if (baglan())

  231                 {

  232                     calisiyor = true;

  233                     thread = new Thread(new ThreadStart(tDinle));

  234                     thread.Start();

  235                     return true;

  236                 }

  237                 else

  238                 {

  239                     return false;

  240                 }

  241             }

Bu fonksyon öncelikle baglan fonksyonunu çağırarak TCPListener soketini dinleme için açar, sonra yeni bir thread oluşturur ve başlatır. Bağlantı başarılıysa true döner, bağlanamazsa false. Thread'in giriş fonksyonu olarak tDinle verilmiş. Asıl işlemin gerçekleştiği fonksyon tDinle fonksyonudur:

  302             public void tDinle()

  303             {

  304                 Socket istemciSoketi;

  305                 while (calisiyor)

  306                 {

  307                     try

  308                     {

  309                         istemciSoketi = dinleyiciSoket.AcceptSocket();

  310                         if (istemciSoketi.Connected)

  311                         {

  312                             try { sunucu.yeniIstemciSoketiBaglandi(istemciSoketi); }

  313                             catch (Exception) { }

  314                         }

  315                     }

  316                     catch (Exception)

  317                     {

  318                         if (calisiyor)

  319                         {

  320                             //bağlantıyı sıfırla

  321                             baglantiyiKes();

  322                             //1 saniye bekle

  323                             try { Thread.Sleep(1000); }

  324                             catch (Exception) { }

  325                             //yeniden bağlantı kur

  326                             baglan();

  327                         }

  328                     }

  329                 }

  330             }

  331         }

tDinle fonksyonu BaglantiDinleyici kapatılana kadar sürekli bir döngü kurar ve bir önceki makalemizdeki gibi AcceptSocket metodu ile gelen bağlantıları kabul ederek, bağlantı başarılıysa sunucu nesnesine iletir (sunucu nesnesinin ait olduğu ASAMSunucusu sınıfı ilerde açıklanacaktır). Bir hata olduğunda ve bağlantı koptuğunda bir saniye bekleyerek yeniden dinleme işlemini başlatır. Yukarıdaki kodda kullanılan nesneler BaglantiDinleyici sınıfı içerisinde aşağıdaki gibi tanımlanmıştır:

  203             /** Gelen bağlantıların aktarılacağı modül */

  204             private ASMESSunucusu sunucu;

  205             /** Gelen bağlantıları dinlemek için sunucu soketi */

  206             private TcpListener dinleyiciSoket;

  207             /** Dinlenen portun numarası */

  208             private int port;

  209             /** thread'in çalışmasını kontrol eden bayrak */

  210             private volatile bool calisiyor = false;

  211             /** çalışan thread'e referans */

  212             private volatile Thread thread;

Baslat fonksyonunda kullanılan baglan fonksyonunun yaptığı tek şey TCPListener sınıfından bir nesne oluşturup başlatmaktır:

  270                     dinleyiciSoket = new TcpListener(System.Net.IPAddress.Any, port);

  271                     dinleyiciSoket.Start();

Son olarak BaglantiDinleyici sınıfı, sunucu kapatılırken dinleme işlemini de sonlandırmak için bir Durdur fonksyonu tanımlar. Bu fonksyon TCPListener nesnesi ve çalışan dinleme thread'ini durdurur. Ayrıntısı kodlar içerisinden incelenebilir.

BaglantiDinleyici sınıfında tDinle fonksyonu içerisindeki aşağıdaki koda dikkat edelim:

  309                         istemciSoketi = dinleyiciSoket.AcceptSocket();

Bu kod ASAMSunucusu sınıfına, yeni bağlanan istemciyle ilişkili Socket nesnesini aktarır. ASAMSunucusu sınıfı bu soketle iletişim kuran bir Istemci nesnesi oluşturup bir thread başlatmalı ve BaglantiDinleyici thread'i bloklamamalıdır ki tDinle içerisindeki döngü devam etsin ve istemci bağlantısı dinleme işi devam etsin. Bu kodları ASAMSunucusu sınıfını incelerken göreceğiz.

İstemcilerle iletişim kurmak

Daha önce bahsettiğimiz gibi, her istemci ile iletişim için ayrı bir thread çalışmalıdır. ASMESSunucusu içerisinde her istemci Istemci adlı sınıftan bir nesne ile temsil edilir. ASMESSunucusu sınıfı BaglantiDinleyici nesnesinden aldığı her yeni istemci bağlantısı için yeni bir Istemci nesnesi oluşturur ve başlatır. Bir Istemci nesnesi bir istemci soketi ile ilişkilidir ve ayrı bir thread olarak çalışarak aşağıdaki görevleri gerçekleştirir:

1) İlgili istemci soketinden gelen mesajları byte düzeyinde dinler, alır, birleştirir ve bir string haline getirip ASAMSunucusu sınıfına iletir, ayrıca YeniMesajAlindi olayını tetikler.
2) ASAMSunucusu tarafından ilgili istemciye gönderilmek istenen string mesajı byte dizisine çevirip soket üzerinden istemciye gönderir.
3) İstenildiği durumda bağlantıyı keser, ilgili soketi dinleyen thread'i sonlandırır ve BaglantiKapatildi olayını tetikler.

Istemci sınıfı aşağıdaki nesneleri kullanır:

  376             /// <summary>

  377             /// İstemci ile iletişimde kullanılan soket bağlantısı

  378             /// </summary>

  379             private Socket soket;

  380             /// <summary>

  381             /// Sunucuya referans

  382             /// </summary>

  383             private ASMESSunucusu sunucu;

  384             /// <summary>

  385             /// İstemciyi temsil eden tekil ID değeri

  386             /// </summary>

  387             private long istemciID;

  388             /// <summary>

  389             /// Veri transfer etmek için kullanılan akış nesnesi

  390             /// </summary>

  391             private NetworkStream agAkisi;

  392             /// <summary>

  393             /// Veri transfer etmek için kullanılan akış nesnesi

  394             /// </summary>

  395             private BinaryReader binaryOkuyucu;

  396             /// <summary>

  397             /// Veri transfer etmek için kullanılan akış nesnesi

  398             /// </summary>

  399             private BinaryWriter binaryYazici;

  400             /// <summary>

  401             /// İstemci ile iletişim devam ediyorsa true, aksi halde false

  402             /// </summary>

  403             private volatile bool calisiyor = false;

  404             /// <summary>

  405             /// Çalışan thread'e referans

  406             /// </summary>

  407             private Thread thread;

soket nesnesi BaglantiDinleyici tarafından tDinle fonksyonu içerisinde AcceptSocket metodu ile elde edilen Socket nesnesidir. Bu nesne önce ASMESSunucusu sınıfına, ordan da Istemci sınıfına aktarılır (nasıl aktarıldığı ileride açıklanacak). Sunucu nesnesi ASMESSunucusu sınıfı ile iletişim için kullanılan referanstır ve istemciden gelen mesajları bu sınıfa iletmek için kullanılır. istemciID ASMESSunucusu tarafından bu istemciye verilen tekil bir ID değeridir (bunun nerede yapıldığı ASMESSunucusu sınıfını açıklarken anlatılacaktır). agAkisi, binaryOkuyucu ve binaryYazici akış nesneleri bir önceki makalemizden hatırlayacağımız gibi soket nesnesi ile veri iletişimini sağlarlar. Diğer değişkenlerin açıklamaları da yukarıda vardır.

Şimdi Istemci sınıfının kurucu fonksyonuna bakalım:

  417             public Istemci(ASMESSunucusu sunucu, Socket istemciSoketi, long istemciID)

  418             {

  419                 this.sunucu = sunucu;

  420                 this.soket = istemciSoketi;

  421                 this.istemciID = istemciID;

  422             }

Görüldüğü gibi ASMESSuncusu nesnesine referans ve istemciID alınıyor. Ayrıca iletişim kurulacak Socket nesnesi de kurucu fonksyona aktarılıyor. Istemci threadini başlatmak ve durdurmak için Baslat ve Durdur fonksyonları tanımlanmıştır. Baslat fonksyonu aşağıdaki gibidir:

  428             public bool Baslat()

  429             {

  430                 try

  431                 {

  432                     agAkisi = new NetworkStream(soket);

  433                     binaryOkuyucu = new BinaryReader(agAkisi, Encoding.ASCII);

  434                     binaryYazici = new BinaryWriter(agAkisi, Encoding.ASCII);

  435                     thread = new Thread(new ThreadStart(tCalis));

  436                     calisiyor = true;

  437                     thread.Start();

  438                     return true;

  439                 }

  440                 catch (Exception)

  441                 {

  442                     return false;

  443                 }

  444             }

Burada da yapılan şey akış nesnelerinin hazırlanması ve tCalis fonksyonunu çalıştıracak olan thread'in başlatılmasından ibarettir. Asıl işlem tCalis fonksyonunda gerçekleşiyor:

  501             private void tCalis()

  502             {

  503                 //Her döngüde bir mesaj okunup sunucuya iletilir

  504                 while (calisiyor)

  505                 {

  506                     try

  507                     {

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

  509                         byte b = binaryOkuyucu.ReadByte();

  510                         if (b != BASLANGIC_BYTE)

  511                         {

  512                             //Başlangıç byte'ı değil, bu karakteri atla!

  513                             continue;

  514                         }

  515                         //Mesajı oku

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

  517                         while ((b = binaryOkuyucu.ReadByte()) != BITIS_BYTE)

  518                         {

  519                             bList.Add(b);

  520                         }

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

  522                         //Okunan paketi sunucuya ilet

  523                         sunucu.yeniIstemciMesajiAlindi(this, mesaj);

  524                         yeniMesajAlindiTetikle(mesaj);

  525                     }

  526                     catch (Exception)

  527                     {

  528                         //Hata oluştu, bağlantıyı kes!

  529                         break;

  530                     }

  531                 }

  532                 //Döngüden çıkıldığına göre istemciyle bağlantı kapatılmış ya da bir hata oluşmuş demektir

  533                 calisiyor = false;

  534                 try

  535                 {

  536                     if (soket.Connected)

  537                     {

  538                         soket.Close();

  539                     }

  540                 }

  541                 catch (Exception)

  542                 {

  543 

  544                 }

  545                 sunucu.istemciBaglantisiKapatildi(this);

  546                 baglantiKapatildiTetikle();

  547             }

Yine dikkat edilirse calistir (bool tipinde) değişkeninin değeri true olduğu sürece (Baslat fonksyonu çalıştırıldıktan itibaren Durdur fonksyonu çalıştırılana kadar bu değer true'dur) istemciden bir byte veri okunuyor, okunan byte başlangıç byte'ı (<) olmadığı sürece bu byte ihmal edilip sonraki byte okunmaya devam ediliyor. Geçen makaleden hatırlanacağı gibi istemci ile sunucu arasındaki bir mesaj aşağıdaki biçimde olmalıdır:

<mesaj metni>

Bir mesaj < ile başlar, mesaj içeriği ile devam eder ve > ile biter. Neticede okunan byte başlangıç byte'ı ise kodlar devam eder ve bitiş byte'ı (>) okunana kadar okunan tüm byte'lar bir listeye atılır. Bitiş byte'ı okunduktan sonra elde edilen liste ASCII kodlamasına göre bir string nesnesine dönüştürülür ve ardından ASMESSunucusu sınıfının yeniIstemciMesajiAlindi fonksyonuna bu string gönderilir. Son olarak da YeniMesajAlindi olayının tetiklendiği görülür (Tetiklenen tüm olayların nasıl kullanıldığı ilerde anlatılacaktır). Yukarıdaki kodda kullanılan 2 sabitin değeri ise şöyledir:

  342             private const byte BASLANGIC_BYTE = (byte)60; // < karakteri

  343             private const byte BITIS_BYTE = (byte)62; // > karakteri

60 ASCII tablosunda < işaretine 62 ise > işaretine karşılık gelir. Neticede yukarıdaki fonksyonda döngünün sonunda eğer açık kalmışsa soket bağlantısı kapatılarak ASMESSunucusu bilgilendirilir ve BaglantiKapatildi olayı tetiklenir.

Istemci nesnesi, istemciden gelen mesajları alıp ASMESSunucusu'na ilettiği gibi ASMESSunucusu'ndan istemciye gidecek mesajları soket üzerinden iletmek için de aşağıdaki fonksyonu tanımlar:

  477             public bool MesajYolla(string mesaj)

  478             {

  479                 try

  480                 {

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

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

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

  484                     b[0] = BASLANGIC_BYTE;

  485                     b[b.Length - 1] = BITIS_BYTE;

  486                     binaryYazici.Write(b);

  487                     agAkisi.Flush();

  488                     return true;

  489                 }

  490                 catch (Exception)

  491                 {

  492                     return false;

  493                 }

  494             }

Yukarıdaki fonksyon da yine birinci makaleden tanıdık gelecektir. Daha iyi anlamak için birinci makaleye bakılabilir. Yapılan işlem verilen string'i ASCII kodlamaya göre byte dizisine çevirmek, başına <, sonuna > karakteri ekleyip binaryYazici nesnesi ile soket üzerinden istemciye göndermekten ibarettir.

Sunucu çatısının geliştirilmesi

Yukarıda, istemcilerle nasıl iletişimin gerçekleştiği anlatıldı. Şimdi artık BaglantiDinleyici nesnesi ve Istemci nesneleri arasında ve bu kütüphaneyi kullanan uygulamalarla nasıl iletişim kurulacağı açıklanacaktır. ASMESSunucusu sınıfı mesajların yönlendirilmesi ve istemcilerin yönetilmesinden sorumludur. Aşağıdaki görevleri gerçekleştirir:

1) Baslat metodu ile bağlantı dinleyiciyi başlatır, Durdur metodu ile bağlantı dinleyiciyi durdurur ve o anda bağlı olan istemcilerle bağlantıyı keser.
2) Üst uygulama katmanı için bir MesajYolla fonksyonunu tanımlar. Bu fonksyon kendi içerisinde ilgili Istemci nesnesinin MesajYolla fonksyonunu çağırır.
3) Istemci'lerde meydana gelen olayları üst katmana iletebilmek için daha önce bahsedilen 3 olayı tanımlar: YeniIstemciBaglandi, IstemciBaglantisiKapatildi ve IstemcidenYeniMesajAlindi.
4) BaglantiDinleyici'den yeni bir istemci bağlantısı geldiğinde bir Istemci nesnesi oluşturarak çalışmasını başlatır.

Şimdi ASMESSunucusu sınıfını inceleyerek bu görevleri nasıl gerçekleştirdiğini anlamaya çalışalım. Öncelikle olaylardan başlayalım. Bu sınıf aşağıdaki 3 olayı tanımlayarak gerekli durumda kendisini kullanan uygulamaları bilgilendirir:

   29         /// <summary>

   30         /// Sunucuya yeni bir istemci bağlantığında tetiklenir.

   31         /// </summary>

   32         public event dgYeniIstemciBaglandi YeniIstemciBaglandi;

   33         /// <summary>

   34         /// Sunucuya bağlı bir istemci bağlantısı kapatıldığında tetiklenir.

   35         /// </summary>

   36         public event dgIstemciBaglantisiKapatildi IstemciBaglantisiKapatildi;

   37         /// <summary>

   38         /// Bir istemciden yeni bir mesaj alındığında tetiklenir.

   39         /// </summary>

   40         public event dgIstemcidenYeniMesajAlindi IstemcidenYeniMesajAlindi;

Bu olayların tetiklendiği kodlar birazdan açıklanacak. Ayrıca ASMESSunucusu sınıfı aşağıdaki alanları tanımlar:

   44         /// <summary>

   45         /// En son bağlantı sağlanan istemci ID'si

   46         /// </summary>

   47         private long sonIstemciID = 0;

   48         /// <summary>

   49         /// O anda bağlantı kurulmuş olan istemcilerin listesi

   50         /// </summary>

   51         private SortedList<long, Istemci> istemciler;

   52         /// <summary>

   53         /// Sunucunun çalışma durumu

   54         /// </summary>

   55         private volatile bool calisiyor;

   56         /// <summary>

   57         /// Senkronizasyonda kullanılacak nesne

   58         /// </summary>

   59         private object objSenk = new object();

   60         /// <summary>

   61         /// Bağlantı dinleyen nesne

   62         /// </summary>

   63         private BaglantiDinleyici baglantiDinleyici;

Görüldüğü gibi istemciler adında bir listemiz var. Yeni bir istemci bağlandığında bu listeye eklenir, bir istemci bağlantısı sonlandığında listeden çıkarılır. Böylece o anda bağlı olan tüm istemciler bir listede saklanmış olur. objSenk isimli nesne istemciler koleksiyonuna eşzamanlı erişimi düzenlemek için kullanılan kilit (lock) nesnesidir. Multithread çalıştığımız için bu koleksiyon nesnesine aynı anda birden çok thread ulaşıp işlem yaptığı takdirde hata oluşabilir. Bu yüzden bu koleksiyon üzerinde işlem yapan thread önce objSenk nesnesi üzerinde bir kilit oluşturur, istemciler nesnesi üzerinde işlem yaparken bu kilitli blokta çalışır. Bir nesne üzerinde kilit olan blokta bir thread çalışıyorsa başka hiçbir thread aynı nesne ile kilitlenen herhangi bir kod bloğuna erişemez. Bu konu multithread programlamanın en önemli konularından birisidir, burada çok fazla detaya giremeyeceğiz ancak birazdan kodları açıklarken tekrar kısaca ele alacağız. ASMESSunucusu sınıfının kurucu fonksyonu aşağıdaki gibidir:

   71         public ASMESSunucusu(int port)

   72         {

   73             this.port = port;

   74             this.istemciler = new SortedList<long, Istemci>();

   75             this.baglantiDinleyici = new BaglantiDinleyici(this, port);

   76         }

Görüldüğü gibi bir ASMESSunucusu oluştururken hangi portu dinleyeceği bilgisi verilir, bu bilgi de BaglantiDinleyici nesnesini oluştururken kullanılır (BaglantiDinleyici'nin kurucu fonksyonunu hatırlayın). ASMESSunucusu sınıfının Baslat fonksyonu sadece BaglantiDinleyici'yi başlatır ve calisiyor bayrağını true yapar. Durdur metodu ise aşağıdaki gibidir:

  100         public void Durdur()

  101         {

  102             //Dinleyiciyi durdur

  103             baglantiDinleyici.Durdur();

  104             //Tüm istemcileri durdur

  105             calisiyor = false;

  106             try

  107             {

  108                 IList<Istemci> istemciListesi = istemciler.Values;

  109                 foreach (Istemci ist in istemciListesi)

  110                 {

  111                     ist.Durdur();

  112                 }

  113             }

  114             catch (Exception)

  115             {

  116 

  117             }

  118             //İstemcileri temizle

  119             istemciler.Clear();

  120             //Çalışıyor bayrağındaki işareti kaldır

  121             calisiyor = false;

  122         }

Durdur fonksyonunda baglantiDinleyici durdurulduktan sonra tüm Istemci nesneleri sırayla durduruluyor, daha sonra calisiyor bayrağı false yapılıyor. Şimdi BaglantiDinleyici nesnesi yeni bir istemci bağlantısı kurduğunda, ASMESSunucusu içerisnde ne yapıldığını inceleyelim:

  140         private void yeniIstemciSoketiBaglandi(Socket istemciSoketi)

  141         {

  142             //Yeni bağlanan istemciyi listeye ekle

  143             Istemci istemci = null;

  144             lock (objSenk)

  145             {

  146                 istemci = new Istemci(this, istemciSoketi, ++sonIstemciID);

  147                 istemciler.Add(istemci.IstemciID, istemci);

  148             }

  149             //İstemciyi çalışmaya başlat

  150             istemci.Baslat();

  151             //YeniIstemciBaglandi olayını tetikle

  152             if (YeniIstemciBaglandi != null)

  153             {

  154                 YeniIstemciBaglandi(new IstemciBaglantiArgumanlari(istemci));

  155             }

  156         }

BaglantiDinleyici sınıfının tDinle fonksyonu içerisindeki döngüde bu fonksyonun çağırıldığını görmüştük. Bu fonksyon da görüldüğü gibi önce verilen Socket nesnesi ile ilişkili bir Istemci nesnesi oluşturulmuş, ardından bu Istemci nesnesi istemciler koleksiyonuna eklenmiş ve Baslat metodu ile istemci thread'i başlatılmış, son olarak da üst katmana yeni bir istemcinin bağlandığı YeniIstemciBaglandi olayı ile haber verilmiş. Şimdi de Istemci sınıfı istemciden bir string mesaj aldığında bu mesajı gönderdiği fonksyona bakalım:

  163         private void yeniIstemciMesajiAlindi(Istemci istemci, string mesaj)

  164         {

  165             if (IstemcidenYeniMesajAlindi != null)

  166             {

  167                 IstemcidenYeniMesajAlindi(new IstemcidenMesajAlmaArgumanlari(istemci, mesaj));

  168             }

  169         }

Görüldüğü gibi Istemci bir mesaj alıp ASMESSunucusu'na gönderdiğinde tek yapılan IstemcidenYeniMesajAlindi olayının tetiklenmesinden ibaret. ASMESSunucusu sınıfının istemciBaglantisiKapatildi fonksyonu Istemci sınıfı tarafından çağırılıyordu. Bu fonksyon da sadece kapanan istemciyi istemciler koleksiyonundan çıkarır ve IstemciBaglantisiKapatildi olayını tetikler.

Şimdi bu noktada yeni bir interface'i inceleyelim:

    7     /// <summary>

    8     /// Sunucuya bağlı olan bir istemciyi temsil eder

    9     /// </summary>

   10     public interface IIstemci

   11     {

   12         /// <summary>

   13         /// İstemciyi temsil eden tekil ID değeri

   14         /// </summary>

   15         long IstemciID { get; }

   16 

   17         /// <summary>

   18         /// İstemci ile bağlantının doğru şekilde kurulu olup olmadığını verir. True ise mesaj yollanıp alınabilir.

   19         /// </summary>

   20         bool BaglantiVar { get; }

   21 

   22         /// <summary>

   23         /// İstemciye bir mesaj yollamak içindir

   24         /// </summary>

   25         /// <param name="mesaj">Yollanacak mesaj</param>

   26         /// <returns>İşlemin başarı durumu</returns>

   27         bool MesajYolla(string mesaj);

   28 

   29         /// <summary>

   30         /// İstemci ile olan bağlantıyı kapatır

   31         /// </summary>

   32         void BaglantiyiKapat();

   33     }

ASMESSunucusu sınıfı üst katmanlarla (yani bu sınıfı ilerde kullanacak uygulamalarla) iletişim kurarken, örneğin bir istemciden bir mesaj alındığında bunu üst katmana event'lar ile bildirirken bu mesajın hangi sitemciden alındığını da bildirmesi gerektiğini daha önce belirtmiştik. İşte üst katmanda bir istemciyi temsil etmek için IIstemci arayüzü kullanılır. Burada denilebilir ki neden hangi istemciden mesaj alındıysa doğrudan o istemci ile ilişkili Istemci nesnesi ile olay tetiklenmiyor. Cevap basit aslında; Çünkü biz üst katmanların Istemci sınıfının tüm özelliklerini doğrudan kullanmasını istemiyoruz, sadece yukarıdaki interface'de tanımlı fonksyonları kullanabilsinler. Böylece daha fazla soyutlama sağlanmış ve üst katmanlar Istemci hakkında daha az şey bilmiş olurlar ki bu da nesne yönelimli programlama tekniğinin ruhunda vardır. Şimdi artık üst katmanın istediği bir istemciye nasıl mesaj yollayabileceğine bakabiliriz:

  129         public bool MesajYolla(IIstemci istemci, string mesaj)

  130         {

  131             return istemci.MesajYolla(mesaj);

  132         }

Yukarıdaki fonksyonda yapılan tek şey Istemci sınıfının MesajYolla fonksyonunu kullanmak. Peki üst katman istemcilere ait IIstemci referansını nasıl elde edecek? YeniIstemciBaglandi ve IstemcidenYeniMesajAlindi olaylarında bağlanan ve mesaj gönderen istemciye ait IIstemci arayüzü üst katmana sağlanıyor, böylece üst katman da bağlı olan istemcileri takip edebiliyor. Makalenin sonunda basit örnek uygulamada bunun nasıl kullanıldığını göreceğiz. Yukarıdan da anlaşılabileceği gibi aslında üst katman ASMESSunucusu sınıfını hiç kullanmadan doğrudan IIstemci interface'inin MesajYolla fonksyonu ile istediği istemciye mesaj yollayabilir çünkü IIstemci referansı zaten kendisinde vardır.

ASMES kütüphanesinin sunucu tarafı yukarıda anlatıldığı gibidir. Şimdi istemci tarafını da inceleyip daha sonra bu kütüphane üzerine basit bir uygulama geliştireceğiz.

İstemci tarafı

İstemci tarafında sadece ASMESIstemcisi adında bir sınıfımız var. Bu sınıf temelde sunucu tarafındaki Istemci sınıfına benzer. ASMESIstemcisi sınıfı istemci tarafındaki tüm işleri yapar:

1) Baglan ve BaglantiyiKes fonksyonları ile istenildiğinde sunucuya bağlanır ve bağlantıyı keser.
2) Üst katmanlardan sunucuya bir mesaj yollanmak istendiğinde MesajYolla fonksyonu ile mesajı yollar.
3) Sunucudan bir mesaj alındığında YeniMesajAlindi olayını tetikleyerek üst katmana mesajı iletir.
4) Sunucuyla olan bağlantı kesilirse BaglantiKapatildi olayını tetikleyerek bunu üst katmana bildirir.

Şimdi ASMESIstemcisi sınıfının kurucu fonksyonuna göz atalım:

   86         public ASMESIstemcisi(string serverIpAdresi, int serverPort)

   87         {

   88             this.serverIpAdresi = serverIpAdresi;

   89             this.serverPort = serverPort;

   90         }

TCP potokolünde sunucuya bağlanabilmek için sunucunun ip ve port bilgilerinin gerektiğini biliyoruz. Kurucu fonksyon da bu bilgileri alarak ilgili değişkenlere atar. ASMESIstemci sınıfını bu bilgileri kullanarak Baglan fonksyonu ile sunucuya bağlantı kurmaya çalışır:

   96         public bool Baglan()

   97         {

   98             try

   99             {

  100                 //sunucuya bağlan

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

  102                 IPEndPoint ipep = new IPEndPoint(IPAddress.Parse(serverIpAdresi), serverPort);

  103                 istemciBaglantisi.Connect(ipep);

  104                 agAkisi = new NetworkStream(istemciBaglantisi);

  105                 binaryOkuyucu = new BinaryReader(agAkisi, Encoding.ASCII);

  106                 binaryYazici = new BinaryWriter(agAkisi, Encoding.ASCII);

  107                 thread = new Thread(new ThreadStart(tCalis));

  108                 calisiyor = true;

  109                 thread.Start();

  110                 return true;

  111             }

  112             catch (Exception)

  113             {

  114                 return false;

  115             }

  116         }

Bu fonksyondaki kodların çok benzerini ilk makalede yazmıştık. Görüldüğü gibi önce TCP protokolünü kullanan bir Socket nesnesi oluşturuluyor, daha sonra sunucunun IP ve PORT bilgileri ile bu sunucuya bağlanılıyor. IPEndPoint nesnesi sunucu uygulamanın dinlediği, IP ve PORT ile temsil edilen bağlantı noktasıdır. Daha sonra akış sınıfları oluşturuluyor ve nihayet tCalis fonksyonunu giriş fonksyonu olarak alan bir thread oluşturulup başlatılıyor.

ASMESIstemcisi sınıfında tanımlanan diğer fonksyonlar (MesajYolla, tCalis.. v.s.) sunucu tarafındaki Istemci sınıfının hemen hemen aynısıdır, anlamak için yukarıdaki açıklamalara tekrar göz atabilirsiniz. Bu yüzden bu makalede detaylarına tekrar girmeyeceğim. Üst katmanın ASMESIstemci sınıfı ile nasıl etkileşim kurduğu yukarıda maddeler halinde açıklandı.

ASMES kütüphanesini kullanmak

Bu kısımda, yukarıda anlatılan ASMES kütüphanesini kullanarak, birden çok istemciye hizmet veren basit bir sunucu ve bu sunucuya bağlanabilen bir istemci uygulaması geliştireceğiz. Uygulamalarımız basit olması için console uygulaması şeklinde olacak ve aşağıdaki özellikler bulunacak:

- Bir istemci sunucuya bağlandığında o istemciye bir (sayısal) ID verilecek (bu zaten ASMES tarafından otomatik veriliyor) ve istemciye 'X ID numarası ile sunucuya bağlandınız..' mesajı iletilecek.
- Bir istemci sunucuya bir mesaj gönderdiğinde, o anda sunucuya bağlı olan tüm istemciler o mesajı ve hangi ID'li istemciden geldiğini ekranlarında görebilecekler. Mesajı gönderen istemciye ayrıca 'mesajınız X kişiye iletildi' şeklinde bir mesaj gönderilecek (burada X o anda bağlı olan istemci sayısı olacaktır).
- istemci sunucuya 'kapat' mesajı gönderdiğinde tüm istemcilere 'X ID numaralı istemci kapattı' mesajı iletilecek ve istemci uygulaması kapanacak.

Bu şekilde çok basitçe bir grup chat yapılabilecek. Bir sonraki makalemizde ise daha gelişmiş bir chat (IRC) sistemi gerçekleştireceğiz. Şimdi öncelikle sunucu uygulamamızı geliştirelim.

Sunucu Tarafı

Öncelikle eğer henüz yapmadıysanız makalenin ek dosyalarından kodları download edin çünkü geliştireceğimiz uygulamada ASMES kütüphanesini kullanacağız. Şimdi Visual Studio'da yeni bir console application oluşturalım.

ASMES kütüphanesini kullanabilmek için ASMESLIB\bin\Debug klasöründe bulunan ASMESLIB.dll dosyasını projemizin References kısmına ekleyelim.

Şimdi kodlarımızı geliştirmeye başlayabiliriz. Öncelikle ASMES kütüphanesini kullanabilmek için gerekli namespace'leri kodumuza ekliyoruz:

    5 //ASMES için namespace bildirimleri

    6 using HIK.ASMES;

    7 using HIK.ASMES.SunucuTarafi;

Bir Console Programının giriş fonksyonu Main fonksyonudur ve sunucu programımızda Main fonksyonunun içerisi aşağıdaki gibi olacak.

   16         static void Main(string[] args)

   17         {

   18             //Sunucu nesnesini ve istemciler listesini oluştur

   19             ASMESSunucusu sunucu = new ASMESSunucusu(10048);

   20             bagliIstemciler = new List<IIstemci>();

   21             //Istemcilerle ilgili olaylara kaydol

   22             sunucu.YeniIstemciBaglandi += new dgYeniIstemciBaglandi(sunucu_YeniIstemciBaglandi);

   23             sunucu.IstemcidenYeniMesajAlindi += new dgIstemcidenYeniMesajAlindi(sunucu_IstemcidenYeniMesajAlindi);

   24             sunucu.IstemciBaglantisiKapatildi += new dgIstemciBaglantisiKapatildi(sunucu_IstemciBaglantisiKapatildi);

   25             //Sunucunun çalışmasını başlat

   26             sunucu.Baslat();

   27             //Enter'a basılana kadar bekle, basılınca sunucuyu durdur ve çık

   28             Console.WriteLine("Sunucuyu kapatmak için Enter'a basınız...");

   29             Console.ReadLine();

   30             sunucu.Durdur();

   31         }

Main fonksyonunda daha önce geliştirdiğimiz ASMESSunucusu sınıfından bir nesneyi 10048 port'unu dinleyecek şekilde oluşturuyoruz. Daha sonra ASMES sunucu katmanı ile iletişim kurabilmek için gerekli olaylara kaydoluyoruz. ASMES kütüphanesi bu olayları tetikleyerek bize gerekli verileri iletecek. Daha sonra görüldüğü gibi enter'a basılana kadar sunucu açık kalıyor. Asıl işi yapan fonksyonlar sunucu olayları için yazdığımız fonksyonlardır. Öncelikle yeni bir istemci bağlandığında tetiklenen YeniIstemciBaglandi olayı için ne yazıldığına bakalım.

   33         //Yeni bir istemci bağlandığında bu fonksyon ASMES tarafından çağırılır

   34         static void sunucu_YeniIstemciBaglandi(IstemciBaglantiArgumanlari e)

   35         {

   36             //Yeni bağlanan istemciyi bagliIstemciler listesine ekle

   37             lock (bagliIstemciler)

   38             {

   39                 bagliIstemciler.Add(e.Istemci);

   40             }

   41             //Bu istemciye 'X ID numarası ile sunucuya bağlandınız' mesajı gönder

   42             e.Istemci.MesajYolla(e.Istemci.IstemciID + " ID numarasi ile sunucuya baglandiniz");

   43             //Sunucu ekranında bu bilgiyi göster

   44             Console.WriteLine(e.Istemci.IstemciID + " ID ile birisi sunucuya baglandi.");

   45         }

Burada yapılan biraz önce uygulamayı tanıtırken yazılanların gerçeklenmesinden başka bir şey değil aslında. Yeni gelen istemci bir listeye ekleniyor, sonra bu istemciye bir bilgilendirme mesajı gönderiliyor ve benzer bir bilgi sunucu ekranında gösteriliyor. Bir istemci bağlantısı kapatıldığında çalışan IstemciBaglantisiKapatildi olayı için ise aşağıdaki fonksyon gerçekleştiriliyor.

   47         //Bir istemci bağlantısı kapatıldığında bu fonksyon ASMES tarafından çağırılır

   48         static void sunucu_IstemciBaglantisiKapatildi(IstemciBaglantiArgumanlari e)

   49         {

   50             //Bağlantısı kapatılan istemciyi bagliIstemciler listesinden çıkar

   51             lock (bagliIstemciler)

   52             {

   53                 bagliIstemciler.Remove(e.Istemci);

   54             }

   55             Console.WriteLine(e.Istemci.IstemciID + " ID'li istemci ile bağlantı kesildi");

   56         }

Bir istemci bağlantısı kapatıldığında yapılan tek şey bu istemciyi listeden çıkartmak ve bu bilgiyi sunucu ekranına yazmaktan ibarettir. Şimdi asıl işleri yapan fonksyona geldik. İstemcilerin birisinden bir mesaj geldiğinde IstemcidenYeniMesajAlindi olayının tetiklendiğini biliyoruz. Aşağıdaki kodlar bu olay olduğunda çalışır.

   58         //Bir istemciden yeni bir mesaj alındığında bu fonksyon ASMES tarafından çağırılır

   59         static void sunucu_IstemcidenYeniMesajAlindi(IstemcidenMesajAlmaArgumanlari e)

   60         {

   61             //Eğer mesaj 'kapat' değilse..

   62             if (e.Mesaj != "kapat")

   63             {

   64                 //Bu mesajı bağlı olan tüm istemcilere ilet

   65                 lock (bagliIstemciler)

   66                 {

   67                     foreach (IIstemci ist in bagliIstemciler)

   68                     {

   69                         //kendisi de listede olduğu için kendisi hariç herkese gitsin

   70                         if (ist != e.Istemci)

   71                         {

   72                             //'[X] herkese selamlar' şeklinde mesajı herkese gönder

   73                             ist.MesajYolla("[" + e.Istemci.IstemciID + "] " + e.Mesaj);

   74                         }

   75                     }

   76                 }

   77                 //Mesajı gönderen kişiye, kaç kişiye iletildiği bilgisini gönder

   78                 e.Istemci.MesajYolla("Mesajiniz " + (bagliIstemciler.Count - 1) + " kisiye iletildi.");

   79                 //Sunucu ekranında gelen mesajı göster

   80                 Console.WriteLine("[" + e.Istemci.IstemciID + "] " + e.Mesaj);

   81             }

   82             //Mesaj 'kapat' ise kendisi hariç tüm istemcilere bu istemcinin çıktığını bildir

   83             else

   84             {

   85                 lock (bagliIstemciler)

   86                 {

   87                     foreach (IIstemci ist in bagliIstemciler)

   88                     {

   89                         //kendisi de listede olduğu için kendisi hariç herkese gitsin

   90                         if (ist != e.Istemci)

   91                         {

   92                             //'X numaralı istemci kapattı' şeklinde mesajı herkese gönder

   93                             ist.MesajYolla(e.Istemci.IstemciID + " ID numarali istemci kapatti..");

   94                         }

   95                     }

   96                 }

   97             }

   98         }

Yukarıdaki kodlarda gerekli açıklamalar vardır. Genel olarak yapılan iki iş vardır. Eğer kullanıcı 'kapat' mesajı göndermişse istemcinin sistemden çıkacağı anlaşılır ve kendisi hariç tüm istemcilere bu kişinin kapattığı mesajı iletilir. Eğer mesaj 'kapat' değilse if şartı doğru olur, birinci kısımdaki kodlar çalışır ve gelen mesaj yine gönderen hariç tüm istemcilere iletilir, daha sonra gönderen kişiye kendisi hariç listede olan kaç istemciye bu mesajın iletildiği bilgisi döner.

İstemci tarafı

Şimdi yukarıda geliştirilen sunucu uygulamayla iletişim kurabilecek bir istemci uygulaması geliştireceğiz. İstemci uygulamamız sunucu uygulamasına göre oldukça basit. Önce gerekli namespace'leri kodumuza dahil edelim.

    5 //ASMES için namespace bildirimleri

    6 using HIK.ASMES;

    7 using HIK.ASMES.IstemciTarafi;

Neredeyse bütün işi yapan fonksyon aslında uygulamanın Main fonksyonu. Main fonksyonunun içeriği aşağıdaki gibidir.

   13         static void Main(string[] args)

   14         {

   15             //Sunucuya bağlanmak için istemci nesnesini oluştur

   16             ASMESIstemcisi istemci = new ASMESIstemcisi("127.0.0.1", 10048);

   17             //Sunucudan gelen mesajları almak için ilgili olaya kaydol

   18             istemci.YeniMesajAlindi += new dgYeniMesajAlindi(istemci_YeniMesajAlindi);

   19             //Sunucuya bağlan

   20             istemci.Baglan();

   21             //kullanıcı "kapat" yazana kadar while döngüsünü sürdür

   22             string mesaj = null;

   23             while (mesaj != "kapat")

   24             {

   25                 //kullanıcıdan bir mesaj al

   26                 mesaj = Console.ReadLine();

   27                 //sunucuya yolla (eğer yollayamadıysak bağlantı kopmuştur, döngüyü bitir)

   28                 if (!istemci.MesajYolla(mesaj))

   29                 {

   30                     break;

   31                 }

   32             }

   33             //Sunucuya olan bağlantıyı kapat

   34             istemci.BaglantiyiKes();

   35         }

Görüldüğü gibi önce ASMESIstemcisi nesnesi oluşturulup gerekli IP ve PORT bilgisiyle sunucuya bağlanılıyor, ardından kullanıcıdan Console.ReadLine fonksyonu ile bir satır metin alınıyor. Bu metin 'kapat' olmadığı sürece döngüsel olarak kullanıcıdan bir string mesaj alıp sunucuya yollanıyor. Kullanıcı kapat yazıp enter'a basınca sunucuya son mesaj da iletilip bağlantı kesiliyor. Bu arada YeniMesajAlindi olayına da kaydolunuyor ki sunucudan gelen mesajlar da alınabilsin. YeniMesajAlindi olayının içerisi de oldukça basit:

   37         static void istemci_YeniMesajAlindi(MesajAlmaArgumanlari e)

   38         {

   39             Console.WriteLine(e.Mesaj);

   40         }

Görüldüğü gibi gelen mesaj sadece ekranda gösteriliyor, başka bir işlem yapılmıyor.

Uygulamaların test edilmesi

Buraya kadar ASMES adlı, TCP soketlerini kullanarak multithread sunucu/istemci istemci altyapısı sağlayan bir kütüphane geliştirildi, üzerine basit bir sunucu ve bu sunucu ile iletişim kurabilen basit bir istemci uygulama yazıldı. Şimdi bu uygulamaları ve dolayısıyla geliştirilen kütüphaneyi çalıştırıp test edebiliriz. Önce sunucu programını çalıştırdığımızda aşağıdaki gibi bir ekranla karşılaşırız:

Enter tuşuna basılmadığı sürece sunucu açık kalacaktır. Sunucuyu kapatmadan, istemci uygulamasını çalıştırdığımızda sunucu ve istemci ekranları aşağıdaki gibi olacaktır:

Görüldüğü gibi sunucu uygulamasında bir istemci bağlandığı, istemci uygulamasında ise sunucuya bağlanıldığı bilgisi gözüküyor. Şimdi ikinci bir istemci penceresi açalım (istemci uygulamasını Visual Studio'dan değil doğrudan Debug klasöründeki uygulamaya çift tıklayarak çalıştıralım).

İki istemcinin aynı anda bağlı olduğu durumda yukarıdaki gibi bir ekranla karşılaşılır. Şimdi de 2 numaralı istemciden sunucuya bir mesaj yollayalım. Mesajımızı yolladıktan sonra sunucu ve istemci pencereleri aşağıdaki gibi olacaktır:

Son olarak 1 ID'li istemciden kapat komutunu yazıp enter'a basarsak ortadaki pencere kapanacak ve sunucuya sadece 2 ID'li kullanıcı bağlı kalacaktır:

Sonuç

Bu makale boyunca önce multithread ve katmanlı programlama tekniklerinden kısaca söz edildi ve bu yapının gerekliliğinden ve avantajlarından bahsedildi. Ardından ASMES adlı bir soketli iletişim kütüphanesi geliştirildi. Son olarak da bu kütüphane üzerine çok basit bir mesajlaşma uygulaması geliştirildi. Görüldüğü gibi ASMES kütüphanesi elimizde olduğu sürece TCP soketlerini kullanan bir sunucu/istemci uygulaması geliştirmek son derece basit. Artık ASMES kütüphanesini kendi yazdığınız projelerde kullanabilirsiniz. String mesaj gönderip alınan her sistemde bu kütüphane kullanılabilir. Tek dikkat edilmesi gereken mesaj içerisinde < veya > işaretleri geçmemelidir. Geçerse ne olacağını tahmin edebilirsiniz, mesajlar yanlış yorumlanır (aslında biraz düşününce < işaretinin sakıncası olmayabilir ama prensip olarak kullanılmaması gerekir).

Gelecek makalede ASMES kütüphanesi üzerine çok daha gelişmiş ve Windows Formları üzerinden kullanılabilecek bir chat (IRC) sistemi geliştirilecek.

Halil İbrahim Kalkan
halilibrahimkalkan@yahoo.com

Referanslar:

[1] http://en.wikipedia.org/wiki/Thread_(computer_science)
[2] http://en.wikipedia.org/wiki/OSI_model
[3] http://en.wikipedia.org/wiki/TCP/IP_model

Örnek kodlar