C++ Üzerinde Soket Programlama

********************************************************************

Evet, renklendirmesi biraz uzun sürdü ama sonunda ilk kodu ekledim. Şimdi kodu satır satır anlatmaya başlıyorum…

Windows.h dosyasını neden eklediğimizi biliyorsunuz herhalde J. Değişken tanımlamaları bölümünü de geçiyorum..

FD_ZERO(&anatanim); // ana kümeyi ve gecici kümeyi temizle

FD_ZERO(&gecici);  

Bu iki satırla kullanacağımız iki dosya tanımlayıcı kümeyi sıfırlıyoruz…

Neden iki tane dosya tanımlayıcı kullandığımızı birazdan antalacağım.

//----------------------------- 

  if( (ss=socket(AF_INET, SOCK_STREAM, 0))==-1 )

       fprintf(stderr,"Sunucu soket hatasi!\n");

//----------------------------- 

  if( (bind(ss, (struct sockaddr *)&sunucu,sizeof(struct sockaddr) ))==-1 )

      fprintf(stderr,"Bind fonksiyonu yurutulemedi!\n");

//-----------------------------

  if (listen(ss,10) == -1)

     fprintf(stderr,"Port dinlenemiyor!\n");

//-----------------------------

Önce socket fonksiyonu ile bir soket yaratıyoruz ve ss soket tanımlayıcımıza atıyoruz. Daha sonra bu soket tanımlayıcımızla önceki sunucu adındaki sunucu bilgilerini tutan yapıyı bind ediyoruz.

Artık soketimiz sahipsiz değil J son olarak soketimiz ile ilgili portu(5000) dinlemeye koyuluyoruz.

Listen’in ikinci parametresini hatırlatacak olursak, maksimum kaç kişinin sunucumuza bağlanabileceğini belirtiyor. Ben 10 yaptım siz 10000 de yapabilirsiniz ;)

FD_SET(ss, &anatanim);

Burada dinleme yaptığımız  ss soketimizi ana tanimlayici kümeye yerleştiriyoruz.. Bir noktayı hatırlatayım, readfds tanımlayıcı kümesi içindeki soketlerden birine veri gelince yani okunmaya hazır olunca uyarı veriyordu, ancak bir bağlantı isteği gelmesi de veri gelmesine eşdeğerdir. Yani dinleme yaptığımız soketi bu kümeye yerleştirirsek bağlantı isteği geldiğinde haberimiz olabilir…

   for(;;) {                   // Ana for döngüsü

      gecici = anatanim;

if(select(0, &gecici, NULL, NULL, NULL)==-1)

fprintf(stderr,"select fonksiyonu yurutulemedi\n");

            i=gecici.fd_array[0];

Ana for döngümüz programın bel kemiği. Her döndüğünde select bölümünde beklenir, select fonksiyonu bloklayan bir fonksiyondur. Açıklayacak olursak kod select fonksiyonuna geldiğinde fonksiyonun dördüncü parametresi kadar süre, tanımlayıcı kümelerindeki soketlerden birine uyarı gelsin diye bekler.

Biz dördüncü parametreyi NULL yani 0 geçtik. Doğal olarak bir uyarı gelene kadar sürekli bekler. Ayrıca üç dosya tanımlayıcı kümesini parametre olarak geçebilirdik ama sadece readfds yi geçmek işimizi görüyor bu programda. Sonuçta amacımız gelen verileri alıp diğer istemcilere iletmek .

Daha sonra anatanim kümemizi  gecici kümemize kopyalıyoruz. Bunu her select çağırılmadan önce yapıyoruz. Nedenine gelecek olursak, biz dosya tanımlayıcı kümelerine kontrol için 1000 dosya da versek, o ilk uyarı gelen soketi bize bildirir ve select fonksiyonunu bitirir. Yani selecte girdiğinde gecici kümesinde birsürü soket olabilir ancak çıkışta sadece okumaya hazır uyarısı veren soket kalır.

İ

İHatırlarsanız, struct fd_set yapısının iki elemanı vardı. Bunlardan birisi  soket kümesini tutan SOCKET fd_array[..] dizisi , diğeri ise dizideki eleman sayısını tutan unsigned int fd_count idi.

İşte buradaki  i=gecici.fd_array[0]; komutu ile uyarı veren soketi yani okumaya hazır olan soketi “i” değişkenimize aktarıyoruz.

Fd_array[..] dizisinde select fonksiyonuna girmeden önce içinde birçok soket olmasına rağmen çıktığında sadece bir soket oluyordu, işte o sokette ilk elemanında yani fd_array[0] da bulunur.

Sanırım her selecte girmeden önce ana kümemizi neden gecici kümesine tekrar kopyaladığımızı anladınız…

Tüm soket fonksiyonları gibi select de hata durumunda -1 döndürüyor, bazıları hata kontrolünde neden printf(“hata var dostum”); yerine fprintf(stderr,”hata var dostum”);

Yazdığımı merak edebilir. Aslında çok gerekli değil. Görünürde aynı işi yapar. Tek fark birincisi hata iletisini stdout dosyasına ikincisi stderr dosyasına yazar.

Windows ve unix işletim sistemlerinde üç önemli dosya vardır. Bunlar standart giriş, standart çıkış ve standart hata dosyalarıdır. Standart çıkış dosyasına yazılanlar bilgisayarın standart çıkışında(genelde monitör) görülür. Aynı şekilde standart girişden (genelde klavye) girilen verilerder de standart giriş dosyasına yazılır.

İstenirse standart giriş ve çıkış dosyaları başka giriş ve çıkış birimlerine yönlendirilir. Mesela standart çıkış dosyası yazıcıya yönlendirilirse hata mesajı yazıcıdan çıktı olarak alınır. Ya da herhangi bir dosyaya yönlendirilirse o dosyanın içine yazılır. Ancak standart hata dosyası yönlendirilemez, daima ekrana basar hata mesajını.

Stdout : standart çıkış

Stdin   : standart giriş

Stderr : standart hata

Yani stdout monitöre yönlendirilmiş olduğundan printf(“selam”) ile fprintf(stdout,”selam”) arasında hiçbir fark yoktur.

Konuyu biraz dağıttım ama neyse J kodumuza geri dönelim..

            if (i == ss) {

               boyut = sizeof(istemci); 

               is = accept(ss, (struct sockaddr *)&istemci,&boyut);           

                  FD_SET(is, &anatanim);             // ana listeye ekle

  printf("Istemci baglandi: %s\n",inet_ntoa(istemci.sin_addr));

                   

Arkadaşlar hatırlarsanız bir bağlantı isteği geldiğinde readfds kümesi soketimizi okumaya hazır olarak bildiriyordu, ve select fonksiyonu sona eriyordu.

Biz de ss soketimizi yani dinleme yaptığımız soketimizi readfds tanimlayicisina kopyaladığımıza göre bir bağlantı isteği geldiğinde select fonksiyonu bitecek ve uyarı gelen soket “gecici” dosya tanımlayıcı kümesinde hazır bulunacak.

Toparlarsak, nasıl ki herhangi bir istemciden bir veri geldiğinde select bu soketi bize bildiriyorsa dinlediğimiz sokete de bir bağlanma isteği gelince aynı uyarıyı verecek.

İşte bizim gelenin bir veri mi yoksa bir bağlantı isteğimi olduğunu kontrol etmemiz lazım. Eğer uyarı dinlediğimiz sokete geldiyse bunu bir bağlanma isteği sayıyoruz ve accept ile bu isteği kabul ediyoruz.

İf(i==ss) ifadesi doğruysa yani uyarı gelen soket( i değişkenine atamıştık) ss soketi ise gelen bağlantıyı kabul edip is soketine aktarıyoruz. Tabi her gelen bağlantıyı is soketine aktarırsak her seferinde bu soket değişecektir ve biz de her seferinde farklı birisiyle ancak sadece bir kişiyle konuşabileceğiz.

Bunun için is soketini FD_SET makrosu ile ana dosya tanımlayıcı kümemize ekliyoruz.

Artık gelen veri ana dosya tanımlayıcı  kümemizde yani güvende, bir sonraki select işleminden önce anatanim’i gecici’ ye kopyalayacağımız için select fonksiyonu yeni istemcimizden gelen verileri de kontrol edecek..

Son olarak ekrana istemci bağlandı diyoruz ve ip sini yazıyoruz. Hatırlarsaniz accept fonksiyonu bağlantı kurulan istemcinin ip ve port bilgilerini verdiğimiz yapıya aktarıyordu. Accept fonksiyonuna istemci yapısının adresini yollamıştık. O halde istemci.sin_addr elemanında istemcinin ip adresi olacak.

Ama bir dakika, bu ip bilgisi ağ bayt düzenine göre çevrilmiş hali değil mi.gethostbyname fonksiyonu bu işe yarıyordu, evet haklısınız bu yüzden adresi ağ bayt düzeninden bizim anlayacağımız bir stringe çeviren inet_ntoa fonksiyonunu kullanıyoruz. Ntoa network to ascii demektir. Hatırlarsanız bir de htonl vardı, host to network, bu fonksiyon sunucu ip sini ağ düzenine çeviriyordu, ntohl fonksiyonu ise network to host, ağ düzenine göre olan adresi intel düzenine yani little endian yapısına çevirir.

Ancak inet_ntoa doğrudan ağ düzenine göre olan ip yi stringe çevirir. Bu konu o kadar önemli değil ancak yine de bilseniz iyi olur..

else {                   

                   if ((gelenbayt=recv(i, tmp, sizeof(tmp), 0)) <= 0) {

                      if(gelenbayt==0)  fprintf(stderr,"Baglantikoptu \n"); 

                          else      fprintf(stderr,"Baglanti hatasi \n");                                

closesocket(i);

                        FD_CLR(i, &anatanim);

                        }

Bir önceki kodda uyarı veren soketin ss soketi yani dinleme yaptığımız soket olma ihtimalini değerlendirmiştik. Şimdi ise “else-aksi halde “ yani uyarı veren soket daha önceden bağlanmış bulunan soketlerden biriyse ne yapacağımıza bakalım..

Eğer uyarı veren soket ss soketi ise bu bir bağlantı isteği, değilse istemcilerden gelen bir veri demiştik.

Bunun için recv fonksiyonu ile gelen veriyi alıyoruz ve önceden tanımladığımız tmp dizisine aktarıyoruz..

Ancak istemciden gelen veriyi doğrudan kullanmadan önce bir kontrol yapmak zorundayız.. Hatırlarsanız recv fonksiyonunu anlatırken bir özelliğinden bahsetmiştim, recv fonksiyonu bağlantı koparsa sıfır değerini gönderiyordu. Yani gelen veri her hangi bir yazı değil, istemcinin bağlantıyı kopardığı bilgisi de olabilir.

Recv fonksiyonu hata verirse -1 değerini döndürüyordu, o halde öncelikle bu iki ihtimali yani bir sorun olduğu ihtimali birlikte düşünelim, recv den dönen değer 0 veya 0 dan küçükse ;

Altta eğer gelenbayt değişkeninin değeri yani recv den dönen değer sıfır veya sıfırdan küçükse if doğru kabul edilir ve alt satırdaki kontrole geçilir. Burada artık gelenbayt değişkeninin 0 yada -1 olduğunu biliyoruz, bu durumda if(gelenbayr==0) komutuyla istemci ile bağlantı kopmuşmu kontrolü yapıyoruz.. Eğer koptu ise ekrana bağlantı koptu yaz diyoruz.

Eğer bağlantı kopmamışsa yani gelenbayt 0 değilse o halde -1 dir yani bir hata olmuştur, o halde ekrana bağlanti hatasi yaz diyoruz.. Ve bu soketi closesocket fonksiyonu ile kapattıktan sonra FD_CLR makrosu ile ana dosya tanımlayıcı kümemizden çıkarıyoruz…

 Böylelikle bir sonraki for dönüşünde yani bir sonraki select fonksiyonunda bu soket kontrol edilmeyecek..

Bu arada closesocket fonksiyonundan da bahsetmiş olayım, bu fonksiyon soketi sonlandırır ve o soketten birdaha veri alış verişi yapılamaz…

Linux da bu fonksiyon close dir. Yani linux da soketler de herhangi bir dosya gibi close ile kapatılırlar…

Evet iki ihtimal saydık, bunlardan birisinde uyarı veren soket ss idi ve gelen bağlantıyı aldık, diğerinde herhangi bir soketti ancak gelen veri bağlantının koptuğu haberiydi.

Şimdi son ihtimali düşünelim. En güzel ihtimali J yani uyarı veren soket istemcilerden birinin soketi ve gelen veri bağlantı koptuğu haberi değil bir yazı. 

else    

   for(j = 0; j < anatanim.fd_count; j++)                              if(anatanim.fd_array[j] != ss && anatanim.fd_array[j] != i)

         send( anatanim.fd_array[j], tmp, gelenbayt, 0);                             

Demin gelen veri bağlantı koptuğu verisiydi ve gelenbayt değişkeni sıfır veya sıfırdan küçüktü, hatırlarsanız eğer bir hata durumu yoksa recv fonksiyonu aldığı bayt sayısını döndüyordu.

Yani gelenbayt<=0 önermesinin else sinden yani gelenbaytın sıfırdan büyük olması durumundan devam ediyoruz koda.

Bu durumda istemciden gelen verinin kendisi ve dinleme yapan soket hariç tüm soketlere iletilmesi lazım. Tipik bir sunucu istemci sistemi böyle  işler. İstemci bir şey yazar, sunucuya yollar, sunucu da bu veriyi diğer istemcilere yollar. Burada da yapacağımız tam olarak bu..

Peki sunucuya bağlı olan tüm istemci soketlerini nereden bileceğiz? Anatanim dosya tanımlayıcı kümesini bunun için tutuyoruz ya J

Hatırlarsanız  dosya tanımlayıcı kümelerinin iki elemanı vardı, bunlardan birisi fd_count diğeri fd_array[..] idi. Bunlardan fd_array tüm soketlerin listesini , fd_count ise dizinin uzunluğunu yani içindeki soket sayısını tutuyordu. Anatanim dosya tanimlayici kümesinin fd_array[..] elemanı içinde o anda sunucuya bağlı olan tüm istemcileri ve birde dinleme yaptığımız soketi tutar. Fd_count ise bağlı olan istemci soketlerinin sayısını tutar..

O halde biz istemciden aldığımız veriyi diğer tüm istemcilere göndermek için istemci sayısı kadar dönecek bir for döngüsü içinde fd_array dizisindeki tüm elemanlara send fonksiyonu ile verimizi gönderiyoruz.

Dikkat ederseniz orada if deyimiyle sıradaki soketin dinleme yapan soket ve veriyi yollayan soket olup olmadığını kontrol ediyoruz. Bu iki soketten biriyse ona veriyi yollamıyoruz.

Son olarak da WSACleanup(); ile soket programımızı bitiriyoruz.  İlk program olduğu için çok ayrıntılı anlattığımı hatta canınızı sıkmış olabileceğimi biliyorum J sonraki örneklerde sadece kritik noktalar için fazladan açıklamada bulunacağım..

Şimdi de soketi kapatmak için kullanılan closesocket fonksiyonunun bir alternatifi olan shutdown fonksiyonunu anlatayım, shutdown ile soketi tek yönlü yada çift yönlü kapatabiliriz.

Tarih:
Hit: 10590
Yazar: Tugberk



Yorumlar


Siftahı yapan siz olun
Yorum yapabilmek için üye girişi yapmalısınız.