'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.
18 /// ASMES kütüphanesini kullanarak ASMES sunucusuna bağlı olan istemci nesnesi
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)
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"])
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)
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))
219 kullanicilar.Add(nick);
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))
262 sohbetFormu = ozelSohbetFormlari[nick];
264 }
265 if (sohbetFormu != null)
267 sohbetFormu.KullaniciCikti();
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.