Makale Özeti

Bu makalede, bir önceki makalede geliştirdiğimiz TCP Socket kütüphanesinin üzerinde çalışan basit bir toplu chat (IRC) sistemi geliştirmeyi açıklayacağız. Sunucuya bağlı kullanıcılar birbirlerini görebilecek, özel ve toplu mesaj atabileceklerdir.

Makale

'TCP Socket Programcılığına Giriş' makalemde TCP soketlerini genel anlamda inceleyip giriş niteliğinde bir örnek yaptık. 'Multithread, Katmanlı TCP Soket Sunucu/İstemci Mimarisi' isimli makalemde ise işin içine multithread programlamayı da koyarak herhangi bir projede TCP Socket altyapısı olarak kullanılabilecek ASMES adında bir kütüphane geliştirip, bunun üzerine çok basit bir örnek yaptık. Şimdi ise ASMES kütüphanesinin üzerine bir chat sistemi geliştirerek bu kütüphanenin kullanımını gerçek bir örnek üzerinde incelemiş olacağız.

Geliştirilen uygulama tek odadan oluşan basit bir IRC sistemidir. Kullanıcılar kendilerine bir nick seçerek odaya girer, odadakilerin listesini görür, onlara özel mesaj atabilir, odadakilerden özel mesaj alabilir, tüm odanın görebileceği şekilde mesaj atıp, odaya atılan mesajları görebilir.

Sistemin tanıtılması

İlk iki makalemin aksine bu defa önce çalışan uygulamayı inceleyeceğiz, ardından bu sistemin nasıl tasarlandığını ve geliştirildiğini açıklayacağız. Sistem sunucu/istemci mimarisinin doğal sonucu olarak iki farklı programdan oluşuyor; Chat sunucusu ve Chat istemcisi. Program kodlarını bilgisayarınıza indirdikten sonra önce sunucu programı çalıştırın. Karşınıza aşağıdaki gibi bir ekran gelecektir.

Bu ekranda üst kısımda sunucuyu başlatıp durdurmaya yarayan 2 buton ve sunucunun bağlantı isteklerini karşılamak için dinleyeceği TCP port numarası vardır. Alt kısımda ise istemcilerden sunucuya gelen son 10 mesaj ve o anda sunucuya bağlı olan istemcilerin (yani chat kullanıcılarının) isimleri (rumuzları ya da nick'leri) vardır. Başlat butonuna basıldığında sunucu başlamış olur. Sunucuyu başlattıktan sonra şimdi de istemci uygulamayı başlatalım. İstemci uygulama açılışta aşağıdaki gibi bir ekran sunar.

Bu ekranda yukarıda sunucu uygulamanın çalıştığı makinenin IP adresi ve sunucu uygulamanın dinlediği Port numarası vardır. Uygulamaları yerel bilgisayarda test ediyorsak bu ayarlar yukarıdaki gibi kalabilir. Nick olarak dilediğimiz bir ismi yazdıktan sonra Sunucuya Bağlan butonuna basalım. Benzer şekilde istemci uygulamasının iki adet daha kopyasını çalıştırıp sunucuya bağlandıktan sonra sunucu programında aşağıdaki gibi üç kişinin online olduğu görülür.

Yukarıdaki ekranda aynı zamanda istemcilerden gelen 3 mesaj gözükür. Mesajların başındaki sayı ilgili istemciye atanan (ASMES kütüphanesinden hatırlayabileceğiniz) ID numarasıdır. Bu şekilde aynı istemciden gelen mesajlar takip edilebilir. Mesajların biçimi bizim chat sistemimize özgü bir protokoldür ve ileride açıklanacaktır. Aşağıdaki şekilde sunucuya bağlı bir istemci penceresi gözüküyor.

Yukarıdaki ekranda toplu atılan mesajlar gözüküyor, formun altındaki alana mesaj yazıp enter tuşuna basıldığında odaya toplu bir mesaj atılmış olur. Sağ tarafta üstte bu istemcinin seçtiği nick ve onun altında da sunucuya bağlı diğer kişilerin nick'leri gözükür. Herhangi birisinin üzerine çift tıklandığında o kişi ile aşağıdaki gibi özel bir sohbet penceresi açılır.

Başlıkta sohbetin kimler arasında yapıldığı gözükür, buradan karşı taraf ile özel mesajlaşma yapılabilir. Sohbet sırasında Halil adlı kullanıcı programını kapatırsa ismail'in penceresinde 'Halil oturumunu kapattı.' mesajı gözükecek ve sohbet penceresi pasif olacak, mesaj yazılmayacaktır. Halil tekrar girerse pencere tekrar aktif olur. Bu sırada sunucu programın penceresinde aşağıdaki gibi tüm işlemler gözükür.

Birden çok istemci programı çalıştırıp denemeler yaparak programları daha iyi keşfedebilirsiniz. Bizim amacımız burada bu programların nasıl geliştirildiğini açıklamak olduğu için daha fazla detaya girilmeyecektir.

Sistemin tasarlanması

İlk iki makalede verdiğimiz şekli burada da tekrar vererek sistemin genel çatısını görmeye çalışalım.

Bu makalede geliştireceğimiz sunucu ve istemci kodlarımız yukarıdaki şekilde 'Chat Sunucu' ve 'Chat İstemci' katmanlarında çalışacaktır. Aşağıdaki şekilde istemciden sunucuya gönderilen bir mesajın temsili olarak iletimi gösterilmiştir.

İstemci uygulamadaki 'Chat İstemci' katmanından string olarak gönderilen şekildeki mesajı ağ üzerinden sunucu uygulamadaki 'Chat Sunucusu' katmanına doğru şekilde iletmek ASMES kütüphanesinin görevidir ve bunun nasıl yapıldığı bir önceki makalede açıklandı. Bu makalede ise iletilen mesajın değerlendirilip chat sisteminin geliştirilmesini inceleyeceğiz. Sunucu ile istemci arasında gönderebildiğimiz verinin düz metinlerden (string) ibaret olduğunu biliyoruz. İstemcinin sunucuya bağlanabilmesi, özel ya da toplu mesaj gönderebilmesi ve diğer işlemleri yapabilmesi için istemci ile sunucu arasında gidip gelen düz metin mesajların özel anlamlarının olması lazım. Bunun için de sunucu ile istemci arasında bir 'mesajlaşma protokolüne' ihtiyaç vardır. Yukarıdaki şekilde istemcinin login işlemi için sunucuya gönderdiği mesaj gözüküyor. Anlaşıldığı gibi gidip gelen string mesajlar anahtar/değer çiftlerinden oluşur. Anahtar ile değerlerin eşleşmesi = işareti ile, anahtar/değer çiftlerinin birbirinden ayrılması ise & işareti ile olur. Yani genel olarak bir mesaj biçimi aşağıdaki gibidir:

anahtar1=deger1&anahtar2=deger2&anahtar3=deger3 ...

Bu biçim web'deki url'lerin sonuna yazılan query string'e benzer. Sunucu ve istemci yukarıdaki biçimde aldıkları mesajları önce & işaretinden ayırır, ardından her parçayı = işaretinden ayırarak karşı tarafın ne mesaj gönderdiğini anlar. Tüm mesajlar sunucu ve istemci arasındadır, iki istemci doğrudan kendi aralarında mesaj gönderip alamazlar (Dolayısıyla özel mesajlar dahi önce sunucuya iletilir, sunucu mesajı ilgili istemciye yönlendirir).

Protokolümüzde ilk anahtar her zaman komut kelimesidir ve karşısında bu mesajın türünü belirtir. Bundan sonra gelen anahtar/değer çiftleri bu komutun parametreleridir. Örneğin 'komut=giris' olursa bu mesaj istemciden sunucuya gönderilen login amaçlı bir mesajdır, 'nick=halil' bu komutun parametresidir ve kullanıcının seçtiği nick'i gösterir. Şimdi tüm mesaj türlerini inceleyelim.

Örnek senaryolar

- Bir istemci programı çalıştırılıp kullanıcı giriş yapmak istediğinde sırayla aşağıdaki olaylar gerçekleşir:

1) Kullanıcı bir nick seçip Sunucuya bağlan butonuna basar.
2) İstemciden sunucuya bir 'Giriş' mesajı gönderilir.
3) Sunucu, giriş yapan istemciye bir 'Giriş Cevabı' mesajı gönderir.
4) Eğer giriş başarılıysa sunucu diğer tüm istemcilere bu olayı haber vermek için 'Kullanıcı Girişi' mesajı gönderir.
5) Eğer giriş başarılıysa sunucu giriş yapan istemciye o anda bağlı tüm kullanıcıların listesini iletmek için bir 'Kullanıcı Listesi' mesajı gönderir.
6) 'Kullanıcı Girişi' mesajını alan tüm istemciler bu yeni giriş yapan kullanıcıyı form üzerindeki listeye ekler.
7) 'Kullanıcı Listesi' mesajını alan istemci tüm kullanıcıları ekrandaki listeye ekler.

- Bir kullanıcı başka bir kullanıcıya özel mesaj atmak istediğinde sırayla aşağıdaki olaylar gerçekleşir:

1) Kullanıcı listeden birisine çift tıklar, açılan pencereye istediği bir şeyler yazar ve enter'a basar.
2) Sunucuya bir 'Özel Mesaj' mesajı gönderilir.
3) Sunucu hedef kullanıcıyı 'nick' parametresinden bularak hedef istemciye bir 'Özel Mesaj' mesajı gönderir.
4) 'Özel Mesaj' mesajını alan istemci kullanıcıya bu mesajı gösterir.

- Bir kullanıcı çıkış yapmak istediğinde sırayla aşağıdaki olaylar gerçekleşir:

1) Çıkmak isteyen kullanıcı, istemci programını kapatır.
2) Sunucuya bir 'Çıkış' mesajı gönderilir.
3) Sunucu kendisine bağlı tüm istemcilere bu mesajı 'Kullanıcı Çıkışı' mesajı olarak iletir.
4) 'Kullanıcı Çıkışı' mesajını alan tüm istemciler bu kullanıcıyı listelerinden silerler.

Sistemin geliştirilmesi

Bu bölümde, yukarıda genel yapısı açıklanan sistemi gerçekleştiren sunucu ve istemci yazılımların nasıl gerçeklendiği açıklanacaktır. Aslında programları geliştirmek demek yukarıda tanımlanan mesaj türleri alındığında ne yapılacağını belirtmek ve gerektiğinde karşı tarafa gönderilecek mesajları göndermek demektir.

Hem sunucuda hem de istemcide çalışması gereken temel bir fonksyon aşağıda verilmiştir. Bu fonksyon karşı taraftan alınan 'anahtar1=deger1&anahtar2=deger2...' şeklindeki mesajı çözümleyerek anahtar değer çiftlerini içeren bir koleksiyon dönderir.

  421         private NameValueCollection mesajCoz(string mesaj)

  422         {

  423             try

  424             {

  425                 //& işaretine göre böl ve diziye at

  426                 string[] parametreler = mesaj.Split('&');

  427                 //dönüş değeri için bir NameValueCollection oluştur

  428                 NameValueCollection nvcParametreler = new NameValueCollection(parametreler.Length);

  429                 //bölünen her parametreyi = işaretine göre yeniden böl ve anahtar/değer çiftleri üret

  430                 foreach (string parametre in parametreler)

  431                 {

  432                     string[] esitlik = parametre.Split('=');

  433                     nvcParametreler.Add(esitlik[0], esitlik[1]);

  434                 }

  435                 //oluşturulan koleksiyonu dönder

  436                 return nvcParametreler;

  437             }

  438             catch (Exception)

  439             {

  440                 return null;

  441             }

  442         }

Eğer gelen mesaj kurallara uygun değilse bu fonksyon null dönderir. NameValueCollection koleksiyonu anahtar ve değer olarak string tutar. Anahtar olarak verilen string'e karşılık bu anahtarla eşleşen değeri geri dönderir.

Sunucu tarafı

Chat sistemimizde sunucu tamamen pasiftir. Herhangi bir mesaj almadığı sürece bir işlem yapmaz, istemcilerden birisinden bir mesaj aldığında ise bu mesajın ne olduğunu anladıktan sonra gerekli işlemleri gerçekleştirir. Sunucu tarafında en temel iki nesne tanımlanır:

   17         /// <summary>

   18         /// ASMES kütüphanesindeki sunucu nesnesi

   19         /// </summary>

   20         private ASMESSunucusu sunucu;

   21         /// <summary>

   22         /// Sunucuya bağlı olan kullanıcıları saklayan liste

   23         /// </summary>

   24         private List<Kullanici> kullanicilar;

sunucu, ASMES kütüphanesini kullanmak için ASMESSunucusu sınıfından bir nesnedir (ASMESSunucusu önceki makalede açıklanmıştır), kullanicilar listesi ise o anda sunucuya bağlı kullanıcıları saklamak için kullanılan bir koleksiyondur. Burada kullanicilar koleksiyonunun Kullanici türünde nesneleri sakladığı görülüyor. Bu sınıfın tanımı aşağıdaki şekildedir.

  446         private class Kullanici

  447         {

  448             /// <summary>

  449             /// ASMES kütüphanesindeki Istemci nesnesine referans

  450             /// </summary>

  451             public IIstemci Istemci

  452             {

  453                 get { return istemci; }

  454                 set { istemci = value; }

  455             }

  456             private IIstemci istemci;

  457 

  458             /// <summary>

  459             /// Kullanıcının Nick'i

  460             /// </summary>

  461             public string Nick

  462             {

  463                 get { return nick; }

  464                 set { nick = value; }

  465             }

  466             private string nick;

  467 

  468             /// <summary>

  469             /// Yeni bir Kullanıcı nesnesi oluşturur.

  470             /// </summary>

  471             /// <param name="istemci">ASMES kütüphanesindeki Istemci nesnesine referans</param>

  472             /// <param name="nick">Kullanıcının Nick'i</param>

  473             public Kullanici(IIstemci istemci, string nick)

  474             {

  475                 this.istemci = istemci;

  476                 this.nick = nick;

  477             }

  478         }

Bu sınıf sunucuya bağlı bir istemciye ait iki nesne saklar. Birincisi ASMESSunucusu katmanıyla iletişim sağlarken bu istemciyi temsil etmek için bir IIstemci nesnesi, diğeri ise bu istemcinin kullandığı nick bilgisidir.

Kodları incelediğinizde sunucu formunda Başlat butonuna basıldığında baslat isimli bir fonksyonun çalıştığı görülüyor. Bu fonksyon form'daki textbox'dan port bilgisini alır, ASMESSunucusu nesnesini oluşturur, olaylarına kaydolur ve nihayetinde sunucuyu başlatır. Önceki makaleden hatırlayacağınız gibi ASMESSunucusu sınıfının tanımladığı 3 olay vardır. Burada YeniIstemciBaglandi olayına kaydolmuyoruz çünkü bir kullanıcının chat sistemine girebilmesi için bağlandıktan sonra bir 'Giriş' mesajı göndermesini istiyoruz, bu mesaj geldiğinde zaten birisinin bağlandığını anlamış oluyoruz. Kaydolduğumuz olaylardan en önemli olanı IstemcidenYeniMesajAlindi olayı. Çünkü sunucunun yaptığı işin neredeyse tamamı istemciden yeni bir mesaj alındığında bunu işlemek ve gerekli görevleri yerine getirmekten ibarettir. IstemcidenYeniMesajAlindi olayı gerçekleştiğinde aşağıdaki fonksyon çalışır:

  113         private void sunucu_IstemcidenYeniMesajAlindi(IstemcidenMesajAlmaArgumanlari e)

  114         {

  115             Invoke(new dgIstemcidenYeniMesajAlindi(mesajAlindi), e);

  116         }

Buradaki koda dikkat etmek gerekiyor. Windows formlarıyla çalışan multithread bir program yazdığımızda önemli bir sorunla karşılaşırız; .NET, formu oluşturan thread dışındaki başka bir thread'in formun kendisine ya da üzerindeki herhangi bir bileşene dair bir değişiklik yapılmasına izin vermez ve bir hata fırlatır. Örneğin başka bir thread'den form üzerindeki bir textbox'ın içeriğini değiştiremeyiz. Form sınıfının Invoke fonksyonu bu formu oluşturan thread'in bir fonksyonu çalıştırmasını sağlar, parametre olarak bir delegate alır. Burada formu oluşturan thread'den mesajAlindi fonksyonunu çalıştırması isteniyor. Yani asıl gelen mesajı işleyen mesajAlindi fonksyonudur. Bu fonksyonun içeriği aşağıdaki gibidir.

  133         private void mesajAlindi(IstemcidenMesajAlmaArgumanlari e)

  134         {

  135             //Gelen mesajı & ve = işaretlerine göre ayrıştır

  136             NameValueCollection parametreler = mesajCoz(e.Mesaj);

  137             //Ayrıştırma başarısızsa çık

  138             if (parametreler == null || parametreler.Count < 1)

  139             {

  140                 return;

  141             }

  142             //Ayrıştırma sonucunda komuta göre gerekli işlemleri yap

  143             try

  144             {

  145                 switch (parametreler["komut"])

  146                 {

  147                     case "giris":

  148                         //parametreler: nick

  149                         komut_giris(e.Istemci, parametreler["nick"]);

  150                         break;

  151                     case "ozelmesaj":

  152                         //parametreler: nick, mesaj

  153                         komut_ozelmesaj(e.Istemci, parametreler["nick"], parametreler["mesaj"]);

  154                         break;

  155                     case "toplumesaj":

  156                         //parametreler: mesaj

  157                         komut_toplumesaj(e.Istemci, parametreler["mesaj"]);

  158                         break;

  159                     case "cikis":

  160                         //parametreler: YOK

  161                         komut_cikis(e.Istemci);

  162                         break;

  163                 }

  164             }

  165             catch (Exception)

  166             {

  167 

  168             }

  169 

  170             //Mesajı 'Son Gelen 10 Mesaj' listesine en başa ekle

  171             lstSon10Mesaj.Items.Insert(0, "[" + e.Istemci.IstemciID.ToString("0000") + "] " + e.Mesaj);

  172             //Listedeki mesaj sayısı 10'u geçmişse sondan sil.

  173             if (lstSon10Mesaj.Items.Count > 10)

  174             {

  175                 lstSon10Mesaj.Items.RemoveAt(10);

  176             }

  177         }

Bu fonksyon daha önce açıklanan mesajCoz fonksyonunu kullanarak istemciden gelen string mesajı, protokolümüzün kurallarına göre anahtar/değer çiftlerine ayrıştırıyor. Daha sonra komut anahtarının değerine göre farklı fonksyonlar çağırıyor. Hatırlayacağınız gibi komut anahtarının değeri mesajın türünü belirliyordu ve sunucuya 4 çeşit mesaj türü gelebilirdi. Burada komut'un değeri bir switch'e sokularak uygun fonksyon çağırılıyor. Şimdi bu fonksyonların ne yaptığına sırayla bakalım. komut=giris olduğunda komut_giris fonksyonu çağırılıyor. Fonksyonun içeriği aşağıdaki gibidir.

  184         private void komut_giris(IIstemci istemci, string nick)

  185         {

  186             //Eşzamanlı erişimlere karşı koleksiyonu kilitleyelim

  187             lock (kullanicilar)

  188             {

  189                 Kullanici kullanici = null;

  190                 //Tüm kullanıcıları tara,

  191                 //aynı nickli başkası varsa giriş başarısızdır

  192                 foreach (Kullanici kul in kullanicilar)

  193                 {

  194                     if (kul.Nick == nick)

  195                     {

  196                         kullanici = kul;

  197                         break;

  198                     }

  199                 }

  200                 //Nick kullanımdaysa istemciye uygun dönüş mesajını verip çık

  201                 if (kullanici != null)

  202                 {

  203                     istemci.MesajYolla("komut=giris&sonuc=basarisiz");

  204                     return;

  205                 }

  206                 //Tüm kullanıcıları tara,

  207                 //aynı istemci zaten listede varsa sadece nickini güncelle

  208                 foreach (Kullanici kul in kullanicilar)

  209                 {

  210                     if (kul.Istemci == istemci)

  211                     {

  212                         kullanici = kul;

  213                         break;

  214                     }

  215                 }

  216                 //İstemci listede varsa sadece nickini güncelle

  217                 if (kullanici != null)

  218                 {

  219                     kullanici.Nick = nick;

  220                 }

  221                 //Listede yoksa listeye ekle

  222                 else

  223                 {

  224                     kullanicilar.Add(new Kullanici(istemci, nick));

  225                 }

  226             }

  227             //Kullanıcıya işlemin başarılı olduğu bilgisini gönder

  228             istemci.MesajYolla("komut=giris&sonuc=basarili");

  229             //Tüm kullanıcılara bu kullanıcının giriş yaptığı bilgisini gönder

  230             tumKullanicilaraMesajYolla("komut=kullanicigiris&nick=" + nick);

  231             //Bu kullanıcıya mevcut kullanıcı listesini gönder

  232             kullaniciListesiniGonder(istemci);

  233             //Kullanıcı listesini ekranda gösterelim

  234             kullaniciListesiniYenile();

  235         }

Yukarıdaki fonksyonda öncelikle aynı nick'de bir istemci olup olmadığını kontrol eder, varsa istemciye başarısız mesajı döner. Yoksa devam eder, eğer bu istemci zaten sistemdeyse sadece nick'ini günceller. Buradan anlaşıldığı gibi giris komutu aslında nick değiştirmek için de kullanılabilir (ancak kullanılmayacak çünkü istemci programında nick değiştirme ekranı yapmadık). Eğer istemci listede yoksa listeye nick'i ile beraber eklenir ve istemciye başarılı sonucu dönderilir. Ayrıca sistemdeki tüm kullanılara bu yeni istemcinin girdiğini belirten kullanicigiris mesajı gönderilir. Son olarak da giriş yapan istemciye o anda sistemde bulunan tüm kullanıcıların listesi protokolümüzdeki kullanicilistesi komutu ile gönderilir (bunu yapan kullaniciListesiniGonder fonksyonunu inceleyin).

mesajAlindi fonksyonunda, gelen mesajın türü 'ozelmesaj' ise çağırılan fonksyon komut_ozelmesaj fonksyonudur. Bu fonksyonun temel görevi bir kullanıcı diğerine özel mesaj atmak istediğinde, bu mesajı atan kişiden alıp hedef kişiye yönlendirilmektir. Fonksyonun yapısı aşağıdaki gibidir.

  243         private void komut_ozelmesaj(IIstemci istemci, string nick, string mesaj)

  244         {

  245             //Kullanıcıları saklamak için değişkenler

  246             Kullanici gonderenKullanici = null, hedefKullanici = null;

  247             //Eşzamanlı erişimlere karşı koleksiyonu kilitleyelim

  248             lock (kullanicilar)

  249             {

  250                 //Tüm kullanıcıları tara,

  251                 //mesajı gönderen ve mesajın hedefinde olan kullanıcıyı bul

  252                 foreach (Kullanici kul in kullanicilar)

  253                 {

  254                     //Gönderen kullanıcıyı Istemci nesnesine göre ayırt ediyoruz

  255                     if (kul.Istemci == istemci)

  256                     {

  257                         gonderenKullanici = kul;

  258                     }

  259                     //Hedef kullanıcıyı nick'e göre ayırt ediyoruz

  260                     if (kul.Nick == nick)

  261                     {

  262                         hedefKullanici = kul;

  263                     }

  264                     //Eğer kullanıcıları bulduysak döngüyü devam ettirmeye gerek yok

  265                     if (gonderenKullanici != null && hedefKullanici != null)

  266                     {

  267                         break;

  268                     }

  269                 }

  270             }

  271             //Kullanıcılar bulunamadıysa fonksyonu sonlandıralım

  272             if (gonderenKullanici == null || hedefKullanici == null)

  273             {

  274                 return;

  275             }

  276             //Hedef kullanıcıya istenilen mesajı gönderelim

  277             hedefKullanici.Istemci.MesajYolla("komut=ozelmesaj&nick=" + gonderenKullanici.Nick + "&mesaj=" + mesaj);

  278         }

Mesajı gönderen ve mesajı alması gereken kullanıcıları bulmak için kullanicilar koleksiyonu taranıyor. Gönderen kullanıcı IIstemci arayüzünden ayırt edilir çünkü kendi nick'ini göndermemiştir. Hedef kullanıcı ise nick bilgisi yardımıyla bulunur. Neticede iki kullanıcı da bulunduğunda 277. satırda görüldüğü gibi hedef kullanıcıya bir ozelmesaj komutu oluşturulup gönderilir. Toplu mesaj atma görevini üstlenen komut_toplumesaj fonksyonu ise daha basittir ve mesajı atan kullanıcının nick'ini bulduktan sonra tumKullanicilaraMesajYolla isimli fonksyon ile o anda sistemdeki tüm istemcilere aynı mesajı gönderir. komut_toplumesaj fonksyonunu kodlardan inceleyebilirsiniz, burada biz tumKullanicilaraMesajYolla fonksyonuna bakalım.

  403         private void tumKullanicilaraMesajYolla(string mesaj)

  404         {

  405             Kullanici[] kullaniciDizisi = null;

  406             //Eşzamanlı erişimlere karşı koleksiyonu kilitleyelim

  407             lock (kullanicilar)

  408             {

  409                 //Listedeki tüm kullanıcıları bir diziye atalım

  410                 kullaniciDizisi = kullanicilar.ToArray();

  411             }

  412             //Tüm kullanıcılara istenilen mesajı gönderelim

  413             foreach (Kullanici kul in kullaniciDizisi)

  414             {

  415                 kul.Istemci.MesajYolla(mesaj);

  416             }

  417         }

Yukarıdaki fonksyon öncelikle kullanicilar koleksiyonunu uzun süre kilitli tutmamak için kullanıcıları bir diziye kopyalıyor, daha sonra bu dizideki herkese aynı mesaj metninin yolluyor. Son olarak sunucu tarafında işlenen mesaj cikis mesajıdır. Bu mesaj geldiğinde mesajAlindi fonksyonunun komut_cikis fonksyonunu çalıştırdığı görülür. Bu fonksyonu incelediğimizde (kodlara bakabilirsiniz) kullanicilar koleksiyonundan çıkış yapan istemciyi çıkardığı, ardından sisteme bağlı herkese kullanicicikis komutu ile bu kullanıcının çıkış yaptığını bildirdiği görülür. Sunucu tarafındaki kodların önemli kısımları ve genel işleyişi burada incelendi, daha fazlası için kod içerisinde ayrıntılı açıklamalar mevcuttur.

İstemci tarafı

İstemci tarafının yaptığı genel anlamda iki işlem vardır; sunucudan gelen mesajları değerlendimek ve kullanıcının isteklerini sunucuya iletmek. Bunun yanında tabii ki istemci, programın açılışında sunucuya bağlanmak ve programdan çıkarken sunucu ile bağlantıyı kapatmak durumundadır. İstemci programımız 3 formdan oluşuyor. frmAna, ana istemci penceresini temsil eden ve sunucu ile mesajlaşmalayı sağlayan temel sınıftır. frmGiris kullanıcıdan sunucuya bağlantı bilgilerini almak, nick seçmesini sağlamak ve sunucuya bağlantıyı gerçekleştirmek için tasarlanmıştır. Son olarak frmOzelSohbet formu ise bir başka kullanıcı ile özel sohbet yapıldığında açılan penceredir.

Yukarıdaki bilgiler çerçevesinde öncelikle kullanılan temel nesneleri inceleyelim.

   17         /// <summary>

   18         /// ASMES kütüphanesini kullanarak ASMES sunucusuna bağlı olan istemci nesnesi

   19         /// </summary>

   20         private ASMESIstemcisi istemci;

   21 

   22         /// <summary>

   23         /// Kullanıcının Nick'i

   24         /// </summary>

   25         public string Nick

   26         {

   27             get { return nick; }

   28             set { nick = value; }

   29         }

   30         private string nick;

   31 

   32         /// <summary>

   33         /// Rastgele sayı üretmede kullanılacak bir nesne

   34         /// </summary>

   35         private Random rnd = new Random();

   36 

   37         /// <summary>

   38         /// Özel sohbet yapılan formlar

   39         /// </summary>

   40         private SortedList<string, frmOzelSohbet> ozelSohbetFormlari;

   41 

   42         /// <summary>

   43         /// Chat ortamında bulunan kullanıcı listesi

   44         /// </summary>

   45         private List<string> kullanicilar;

ASMESIstemci sınıfından türetilen istemci nesnesi önceki makaleden de hatırlayabileceğimiz gibi ASMES istemci katmanından faydalanmamız için kullanılan nesnedir. ozelSohbetFormları koleksiyonu ile kullanıcının özel sohbet yaptığı kişilerle konuşmalarını gerçekleştirdiği pencerelerin listesi saklanır. kullanicilar koleksiyonu ise sohbet ortamında bulunan tüm kullanıcıların bir listesidir. Şimdi ilk olarak sunucu ile nasıl bağlantı kurulduğu ve giriş yapıldığı incelenecektir. Yukarıda anlatıldığı gibi bir istemci sunucuya bağlanmak için öncelikle TCP üzerinden bir bağlantı kurar, ardından giris komutunu içeren mesajı sunucuya gönderir, sunucu da eğer seçilen nick kullanılmıyorsa basarili, aksi halde basarisiz seklinde bir giris komutu gönderir istemciye cevap olarak. frmAna sınıfının Shown olayında aşağıdaki kodlar yazılmıştır (Shown olayı bir windows formu ilk kez kullanıcı tarafından görüldüğünde tetiklenir).

   54         private void frmAna_Shown(object sender, EventArgs e)

   55         {

   56             //Giriş formunu göster

   57             frmGiris girisFormu = new frmGiris();

   58             girisFormu.ShowDialog();

   59             //Eğer giriş formundan sunucuya doğru bağlanıldıysa sistemi başlat

   60             if (girisFormu.GirisYapildi)

   61             {

   62                 //ASMESIstemcisi referansını al

   63                 istemci = girisFormu.Istemci;

   64                 nick = girisFormu.Nick;

   65                 //Olaylara kaydol

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

   67                 //Sunucuya giriş mesajı gönder

   68                 Text = "Chat İstemcisi - Bağlanıyor...";

   69                 istemci.MesajYolla("komut=giris&nick=" + nick);

   70                 //textbox'a odaklan

   71                 txtTopluMesaj.Focus();

   72             }

   73             //Aksi halde formu kapat

   74             else

   75             {

   76                 Close();

   77             }

   78         }

Bu fonksyonda öncelikle bir frmGiris formu oluşturup gösteriliyor. Bildiğiniz gibi ShowDialog metodu, açılan form kapana kadar çağıran yere dönmez, dolayısıyla burada da giriş formu kapatılınca kodlar if deyimimden devam eder. frmGiris formunu incelediğimizde yapılan işlem kullanıcının sunucunun ip ve port bilgilerini girmesine ve kendisine bir nick seçmesine yardımcı olmaktır. Kullanıcı 'Sunucuya bağlan' butonuna bastığında kullanıcının girdiği veriler doğrulandıktan sonra baglan fonksyonu çağırılır.

   78         private bool baglan()

   79         {

   80             try

   81             {

   82                 //Formdan IP ve PORT bilgilerini al

   83                 string ip = txtIP.Text;

   84                 int port = Convert.ToInt32(txtPort.Text);

   85                 //Bir istemci nesnesi oluştur ve bağlan

   86                 istemci = new ASMESIstemcisi(ip, port);

   87                 return istemci.Baglan();

   88             }

   89             catch (Exception)

   90             {

   91                 return false;

   92             }

   93         }

baglan fonksyonu doğru bir şekilde sunucuya bağlandıysa aşağıdaki kodlarla (btnBaglan butonunun click olayında) form kapatılarak frmAna sınıfının shown olayından devam edilir.

   72                 girisYapildi = true;

   73                 nick = txtNick.Text;

   74                 Close();

frmAna_Shown metoduna geri dönersek girisYapildi bool değişkeni false ise formu (ana form olduğu için, dolayısıyla programı) kapattığı görülür. Bu değer true ise bağlantı başarılı demektir (ki hemen yukarıdaki kodlarda nerede true yapıldığı gözüküyor) ve ASMESIstemcisi sınıfından oluşturulan istemci nesnesinin YeniMesajAlindi olayına kaydolunduktan sonra sunucuya bir 'giris' komutu gönderilir. Bu mesajın nasıl değelendirildiği sunucu tarafında detaylı anlatıldı. Sunucu giris mesajımıza cevap verene kadar kullanıcı formu kullanamaması gerekir. Bu yüzden başlangıçta formu üzerindeki kontrollerin Enabled özelliği false yapılmıştır. Ayrıca giris mesajı gönderilmeden önce de (satır 68) formun başlığı 'Chat İstemcisi - bağlanıyor...' olarak olarak değiştirilmiştir. Sunucudan alınan giris mesajını değerlendirilirken ne yapıldığı az sonra anlatılacak. 66. satırda sunucunun YeniMesajAlindi olayına kaydolduğumuz için sunucudan bir mesaj alındığında istemci_YeniMesajAlindi fonksyonumuzun çağırılacağını biliyoruz.

   80         void istemci_YeniMesajAlindi(MesajAlmaArgumanlari e)

   81         {

   82             Invoke(new dgYeniMesajAlindi(mesajAlindi), e);

   83         }

Bu fonksyon alınan mesaj üzerinde hiç işlem yapmada mesajı derhal mesajAlindi fonksyonuna Invoke metodu ile iletiyor (Neden böyle yapıldığı sunucu tarafında açıklanmıştı). Asıl mesajı değerelendiren foksyonumuz mesajAlindi fonksyonu aşağıdaki gibidir.

   97         /// <summary>

   98         /// Sunucudan bir mesaj alındığında buraya gelir

   99         /// </summary>

  100         /// <param name="e">Alınan mesajla ilgili bilgiler</param>

  101         private void mesajAlindi(MesajAlmaArgumanlari e)

  102         {

  103             //Gelen mesajı & ve = işaretlerine göre ayrıştır

  104             NameValueCollection parametreler = mesajCoz(e.Mesaj);

  105             //Ayrıştırma başarısızsa çık

  106             if (parametreler == null || parametreler.Count < 1)

  107             {

  108                 return;

  109             }

  110             //Ayrıştırma sonucunda komuta göre gerekli işlemleri yap

  111             try

  112             {

  113                 switch (parametreler["komut"])

  114                 {

  115                     case "giris": //Yolladığımız giris mesajına karşılık gelen mesaj

  116                         komut_giris(parametreler["sonuc"]);

  117                         break;

  118                     case "ozelmesaj": //Bir kişiden bize gelen özel mesaj

  119                         komut_ozelmesaj(parametreler["nick"], parametreler["mesaj"]);

  120                         break;

  121                     case "toplumesaj": //Bir kişiden tüm gruba gelen mesaj

  122                         komut_toplumesaj(parametreler["nick"], parametreler["mesaj"]);

  123                         break;

  124                     case "kullanicigiris": //Bir kişi girdiğinde bize gelen bilgi

  125                         komut_kullanicigiris(parametreler["nick"]);

  126                         break;

  127                     case "kullanicicikis": //Bir kişi çıktığında bize gelen bilgi

  128                         komut_kullanicicikis(parametreler["nick"]);

  129                         break;

  130                     case "kullanicilistesi": //Tüm kullanıcıların listesi

  131                         komut_kullanicilistesi(parametreler["liste"]);

  132                         break;

  133                 }

  134             }

  135             catch (Exception)

  136             {

  137 

  138             }

  139         }

Bu fonksyonun yapısı sunucu tarafındakinin hemen hemen aynısıdır (mesajın nasıl çözümlenerek NameValueCollection'a atıldığına dikkat edin). Yapılan şey komutun türüne göre gerekli fonskyonu çağırmaktan ibarettir. Mesajı bu çağırılan fonksyonlar değerlendirecek. Şimdi buradaki mesaj türlerine göre yapılan işlemleri tek tek inceleyeceğiz. Öncelikle biz bağlantıyı sağladıktan hemen sonra sunucuya bir 'giris' komutu gönderdiğimiz için (yukarıda anlatıldığı gibi) sunucu bize cevap olarak başarılı yada başarısız içeren bir giris komutu gönderecektir. Yukarıda görüldüğü gibi komut=giris ise komut_giris fonksyonu çağırılır. Fonksyonun yapısı aşağıdaki gibidir.

  145         private void komut_giris(string sonuc)

  146         {

  147             //giriş başarılıysa gerekli kontrolleri aktif yap

  148             if (sonuc == "basarili")

  149             {

  150                 gbKullanicilar.Enabled = true;

  151                 gbMesajlar.Enabled = true;

  152                 lblNick.Text = nick;

  153                 Text = "Chat İstemcisi - Bağlı";

  154             }

  155             //giriş başarısızsa (nick kullanımdaysa) sonuna 1-9 arası rastgele bir sayı ekleyip yeniden giriş yap

  156             else

  157             {

  158                 int rs = rnd.Next(1, 9);

  159                 nick += rs.ToString();

  160                 istemci.MesajYolla("komut=giris&nick=" + nick);

  161             }

  162         }

Görüldüğü gibi eğer sunucu giris sonucunu basarili olarak göndermişse tek yapılan şey formun üzerindeki kontrolleri aktif yapmak, formun başlığını ve üzerindeki nick alanını değiştirmekten ibarettir. Bundan sonra kullanıcı formu kullanabildiği için gerisi kullanıcıya kalacaktır. Eğer giriş işlemi başarısızsa nick kullanılıyor demektir, nick'in sonuna 1-9 arası rastgele bir sayı eklenerek yeniden giris yapılmaya çalışılır. Giriş işlem başarılı olana kadar da bu işlemin süreceği açıktır (örneğin istemci halil nickini seçer, sunucu başarısız der, istemci halil4 olarak yeniden dener, sunucu yine başarısız der, istemci halil47 olarak dener, sunucu başarılı der ve işlem sonlanır). Eğer başka bir kullanıcı bize özel mesaj göndermişse hatırlayacağınız gibi sunucu bize aşağıdaki gibi bir mesaj gönderiyordu:

komut=ozelmesaj&nick=ahmet&mesaj=selam dostum nasilsin

Yukarıdaki gibi bir mesaj aldığımızda ahmet bize 'selam dostum nasilsin' demiştir. Bu komutu değerlendirip gereğini yerine getiren fonksyonumuz aşağıdaki gibidir.

  169         private void komut_ozelmesaj(string nick, string mesaj)

  170         {

  171             //Eğer bu nick'li kullanıcıyla bir sohbet penceresi açıksa o pencereye referans alalım.

  172             frmOzelSohbet sohbetFormu = null;

  173             //Eşzamanlı erişimlere karşı koleksiyonu kilitleyelim

  174             lock (ozelSohbetFormlari)

  175             {

  176                 if (ozelSohbetFormlari.ContainsKey(nick))

  177                 {

  178                     sohbetFormu = ozelSohbetFormlari[nick];

  179                 }

  180             }

  181             //Bu kişiyle bir sohbet penceresi açık değilse önce sohbet penceresini oluşturup açalım

  182             if(sohbetFormu == null)

  183             {

  184                 sohbetFormu = new frmOzelSohbet(this, nick);

  185                 lock (ozelSohbetFormlari)

  186                 {

  187                     ozelSohbetFormlari.Add(nick, sohbetFormu);

  188                 }

  189                 sohbetFormu.Show();

  190             }

  191             //Mesajı bu pencereye yönlendirelim

  192             sohbetFormu.MesajAl(mesaj);

  193         }

Birisinden mesaj aldığımızda iki durum olabilir; Ya bu mesaj o kişiden gelen ilk mesajdır ve bir konuşma başlatmak istiyordur ya da biz bu kişiyle bir özel mesaj penceresi açmış ve sohbet ediyor durumdayızdır. Bunu anlamak için yukarıdaki fonsyonda ozelSohbetFormlari koleksiyonu inceleniyor (ozelSohbetFormlari SortedList<string, frmOzelSohbet> tipinde bir nesnedir. Anahtar olarak sohbet edilen kişinin nick'ini, değer olarak da bu nick ile yapılan sohbet penceresine referans tutar). Eğer bu koleksiyonda varsa bu kişi ile bir sohbet penceresi açıktır, ve o pencereye olan referans sohbetFormu değişkenine atanır. Eğer koleksiyonda bulunamadıysa bu kişi bir sohbet başlatmak istiyordur (ya da sohbet sırasında biz pencereyi kapatmışızdır ancak karşı taraf bir mesaj daha yazmıştır, her durumda farketmez). Bu durumda yeni bir pencere açıp ozelSohbetFormları koleksiyonuna ekliyoruz. Son olarak da gelen mesajı bu pencereye (MesajAl fonksyonu ile) göndermemiz gerekiyor ki bu pencerede mesajı gösterebilelim (frmOzelSohbet sınıfını daha sonra inceleyeceğiz). frmAna sınıfındaki mesajAlindi fonksyonunda çağırılan bir diğer fonksyon komut_toplumesaj fonksyonudur ve sunucudan bir toplumesaj komutu aldığında çağırılır.

  200         private void komut_toplumesaj(string nick, string mesaj)

  201         {

  202             //gelen mesajı sohbet alanına ekle

  203             string mesajlar = txtTopluMesajlar.Text;

  204             mesajlar += "\r\n" + nick + ": " + mesaj;

  205             txtTopluMesajlar.Text = mesajlar;

  206         }

toplumesaj komutunun değerlendirilmesi çok kolaydır. Yapılan tek şey gelen mesajın ana formdaki mesaj alanında gösterilmesinden ibarettir. kullanicigiris komutunun bir başka kullanıcı sisteme girdiğinde bizim (istemci) ekranımızdaki kullanıcıların listesine bu yeni kullanıcıyı eklemek için sunucu tarafından bize gönderilen bir komut olduğunu biliyoruz. mesajAlindi fonksyonu bir kullanicigiris mesajı aldığında bunu aşağıdaki fonksyona gönderir.

  212         private void komut_kullanicigiris(string nick)

  213         {

  214             //Eğer kullanıcı 'kullanıcılar' listesinde yoksa listeye ekle

  215             lock (kullanicilar)

  216             {

  217                 if (!kullanicilar.Contains(nick))

  218                 {

  219                     kullanicilar.Add(nick);

  220                 }

  221                 kullanicilar.Sort();

  222             }

  223             //Ekrandaki listeyi güncelle

  224             kullaniciListesiniGuncelle();

  225             //Eğer bu kullanıcıyle bir sohbet penceresi açıksa, pencereye bilgi gönder

  226             frmOzelSohbet sohbetFormu = null;

  227             lock (ozelSohbetFormlari)

  228             {

  229                 if (ozelSohbetFormlari.ContainsKey(nick))

  230                 {

  231                     sohbetFormu = ozelSohbetFormlari[nick];

  232                 }

  233             }

  234             if (sohbetFormu != null)

  235             {

  236                 sohbetFormu.KullaniciGirdi();

  237             }

  238         }

Bu fonksyon önce kullanicilar koleksiyonuna (eğer yoksa) bu kullanıcıyı ekler ve listeyi sıralar. Daha sonra kullaniciListesiniGuncelle fonksyonunu çağırır (bu fonksyon kullanicilar listesini ekranda yenilemekten başka bişey yapmaz). Son olarak yapılan işlem de önemlidir. Burada eğer bu kullanıcı ile bir sohbet penceresi açıksa bu pencereye (236. satırda) kullanıcının yeniden girdiği haber verilir (Aslında sistemimizi anladıysanız eğer, ahmet isimli bir kullanıcı çıkış yapıp yerine başka ahmet isimli kullanıcı girerse diğer kullanıcılar bu kişinin aynı ahmet olup olmadığını bilemez, dolayısıyla nick kaydetme olmadığı için sistemimiz çok güvenli değildir (hatta başka eksiklikler de vardır) ancak bu makalede amacımız bu tip ayrıntıları yapmak yerine genel anlamda bir chat sistemi geliştirmek ve ayrıntıları okuyucunun hayal gücüne bırakmaktır). Bir kullanıcı sistemden çıktığında sunucunun diğer tüm kullanıcılara bir kullanicicikis mesajı gönderdiğini yukarıdaki anlatılanlar çerçevesinde biliyoruz. Şimdi bakalım bir istemci sunucudan bir kullanicicikis mesajı aldığında ne yapıyor.

  244         private void komut_kullanicicikis(string nick)

  245         {

  246             //Eğer kullanıcı 'kullanıcılar' listesinde varsa listeden sil

  247             lock (kullanicilar)

  248             {

  249                 if (kullanicilar.Contains(nick))

  250                 {

  251                     kullanicilar.Remove(nick);

  252                 }

  253             }

  254             //Ekrandaki listeyi güncelle

  255             kullaniciListesiniGuncelle();

  256             //Eğer bu kullanıcıyle bir sohbet penceresi açıksa, pencereye bilgi gönder

  257             frmOzelSohbet sohbetFormu = null;

  258             lock (ozelSohbetFormlari)

  259             {

  260                 if (ozelSohbetFormlari.ContainsKey(nick))

  261                 {

  262                     sohbetFormu = ozelSohbetFormlari[nick];

  263                 }

  264             }

  265             if (sohbetFormu != null)

  266             {

  267                 sohbetFormu.KullaniciCikti();

  268             }

  269         }

Burada yapılan da neredeyse kullanicigiris komutunda yapılanın tersidir, kullanıcı varsa listeden siliniyor, ekranda liste güncelleniyor, son olarak eğer bu kullanıcı ile bir özel sohbet penceresi açıksa bu pencereye kullanıcının çıktığı haber veriliyor. Sunucudan istemciye gönderilen son mesaj çeşidi ise kullanicilistesi mesajı. Bu mesaj ilk giriş yaptıktan sonra sistemdeki tüm kullanıcıların listesini bir seferlik almak amacıyla gönderilir. Aşağıdaki fonksyonda da görüldüğü gibi yapılan tek şey kullanicilar listesini yeniden oluşturup ekranda göstermekten ibarettir. Gelen listedeki kullanıcıların nicklerinin ',' ile ayrılmış olduğunu biliyoruz, buna göre gelen mesajı bölüyoruz.

  275         private void komut_kullanicilistesi(string liste)

  276         {

  277             //Tüm kullanıcıları temizle ve gelen listeye göre yeniden oluştur

  278             try

  279             {

  280                 //Gelen mesajı , ile ayır

  281                 string[] kullaniciDizisi = liste.Split(',');

  282                 lock (kullanicilar)

  283                 {

  284                     //Mevcut listeyi temizle

  285                     kullanicilar.Clear();

  286                     //Gelen listeyi ekle

  287                     kullanicilar.AddRange(kullaniciDizisi);

  288                 }

  289             }

  290             catch (Exception)

  291             {

  292 

  293             }

  294             //Ekrandaki listeyi güncelle

  295             kullaniciListesiniGuncelle();

  296         }

Buraya kadar sunucudan gelen mesajları nasıl işlediğimizi inceledik. Şimdi kullanıcıyla etkileşim ve gerektiğinde sunucuya mesaj göndermekle alakalı kısımlara bakacağız. Kullanıcı birisine özel mesaj göndermek istediğinde sunucuya bir ozelmesaj komutunu içeren mesaj göndermemiz gerektiğini (protokolümüzü hatırlarsak) biliyoruz. Özel mesajlaşmalar ana formdan değil özel mesajlaşma formundan (frmOzelSohbet) yapıldığı için bu formdan yazılan mesajların sunucuya iletilmesi için bir yönteme ihtiyaç var. Bunu sağlayan fonksyon frmAna sınıfının içerisinde aşağıdaki gibi tanımlanmıştır. Birisine özel mesaj gönderileceği zaman bu fonksyon çağırılır.

  303         public void OzelMesajYolla(string nick, string mesaj)

  304         {

  305             istemci.MesajYolla("komut=ozelmesaj&nick=" + nick + "&mesaj=" + mesaj);

  306         }

Yukarıdaki fonksyonda sadece protokol kurallarına uygun olarak sunucuya bir ozelmesaj komutu yollandığı görülüyor. Kullanıcının odadaki tüm kullanıcıların görebileceği şekilde toplu mesaj atması için ana formda alttaki textbox'a yazıp enter'a basması gerekiyor. Bu yüzden bizim textbox'un KeyPress olayına gerekli kodları yazmamız lazım.

  378         private void txtTopluMesaj_KeyPress(object sender, KeyPressEventArgs e)

  379         {

  380             //Enter'a basılmışsa ve textbox'da bir metin varsa sunucuya yolla

  381             if (e.KeyChar == (char)13 && txtTopluMesaj.Text.Length > 0)

  382             {

  383                 //Mesajı kontrol et, uygunsa yolla

  384                 if (mesajGonderimeUygun(txtTopluMesaj.Text))

  385                 {

  386                     //mesajı sunucuya yolla

  387                     istemci.MesajYolla("komut=toplumesaj&mesaj=" + txtTopluMesaj.Text);

  388                     //tuşa basılmayı iptal et ( basılan enter tuşunu dikkate alma )

  389                     e.Handled = true;

  390                     //yazı alanını bir sonraki mesaj için boşalt               

  391                     txtTopluMesaj.Text = "";

  392                 }

  393                 else

  394                 {

  395                     //Mesaj uygun değilse uyarı göster

  396                     MessageBox.Show("Göndermek istediğiniz mesajda uygun olmayan karakterler var. Mesaj içerisinde şu karakterler olamaz: < > & =", "Dikkat!", MessageBoxButtons.OK, MessageBoxIcon.Warning);

  397                     return;

  398                 }

  399             }

  400         }

Yukarıdaki kodda kullanıcı enter'a basmışsa (enter tuşunun ASCII kod tablosundaki değeri 13'tür) ve bişeyler yazmışsa if şartı doğru olur ve mesaj sunucuya iletilmelidir. Ancak burada (mesajGonderimeUygun fonksyonu ile) bir kontrol daha yapılır ve kullanıcının yazdığı mesajda <, >, & veya = işaretleri kullanılmışsa uyarı verilir (Biliyoruz ki bu işaretler bizim protokolümüzün özel işaretleridir ve bunlar mesaj içinde geçerse sunucu mesajı yanlış yorumlayacaktır). Eğer mesaj uygunsa sunucuya bir toplumesaj komutu gönderilir.

Kullanıcı ana ekranda sağdaki diğer kullanıcılardan birisinin üzerine çift tıklandığında, tıkladığı kişi ile özel bir sohbet penceresinin açılması gerekiyor. Eğer tıkladığı kişi ile zaten bir sohbet penceresi açıksa sadece bu pencere aktif yapılır (odaklanılır ve ekranın en önüne getirilir). Aşağıdaki kod tam olarak bu işi yapar. ozelSohbetFormlari koleksiyonu o anda aktif olan özel sohbet pencerelerini saklar. Dolayısıyla açılan yeni sohbet penceresini bu koleyksiyona ekliyoruz.

  346         private void lstKullanicilar_MouseDoubleClick(object sender, MouseEventArgs e)

  347         {

  348             //Seçilen eleman varsa..

  349             if (lstKullanicilar.SelectedItems.Count > 0 && lstKullanicilar.SelectedItem != null)

  350             {

  351                 //Seçilen kullanıcının nick'inin al

  352                 string secilenNick = lstKullanicilar.SelectedItem as string;

  353                 if (secilenNick != null)

  354                 {

  355                     lock (ozelSohbetFormlari)

  356                     {

  357                         //Eğer bu kullanıcı ile bir sohbet formu zaten açıksa formu aktif yap,

  358                         //değilse yeni bir özel sohbet formu aç ve ozelSohbetFormlari listesine ekle

  359                         if (ozelSohbetFormlari.ContainsKey(secilenNick))

  360                         {

  361                             ozelSohbetFormlari[secilenNick].Activate();

  362                         }

  363                         else

  364                         {

  365                             frmOzelSohbet sohbetFormu = new frmOzelSohbet(this, secilenNick);

  366                             ozelSohbetFormlari.Add(secilenNick, sohbetFormu);

  367                             sohbetFormu.Show();

  368                         }

  369                     }

  370                 }

  371             }

  372         }

Ana formda inceleleyeceğimiz son fonksyon OzelSohbetFormuKapandi fonksyonudur. Bu fonksyon kapatılan özel bir sohbet formunu ozelSohbetFormlari koleksiyonundan silmeye yarar. Bir özel sohbet formu kapatılırken bu fonksyon çağırılır. Özel sohbet formunu birazdan inceleyeceğiz.

  312         public void OzelSohbetFormuKapandi(string nick)

  313         {

  314             lock (ozelSohbetFormlari)

  315             {

  316                 if (ozelSohbetFormlari.ContainsKey(nick))

  317                 {

  318                     ozelSohbetFormlari.Remove(nick);

  319                 }

  320             }

  321         }

Özel sohbet formu adından anlaşılacağı gibi kullanıcının bir başka kullanıcı ile özel sohbet edebileceği bir penceredir. Bu formun kodları frmOzelSohbet sınıfından tanımlanmıştır. Bu sınıfın kurucu fonksyonu aşağıdaki gibidir.

   23         public frmOzelSohbet(frmAna anaForm, string nick)

   24         {

   25             InitializeComponent();

   26 

   27             this.anaForm = anaForm;

   28             this.nick = nick;

   29 

   30             baslikYaz();

   31         }

Burada kurucu fonksyonun 2 parametre aldığı görülüyor. Birinci parametre ana form'a referanstır ki bu referans vasıtasıyla ana form ile iletişim kurularak mesaj gönderme gibi işlemler yapılır. İkinci parametre ise karşı tarafın nick bilgisidir. Bildiğimiz gibi sunucudan mesajları alma işini frmAna sınıfı (yani ana form) gerçekleştiriliyor. Gelen mesaj eğer bir ozelmesaj ise mesajın özel sohbet penceresine gönderilmesi gerekir, bunu sağlamak için frmOzelSohbet formu MesajAl adında bir fonksyon tanımlar. Bu fonksyon sadece gelen mesajı ekrandaki mesaj alanına ekler.

   37         public void MesajAl(string mesaj)

   38         {

   39             string mesajlar = txtOzelMesajlar.Text;

   40             mesajlar += "\r\n" + nick + ": " + mesaj;

   41             txtOzelMesajlar.Text = mesajlar;

   42         }

frmOzelSohbet formunun KullaniciGirdi ve KullaniciCikti fonksyonları ise sohbet edilen kullanıcı giriş çıkış yaptığında frmAna sınıfı tarafından çağırılır (yukarıda açıklandı). Bu fonksyonlar sadece kullanıcıyı bilgilendirir ve eğer karşı taraf çıkmışsa mesaj yazılmasını önlemek için gerekli kontrollerin Enabled özelliğini false yapar. Kullanıcı özel sohbet formunda bir mesaj yazıp enter'a bastığında ise txtOzelMesaj_KeyPress fonksyonu çalışır ve kullanıcının yazdığı mesajı kontrol ettikten sonra eğer uygunsa ana formun MesajYolla fonksyonu ile mesajı sunucuya gönderir. Bu üç fonksyon için kodları inceleyiniz. Makalemizde inceleyeceğimiz son fonksyon özel sohbet formu kapandığında gerçekleşen FormClosed olayı için yazılmış olan frmOzelSohbet_FormClosed fonksyonudur.

  117         private void frmOzelSohbet_FormClosed(object sender, FormClosedEventArgs e)

  118         {

  119             anaForm.OzelSohbetFormuKapandi(nick);

  120         }

Burada yapılan işlem sohbet formu kapatıldığında bunu ana form'a bildirmektir. Ana form da yukarıda daha önce açıklandığı gibi bu pencereyi ozelSohbetFormlari koleksiyonundan çıkarır.

Sonuç

Üç makalelik bu makale dizimizde önce çok temelden TCP üzerinden byte düzeyinde iletişimi inceledik, ardından iletişimi kolaylaştırmak için bir kütüphane geliştirdik, bu makalemizde ise bu kütüphane (ASMES) üzerine basit bir chat sistemi inşa ettik. Makalede öncelikle chat sisteminin mimarisini anlattık, daha sonra sunucu istemci arasında bir iletişim protokolü geliştirdik, son olarak da bu geliştirdiğimiz protokole göre ASMES kütüphanesi üzerinden mesajlaşan bir sunucu ve bir istemci uygulaması yazdık.

Socket programlama iki uygulamanın iletişimi için var olan yollardan birisidir. Genel anlamda birbirinden bağımsız uygulamaların (proseslerin) iletişimine Interprocess Communication (Prosesler arası iletişim) ismi verilir. Named Pipes, Message Queue, Web Servisleri.. v.s. bu işlemi gerçekleştirmek için yöntemlerden bazılarıdır. Hepsinin kendisine göre avantajları ve dezavantajları vardır.

Soket programlada iki uygulama farklı bilgisayarlarda, farklı işletim sistemleri üzerinde hatta farklı programlama dilleri ile yazılmış olabilir. TCP/IP standart bir protokol olduğu için bu şekildeki iki uygulama dahi hiçbir zorluk olmadan doğrudan haberleşebilirler. TCP protokolünü kullanarak yapılan socket programlama modelinin bir iyi özelliği de çift taraflı olmasıdır. Bu makalelerde açıklandığı gibi sunucu ve istemci birbirlerine eşzamansız (Asenkron) olarak mesaj gönderebilirler. Bu da talep/cevap (request/response) mantığından daha esnek ve programcıya imkan veren bir yapıdır. Talep/Cevap mantığında istemci sunucuya bir talep yapar ve cevabı alır. Talep yapmadığı sürece sunucu istemciye bişey gönderemez. Bu talep/cevap modelinin en büyük dezavantajıdır. Bu makalede geliştirdiğimiz sistemi talep/cevap mimarisine göre yapmış olsaydık istemciler döngüsel olarak sürekli sunucuya 'bana gelen yeni bir mesaj var mı' sorusunu sormalı, sunucu varsa gelen mesajı vermeli yoksa yok demelidir. Birçok istemci aynı sunucuya bağlı olduğu durumda bunun ne kadar büyük bir performans kaybına neden olduğu açıktır. Hatta mesajların çabuk iletilmesi için istemci bu soruyu çok sık (mesela saniyede bir) sormalıdır (sunucu da mesajları ilgili istemci alana kadar saklamalıdır). Bu durumda performans kaybı daha da artar ama buna rağmen yine de gerçek zamanlı olmaz (ortalama 0.5 saniye gecikmeli olur) mesajlaşmalar. Web servisi mimarisi mecburen talep/cevap üstüne kuruludur. Neyse ki Microsoft, .NET framework 3.0 ile WCF (Windows Communication Foundation) teknolojisini duyurdu ve çift taraflı eşzamanlı iletişime de imkan veren bir framework geliştirmiş oldu. Yine de WCF, TCP protokolünü kullanarak sadece diğer .NET projeleriyle iletişim kurabilir ancak bizim bu makalede geliştirdiğimiz chat sistemi, kendi tanımladığımız mesajlaşma protokolünü uygulayan herhangi bir programlama dilinde yazılmış uygulamayla ve herhangi bir platform (işletim sistemi) ile haberleşebilir. WCF ise bunu başarmak için Http protokolünü kullanan Xml mesajlarıyla iletişim kurmayı çözüm olarak geliştirmiştir. Yine de TCP kadar alt düzeyde, hızlı ve platform bağımsız değildir.

Bu makalelerde geliştirilen ASMES kütüphanesi birçok ihtiyacımızı görmekle beraber tamamen yeterli olmadığı durumlar olabilir. En başta mesajları ASCII formatında gönderip aldığı için standart bir iletişim yöntemi sağlamakla beraber Türkçe karakterlerde problemler meydana gelir. Ayrıca < ve > işaretlerini mesajlarda kullanmaya izin vermez. Aslında bu basitçe üstesinden gelinebilecek bir durumdur. Mesaja başlangıç ve bitiş işaretleri koymak yerine her mesajın başına mesajın uzunluğunu yazarsak ve önce bu uzunluğu okuyup sonra buna göre devam eden mesajı okursak bu problemi çözmüş oluruz. Bu makelelerde amaç mükemmel bir protokol geliştirmekten öte bir modeli açıklamak olduğu için buna gerek görülmemiştir. ASMES kütüphanesi ayrıca %100 hata kontrolü yapan bir kütüphane değildir ama büyük ölçüde hata töleranslıdır, istemci ile iletişim kurulmadığında istemci ile olan bağlantıyı hata vermeden kapatır ya da sunucu dinleme işinde problem olursa bir süre bekleyip yeniden dinlemeye başlar. TCP protokolü temelde byte dizisi ile alışveriş yapabildiği için aslında TCP üzerinden .NET nesnelerinin serileştirilerek transferinin mümkün olduğu da açıktır ancak bu projede bu da yapılmamış daha zor ve alt düzey mesajlaşma gerçekleştirilmiştir (İlk makalemde BinaryWriter / BinaryReader ile string gönderip almaya bakabilirsiniz). ASMES kütüphanesinin en iyi yani ise üst katmanda basit birkaç kod yazdıktan ve bir protokol belirledikten sonra, karşı tarafın bir fonksyonunu çağırmak kadar olmasa da buna yakın bir kolaylıkla iletişim imkanı sunmasıdır.

Bu makalede geliştirilen chat sistemideki en büyük eksiklik odaların bulunmayışıdır. Sistemdeki herkes aynı odadadır. Bunu gerçekleştirmek aslında zor değildir, kendi mesajlaşma protokolümüzde yapılacak bazı değişikliklerle bu yapı geliştirilebilir ancak burada basitliği ön planda tuttuğumuz için bu kadar detaya girmek istemedik.

Neticede multithread TCP socket programlama kolay bir konu olmamakla birlikte bu makaleleri anladıysanız artık büyük ölçüde hakimsiniz demektir. Size programınızın dışına çıkıp diğer programlarla iletişim imkanı verir. Bu dahi tek başına programcıyı heyecanlandırmaya yetecek bir konudur

İleride socket programlama ya da diğer konulardaki makalelerimde yeniden buluşmak üzere, hoşçakalın.

Proje