Başlangıç > Dökümanlar > C++ ile soket programlama

C++ ile soket programlama

C dili ile ilgili onlarca kitap yayınlanmasına rağmen soket programlama konusuna hiç değinilmez. Tamamen derleyici eklentisi olan Turbo C++ grafik pakedi bile C kitaplarının %90 ında bulunur.

Bu dökümana başlama sebeplerimden birisi de budur. Soklet programlama ile ilgili internette az da olsa kaynak bulabilirsiniz ancak yazılan örnek programlar aşırı basit olduğu için öğrenilen birkaç şey de hemen unutuluyor ve birçok kişi ciddi bir uygulama yazamadan bu işten soğuyor.

Yani bu dökümanın amacı adam gibi, işe yarar programlar yazabilecek kadar soket programlama bilgisi ve örnekleri vermektir.

Soket programlama fonksiyonları ansi c de olmasa da sistemden bağımsızdır, yani windows da yazılmış bir soket program birkaç küçük değişiklikle linux da, bsd de ya da solaris de çalıştırılabilir.

Program yazmaya başlamadan önce hangi derleyiciyi kullanacağınıza karar vermelisiniz, Turbo C demeyin sakın, borland firması bile müzeye koydu biz hala  Turbo C ile 16 bit dos programı yazıyoruz. Neyse ben en sık kullanılan iki C derleyicisine göre anlatayım.

Eğer Dev-Cpp idesini kullanıyorsanız GCC derleyicisini kullanıyorsunuz demektir. GCC de kütüphane dosyası eklemek için –l  anahtarını kullanmanız gerekir. Bunun için Dev-C++ da “araçlar/derleyici ayarları” yazan  yere geliyoruz ve “Derleyiciyi çağırırken komut satırına şunları ekle” yazan yeri işaretliyoruz. Altındaki boşluğa ise “-lwsock32” yazıyoruz.

Evet artık Dev-C++ da soket programlama yapabilirsiniz.

Diğer çok kullanılan derleyici Visual C++ da ise “project/settings/link/” yazan yere gelip, “object-library modules” yazan yerin en sonuna bir boşluk bırakıp “wsock32.lib” yazıyoruz. Olay bukadar.

Ben dev-cpp de kod yazmayı tercih ediyorum, hem ide’yi, renklendirmeleri, kendinize göre ayarlıyabiliyorsunuz, hemde tamamen Türkçe ayrıca gcc derleyicisini kullanıyor. Linux altında da bu derleyiciyi kullandığımız için programınızın taşınabilirliği de artıyor. (derleyici eklentilerini kullanırsak)

Evet artık gerçekten başlayabiliriz. Kısaca soket programlama nedir ne değildir ondan bahsedeyim.

Soket programlama kısaca Ağ programlama olarak açıklana bilir, yani yerel ağ ya da internet ağı için program yazıyorsanız bu program soket program oluyor.

Soketler internete bağlanmak için kullanılan nesnelerdir. İnternete bağlanan tüm programlar soketleri kullanır. Soketler aslında işletim sistemi tarafından birer dosya olarak görülür.

Mesela tahribat.com a programımızdan bağlanmak istiyoruz, bu durumda soketimize sunucu adını ve bağlanmak istediğimiz portu bildirirz ve bağlan deriz, sonra ister veri yollar ister veri çekeriz.  Her soketle tek sunucuya veya istemciye bağlanabiliriz, eğer sunucuysak birden fazla istemci ile bağlantı kurmamız gerekecektir. Bu durumda tüm istemciler için ayrı birer soket açmak durumundayız.

Eğer amacınız bir client-server programı yazmak ise (trojanlar da bu gruba girer) istemci programları visual basic gibi daha yüksek seviye bir dilde yazmanızı tavsiye ederim. Mesela bir chat programı yazacaksanız, asıl işi yapan sunucu programı C ile yazarsınız ve shell hesabınızdan ya da çok kişi bağlanmayacaksa kendi bilgisayarınızdan çalıştırırsınız,  istemcileri de görsel programlama dillerinden birinde yazarsınız, tabi benim gibi konsol da chat yapmakdan zevk alıyorsanız orasını bilmem.

Aslında borland C++ 6 ile visual basic kadar hata bence daha kolay bir şekilde görsel programlar yazarsınız ancak  ben C ile öyle pencereli programlar yazmaktan hoşlanmıyorum.

C sistemler üstü bir dil olduğu için ve işletim sistemleri onunla yazıldığı için C de yadığınız kodun taşınabilir olması daha iyidir.  Siz iyisimi istemciyi visual basic de yazın, bende öyle yapacam  J

Bu kadar teorik bilgiden sonra artık birşeyler karalayabiliriz sanırım..

Windows programcıları windows.h dosyasını eklemeliler, aslında winsock.h dosyasını eklemek kafi ancak zaten windows.h dosyası bu dosyayı da ekliyor, hem apileri de kullanacak olursak birdaha ekleme derdimiz olmaz.

#include

#include

int main(){

Windows da soket programlama WSAStartup fonksiyonu ile başlar ve WSACleanup fonksiyonu ile biter. Linux da böyle bir olay yok tabi herşey doğal. Bill amcanın işleri işte..

WSAStartup iki parametre alır, birisi winsock bilgilerini tutacak olan bir yapının adresi WSADATA türünde, diğeri ise winsock un versiyon numarası. Bu fonksiyonla hiçbir işimiz yok, her programda standart olarak yazacaz o yüzden fazla uzatmıyorum ve yazıyorum..

WSADATA wsdata;                      //bilgiler buraya yazılacak, biz ilgilenmiyoruz.

WSAStartup(MAKEWORD(2,0),&wsdata);  // makeword verilen versiyon numarasını word e çevirmek için kullandığımız bir makro.

Gelelim soket yaratma olayına, öncelikle soket değişkenini tanımlamamız gerekiyor. Türü ne mi dersiniz, SOCKET J

SOCKET skt;

Soketimizi tanımladık, SOCKET tipi aslında basit bir tamsayıdır, int ten farkı yoktur ancak winsock.h da SOCKET olarak typedef edilmiştir. Soketin bir dosya olduğunu söylemiştik,  soketi yaratan fonksiyon da dosya numarası ile geri döner, işte bu numara skt değişkenimize atanacak.

İsterseniz soketi oluşturan fonksiyonuda yazalım, adı ne dersiniz?, socket J

Skt=socket(AF_INET, SOCK_STREAM,0);

AF_INET standarttır adres family demek , SOCK_STREAM soketin türü mesela tcp ise SOCK_STREAM, udp ise SOCK_DGRAM olacak. Son parametre de her zaman 0 olacak, soket tipi parametresinden  uygun tipi seçmesi için varsayılan 0 dır.

Fonksiyon başarılı olursa soketin numarasını, başarısız olursa -1 değerini döndürür. Bence bir programın kalitesi o programda hata denetiminin kullanılıp kullanılmadığı ile anlaşılır. Bu yüzden muhakkak hata kontrolü uygulayın. Yazacağınız çok bir şey değil..

if((Skt=socket(AF_INET, SOCK_STREAM,0))==-1) printf(“soket açılamadı dostum üzgünüm\n”);

Evet artık bir tcp soketimiz var, ancak bu sokete hangi sunucuya bağlanacağını, hangi porttan bağlanacağını ya da hangi portu dinleyeceğini nasıl anlatacağız?

İşte bunun için bir veri yapısı kullanılır, bu soket ile veri yapısı bind edilir ve artık o soketin herşeyi belli olmuştur, veri alışverişine hazırdır.

Bahsettiğim veri yapısı sockaddr_in , bu yapının tanımlamasını yapalım ve elemanlarını dolduralım..

struct sockaddr_in bilgiler;  // bilgiler in elemanlarını soket bilgileri ile dolduralım ki soketimiz nereye bağlanacağını vs bilsin..

bilgiler.sin_family = AF_INET;

bilgiler.sin_port = htons(PORT);

iki bilgisini doldurduk bile, sin_family her zaman af_inet kalacak,  Port yazan yere portumuzu yazacaz mesela 5000, htons, “ host to network short” demektir. Yani ağda kullanılan standart yazım sistemi ile intel işlemcileri terstir. İntel işlemcileri little endian, kullanır yani mesela intel işlemcisi ile belleğe AC F5 baytlarını yolla dersneiz belleğe F5 AC diye yazar. Ancak ağda kullanılan standart böyle olmadığı için 2 baytlık verileri htons 4 baytlıkları htonl ile ağ standartına çevirmemiz lazım..

bilgiler.sin_addr = INADDR_ANY;

sin_addr elemanı sunucu adını yani ip veya alan adını alır, tabi aslında aldığı değer long türden bir tamsayıdır ancak biz alan adını uygun şekilde düzenleyip içine atacaz.  Burada önemli bir ayrıntı var, eğer programımız sunucuysa soket bilgi yapısının  sunucu adresi bölümüne bir şey yazmanın bir anlamı var mı? Zaten kendisi sunucu ve belli bir portu dinleyecek ve istemciler ona bağlanacak.

İşte eğer sunucu iseniz sin_addr elemanına INADDR_ANY değerini geçmelisiniz. Bu herhangi bir adres çağrışımı yapıyor ama kendi adresimizi yazar bu elemana..

Evet sunucuysak bu elemanı doldurmak kolay, peki ya istemciysek, mesela adam www.tahribat.com diye adresi girdi, bunu long türden bir tamsayı olan adres bilgisine nasıl çeviririz, ya da 88.54.256.55 gibi bir ip adresi girildi bunu nasıl bu elemana aktaracağız.

İşte burada işler biraz karışıyor ama ilerde daha iyi anlayacaksınız, şimdilik bu vereceğim çevirme fonksiyonlarına fazla takılmayın , zaten bunlar rutin işlerdir, programı yazarken sadece bir kere doldurulur…

Gelelim olayın ayrıntılarına, kullanıcıya sunucu adını girmesini söyledik ve bekliyoruz, kullanıcı herhangi bir string girdi ve biz bu stringi char IP[30] gibi bir diziye aktardık. Bu sunucu adını çözmek için gethostbyname() fonksiyonunu kullanacağız..

Gethostbyname fonksiyonu sizden bir string alır ve bu adı çözerek bir ip bilgisi çıkarır, bu ip ile ilgili çeşitli bilgileri kendi içinde bir yapıya aktarır ve bu yapının adresini bize geri döndürür. Geri döndürdüğü yapının türü “struct hostent” dir. Bu adresi alıp yapıyı kullanabilmemiz için tahmin edeceğiniz gibi bu türden bir yapı tanımlamamız lazım tabi pointer olarak, yani yapının kendisini değil adresini gönderir fonksiyon …

struct hostent *sunucu_adi;

tanımlamamızı yaptıktan sonra ;

sunucu_adi = gethostbyname(IP);

diyoruz ve *sunucu_adi yapımızda artık istediğimiz adresimiz var. Hostent yapısının içeriğini söylemeye gerek duymuyorum daha fazla kafalar karışmasın diye, biz sadece h_addr elemanıyla ilgilenecez çünkü, aslında bakarsanız  hostent yapısının h_addr diye bir elemanı yoktur J

#define h_addr *h_addr_list   ya da

#define h_addr h_addr_list[0]

olarak tanımlanmıştır.. Dediğim gibi biz sunucu_adi->h_addr  ile ilgileneceğiz.. Şimdi aldığımız bu adresi soketimizin bilgiler yapısına aktaralım…

bilgiler.sin_addr = *((struct in_addr *)sunucu_adi->h_addr);

derleyicinin uyarısını kesmek için sunucu_adi yapı adresimizi in_addr yapi adresine çeviriyoruz. Basit bir pointer dönüşümü fazla takmayın. En başta neden yıldız var diyenler için açıklayayım merak etmeyenler alt paragraftan devam etsin J   #define ettiğimiz h_addr_list aslında bir gösterici göstericisidir, yani tipi char  ** dır. Dolayısıyla h_addr de aslında doğrudan değer değil bir bellek adresidir. Bu yüzden * içerik operatörüyle gerçek sunucu adresi bilgisini yapımıza aktarıyoruz..

Son derece gereksiz bir işlem kaldı geriye,

memset(&(bilgiler.sin_zero), 0,0) ;

ben yine de ilgilenenler için kısaca bahsedeyim, isteyen bu bölümü de okumayabilir . Aslında soketin bilgilerini struct sockaddr_in yapısı değil struct sockaddr yapısı tutar. Ancak bu yapının kullanımı daha zor olduğu için onun yerine ona eşdeğer olan ama kullanımı daha kolay olan struct sockaddr_in yapısı tasarlanmış.

Ancak sockaddr yapısı 16 bayt iken sockaddr_in yapısı 8 bayt kalır mıJ düzen bozulmasın diye sockaddr_in yapısına bir eleman daha eklenmiş, o da : unsigned char sin_zero[8] elemanı, adı üstünde tamamen boşluk doldurmak için olan bu eleman sıfırla doldurulmalıdır, daha doğrusu doldurulsa iyi olur. Bu yüzden memset ile içini sıfırlıyoruz…

Sonunda sockaddr_in yapısı da bitti. Elimizde bir soketimiz bir de yapımız var ama bunlar henüz tanışmadı J tanıştırmak tabiki bize düşüyor…

Nasıl mı ? tabi ki bind fonksiyonuyla . Bind etmek bağlamak, birleştirmek gibi bir anlam taşır. Yani soketimiz ile soket bilgilerini taşıyan yapımızı bind ediyoruz…

bind(skt,(struct sockaddr *)&bilgiler , sizeof(struct sockaddr) );

Evet, aldığı ilk parametre tabi ki soketimiz, ikinci parametre ise bilgileri taşıyan yapımızın adresi, asla yapının kendisi bir fonksiyona parametre olarak geçilmez. Aynı şekilde dizilerde geçilmez. Hatta dizilerin adının dizinin başlangıç adresi olması hikayesi de bu sebeptendir. Dennis amcam C yi ilk yazdığında struct diye bir şey henüz yoktu, büyük veri yapıları dizilerdi ve fonksiyonlara büyük veri yapıları geçirileceği zaman sorun olacağı için (gereksiz blok kopyalama ve çok önemli bir sorun daha) veri yapısının adını parametre olarak yazıp tüm verileri  yönetmeyi düşünmüş. Ne akıllı bir adam J

Konuyu bir hayli dağıttıktan sonra son parametreye dönelim.Gördüğünüz gibi son parametre ikinci parametrenin boyutunu alıyor. Direkt 16 da yazılabilridi ama size tavsiyem her zaman sizeof kullanın, sadece bunun için değil en basitirnden fwrite de dosyaya veri yazarken bile fwrite(tmp,sizeof(char),n,dosya) gibi kullanın. Böylece o parametrenin hangi amaçla kullanıldığını çok rahat görürsünüz ve kodu anlamanız kolaylaşır.

Son olara neden (struct sockaddr*) diye tür dönüşümü yapıldığını düşünenler için demin yazdığımı tekrarlayım. Aslında soket bilgilerini tutan yapı sockaddr dir, sockaddr_in onun sonradan yazılmış bir versiyonudur, bu yüzde parametre olarak geçerken adresini sockaddr türüne çevirmekte yarar var.

Aslında bakarsanız tek yararı derleyicinin uyarısını kesmek. Ama olsun siz iyi bir programcı olun ve tür dönüşümünü yapmayı ihmal etmeyinJ

Bind fonksiyonunu anladık ancak şunu belirmeliyim. Bind fonksiyonu eğer bir portu dinleyeceksek yani sunucuysak kullanılır. Eğer istemciysek yani belli bir adrese bağlanmak ise amacımız bind işlemi ile soketimiz ve yapımızı birleştirmemize gerek yoktur. Bunun yerine soketimizi ve yapımızı tıpkı bind fonksiyonundaki gibi connect fonksiyonuna yollarız, gerisini connect halleder ;)

connect(skt, (struct sockaddr *)&bilgiler,sizeof(struct sockaddr) );

bind fonksiyonumuza ne kadar benziyor dimi, aslında tek fark adıymış gibi duruyor. Connect fonksiyonu bilgiler yapısında aldığı sunucuya aynı yapıdan aldığı port üzerinden bağlantı isteği gönderir. Eğer o sunucuda o portu dinleyen bir program varsa bizim bağlantı isteğimiz kabul edilir ve artık veri alışverişi aşamasına gelmiş bulunuruz..

Olaya biraz da sunucu tarafından bakalım. Sunucu denilen programın tek yaptığı belli bir portu sürekli dinlemek ve kendisine bağlantı isteği gelirse bu isteği kabul etmektir.

Yani dinlemek=listen, kabul etmek=accept…

Evet bir portun açık olması bir anlam ifade etmez, önemli olan o portu dinleyen bir programın bulunup bulunmamasıdır. Portu dinlemek için kullanılan fonskiyonumuz;

listen(skt,baglanti_sayisi);

Kolay görünüyor değilmi, öyle de.. skt bizim dinlemek için kullanacağımız soket, ancak burada dikat edin soket bilgilerini ayrı bir parametre olarak geçmiyoruz, çünkü zaten bind fonksiyonu ile soketimizi ve yapımızı birleştirdik. Bind fonksiyonu sadece sunuculara özgüdür, ve listen işleminden önce kullanılır bunu unutmayalım..

Baglantı sayısı parametresine gelecek olursak, elbette size bağlanacak maksimum istemci sayısını sınırlamak isteyebilirsiniz. Eğer bir chat sunucusu iseniz ve sağlam bir shell hesabınız yoksa 1000 kişi odaya girdiği anda muhtemelen sunucu çökecektir, tabi bizim 1000 kişilerle işimiz yok, uygun bir değer olarak 10,20 koyabiliriz.

Evet, portumuzu dinlemeyi anladık, peki gelen bağlantıları nereden anlayacaz, nasıl kabul edecez nasıl yönetecez vs. Tabi ki accept fonskiyonu ile. Accept fonksiyonu listen ile dinlenen sunucuya bir bağlantı isteği geldiğinde bu isteği kabul eder. Bağlantı istekleri bir kuyruk ta tutulur ve accept fonksiyonu çağırılıncaya kadar o kuyrukta bekler. Accept fonksiyonu her çağırıldığında bir bağlantı kabul edilir.

İntboyut=sizeof(bilgiler);

accept(skt,(structsockaddr)&bilgiler,&boyut));

Burada bilgiler yapımızın boyutunu direkt sizeof ile veremedik çünkü direkt boyutu değil boyutu tutan değişkenin bellek adresini istiyor.

Bu arada accept fonksiyonu istemciyi kabul ettikten sonra istemcinin ip, port gibi bilgilerini parametre olarak verdiğimiz bilgiler yapısına aktarır…

Accept fonksiyonunun çok önemli bir özelliği vardır. Bu fonksiyona dinleme yaptığınız soketi verirsiniz, o sokete gelen bağlantı isteğini kabul eder, daha sonra bu bağlantı isteğini yeni bir sokete aktarır.

Yani sıfırdan bir soket oluşturur ve gelen bağlantı isteğini bu sokete aktarır. Dinleme yaptığınız eski soketiniz de dinlemeye devam eder tabiki. Her yeni istemci bağlantısında sunucu da yeni bir soket açılır ve bu soket istemcinin soketine bağlanır. Artık veri alışverişi aşamasına gelinmiştir.

Kısaca özetlersek…

Sunucu İstemci

*Bind ile soketi hazırlarız…                        *bu arada bir şey yapmayız..

*Listen ile soketi dinleriz…                        *connect ile bağlantı isteği yollarız..

*Accept ile isteği kabul ederiz                   *istediğimiz gibi veri alışverişi yapabiliriz artık

Sonunda sunucumuzla istemcimizi birbirine bağladığımıza göre artık veri alışverişine geçebiliriz…

Çok basit iki fonksiyon ile her türlü veri alışverişini gerçekleştireceğiz. İnternetten dosya indirirken, web sitesi gezerken, telnet ile ftp ye vs bağlanırken aslında hep bu fonksiyonlar çalışır arka planda. Ve gerçekten basit fonksiyonlardır.

Send ve recv fonksiyonları..

Send fonksiyonu adi üstünde veri yollamak için, recv fonksiyonu ise veri almak için kullanılır. Bu fonksiyonlara geçmeden önce unuttuğum birşeyi ekleyeyim, bu kullandığımız fonksiyonların tamamı hata durumunda -1 değerini geri döndürür. Bu yüzden her aşamada hata kontrolü yapmayı ihmal etmeyelim. Başlarda zor gelir ancak zamanla programlamanın en önemli kurallarından birisi olduğunu anlarsınız..

send(skt,tmp,sizeof(tmp),0);

Evet bu basit fonksiyon tüm web iletişiminin kalbidir adeta J skt bağlantı kurduğumuz soket, tmp yollayacağımız verilerin bulunduğu adres, sizeof(tmp) kaç bayt veri yollayacağımız , son parametre tcp/ip de herzaman 0 olarak bırakılmalı.

Send fonksiyonu gönderdiği bayt miktarını döndürür. Neden böyle bir değer döndürdüğünü düşünebilirsiniz, sonucta göndereceği değeri zaten 3. parametrede belirtiyoruz..

Maalesef send fonksiyonu sizin ona gönder dediğiniz verinin tamamını göndermeyi garanti etmez. Bir bayt bile gönderse başarılı olarak döner ve 1 değerini döndürür.

Eğer bir hata olursa tahmin edeceğiniz gibi -1 değerini döndürür.

Hemen umutsuzluğa kapılmaya gerek yok J çok basit bir fonksiyon yazarak istediğimiz tüm verinin ne pahasına olursa olsun gitmesini sağlayabiliriz. Böylesi çok daha güvenlidir.

Recv fonksiyonuna gelecek olursak ..

recv(s2,tmp,sizeof(tmp),0);

send fonksiyonunun aynısıdır, parametrelerin anlamı da aynıdır, yani send ile tmp deki veri yollanır recv ile gelen veri tmp alanına yazılır. Recv fonksiyonunu çalıştırdıktan sonra fonksiyon veri gelene kadar bekler. Aslında recv fonksiyonu blokeli ya da blokesiz modda çalışabilir.

Yani istersek veri gelene kadar bekleme o anda ağ tamponunda veri varsa al yoksa boşver sonraki komuta geç diyebiliriz.

Ancak varsayılan olarak blokeli moddadır ve herhangi bir veri gelene kadar bekler. Geri dönüş değeri olarak aldığı bayt miktarını döndürür, o an ağ tamponunda 1 bayt bile olsa bu bir baytı okuyup başarılı bir şekilde dönüş yapar.

Eğer hata olursa -1 değerini döndürür. Ama bu fonksiyona has olan bir değer daha vardır. Eğer karşı taraf ile bağlantı kopmuşsa recv fonksiyonu 0 değerini döndürür.

Bu bloklama olayı sadece recv fonksiyonuna özgü değildir. Ağ programlama da karşı bilgisayardan her an bilgi alamayacağınız için ve bağlantı her an düzgün işlemeyebileceği için bloklama önemlidir. Mesela accept fonksiyonu da blokelidir. Eğer accept fonksiyonunu çağrırsanız bir bağlantı gelene kadar bekler. Bir bağlantı isteği gelince yeni bir sokete bağlantıyı atar ve sonraki satırdan devam eder.

UDP hakkında da bir şeyler anlatayım. Udp de sunucu ile istemcinin bağlantı kurmasına gerek yoktur. Verinin iletilip iletilmediği ile de ilgilenilmez. program verinin teppesine adresi portu yapıştırır yollar, diğer program da bu veriyi alır kullanır.

Ortada bir bağlantı olmadığına göre connect fonksiyonuna da gerek yoktur. Udp ile veri gönderme ve veri alma fonksiyonları tcp ye çok bezner ama ufak tefek farklılıkları vardır. Veri göndermek için sendto veri almak için ise recvfrom fonksiyonları kullanılır.

Hatırlarsanız tcp soketleri için her soket ancak bir bağlantı için kullanılır demiştik. Yani siz bir soketle bir sunucuya ya da istemciye bağlandıysanız o soket artık sadece o bağlantıya özgüdür. Dolayısıyla send fonksiyonu ile soketten  bir veri yolladığınızda bu verinin nereye gideceği bellidir.

Ancak udp soketleri bağımsızdır. Tek bir soket birsürü farklı bilgisayara veri yollayabilir. Ortada bağlantı yoktur.

Eğer bir soketi sadece yarattıysak ve hiçbir adres, port bilgisi ile bind etmediysek o halde veri yollarken hangi ip ve porta yollayacağını da parametre olarak bildirmeliyiz..

sendto(skt,tmp,sizeof(tmp),0,(struct sockaddr*)&bilgiler \

,sizeof(struct sockaddr) );

Sanırım tanıdık gelmiştir. İlk üç parametre send ile tamamen aynı, 4. parametre hangi ip ve porta gönderileceği bilgisini tutar, son parametre ise bilgiler yapısının boyutunu tutar.

Recvfrom fonksiyonu da çok benzerdir.

recvfrom(skt,tmp,sizeof(tmp),0,(struct sockaddr*)&bilgiler \

,sizeof(struct sockaddr) );

Bu fonksiyon da kendisine gelen veriyi tmp ye yazar. Udp de aslında bir bağlantı ve konrol sistemi olmadığı için veriler tcp den çok daha az paketlenir. Kendi yazacağınız bir uygulama katmanı ile udp paketlerini numaralandırarak sırayla alır ve aldığınız her veri için aldım cevabı gönderebilirsiniz. Aldım cevabı gelmezse aynı veriyi tekrar gönderirsiniz. Bu yöntem çok sık kullanılır. Kendi iletişim protokolünüzü yazmanız mümkün yani. Aslında udp daha çok ses ve görüntü iletiminde kullanılır. Çünkü canlı ses ve görüntü iletiminde tcp ile verinin paketlenmesi boyutunu çok büyüteceği için o ses ve görüntü anlık olmaz. Ses ve görüntü iletiminde paket kontrolü yapmak da genelde tercih edilmez, buna rağmen sorunsuz çalışır…

Bu arada unutmadan, eğer ben udp kullanarak tek bir soketle bağlantır kuracağım derseniz connect fonksiyonunu kullanabilirsiniz. Bu durumda udp bağlantı olduğu halde send ve recv fonksiyonlarını da kullanabilirsiniz.  Ben bu kullanımı tercih ediyorum ama ihtiyaca göre tabi…

*    *     *

İsterseniz biraz istemci sunucu mimarisinden bahsedelim. Daha önce de belirttiğim gibi sunucu belli bir portu dinler ve bağlanan istemcilere hizmet sunar. Ancak  ortada küçük bir problem var, sunucu accept fonksiyonu ile bağlantı beklerken ( accept fonksiyonu blokeli çalışır) başka bir iş yapamaz.

Yani 10 tane istemci bağlanmaya çalışıyor diyelim. İlk istemci bağlantı isteği gönderdiğinde accept ile kabul edilir, ve ardından tekrar accept fonksiyonu çalışır.

Peki accept fonksiyonu blokeli çalışıyorken aynı zamanda bağlantı kurduğumuz istemcilere nasıl hizmet vereceğiz?

Bunun bir yöntemi soketi blokeli moddan çıkarıp (fcntl ile) sürekli genel bir döngü içinde bağlantı isteği olup olmadığını kontrol etmektir.

Ancak bu yol işlemcinin daima %100 kullanılması demektir ve kullanışsız olmasının yanında verimsiz bir yoldur. Ayrıca recv fonksiyonu da ağ tamponuna bir veri gelene kadar bloklanır ve bekler, yani biz istemcilerden birinden bir veri alacaksak diğerlerine ondan veri gelene kadar hizmet edemeyiz.

Bu sorun elbette çözümsüz değildir, Windows ortamında gelen her bağlantı için CreateThread fonksiyonu ile yeni bir thread yaratılarak tüm istemcilere hizmet verilebilir.

Bu yöntem 10 istemciye kadar normal görülebilir ancak istemci sayısı arttıkça sunucu yerlerde sürünmeye başlar. İşte bu durum düşünülerek işletim sistemleri tarafından sağlanan bir select fonksiyonu tasarlanmıştır. Select fonksiyonu aslında bir dosya fonksiyonudur, linux da her türlü dosyayla birlikte kullanılabilir ancak windows da sadece soketler için kullanılabilir.

Select fonksiyonu birçok soketi aynı anda gözlemleyebilir ve herhangi bir sokette hareketlilik olduğunda bunu bize bildirir. Diğer fonksiyonlardan biraz daha karmaşık olduğu halde çok yararlı bir fonksiyondur..

Select fonksiyonu 3 tane dosya tanımlayıcı kümesi ni parametre olarak alır, bunu biraz açıklayalım… Dosya tanımlayıcılar int türden nesnelerdir. Her dosya tanımlayıcı bir dosya ile ilişkilidir. Örneğin SOCKET dosya tanımlayıcısı socket() fonksiyonu ile oluşturulur. Yukarıdaki örneklerde kullandığımız skt nesnesi aslında soketin kendisi değil onun dosya tanımlayıcısıdır. Yani aslında skt, int türden bir tamsayıdır. Bu tam sayı tek bir dosyaya ilişkin olduğu için istediğimiz fonksiyonda bu sayı ile o dosyaya yani sokete erişebiliriz.

Fopen ile açılan bir dosya FILE * türünde bir dosya handlesi döndürüyordu, bu handle dosya tanımlayıcısı değildir ancak dosya tanımlayıcılar üzerine kurulmuş daha üst seviye bir arayüzdür. Aslında sistemde açılan her dosyanın bir dosya tanımlayıcısı vardır. Giriş çıkış işlemleri en temel seviyede bu dosya tanımlayıcıları kullanarak dosyaya yazma ve okuma işlemleridir.

Dosya tanımlayıcılardan bahsettikten sonra dosya tanımlayıcı kümesinden kastımızın ne olduğunu söyleyeyim…Aslında dosya tanımlayıcı kümesi int türden bir dizi olarak düşünülebilir. Biz bir kümeye dosya tanımlayıcı ekleyebiliriz ya da silebiliriz. Yani içnide birden fazla dosya tanımlayıcı barındıran bir kümedir bu. Bu dosya tanımlayıcılar pekala soketler olabilir.

İşte select fonksiyonu bu şekilde 3  dosya tanımlayıcı küme alır. Bu kümeleri sürekli gözlemler, bir dosya tanımlayıcı uyarı gönderirse bunu hemen bize bildirir. Yani biz select fonksiyonuna 1000 elemanlı bir soket kümesini verdiğimizde bu fonksiyon hangisinin o anda işlem yapmak için hazır olduğunu bize bildirebilir.  Ne güzel değilmi J

Peki neden üç tane küme alır, tek küme alması yeterli değil mi?  İşte aldığı ilk dosya tanımlatıcı kümesi readfds (sanırım “read file descriptor set”) dir, ikincisi writefds ( “write file descriptor set”) , sonuncusu ise exceptfds (“execpt file descriptor set”) dir.

Bunları kısaca açıklayayım, select fonksiyonunun kendisine verilen dosya tanımlayıcı kümelerini kontrol ettiğini biliyoruz, işte readfds kümesindeki tanımlayıcılardan  birisi okunabilir (readability) durumda ise yani bir veri gelmiş ise , select fonksiyonu uyarı verir..

Aynı şekilde writefds kümesindeki tanımlayıcılardan birisi yazılabilir (writability) durumda ise select fonksiyonu yine uyarı verir.

Yani biz herhanngi bir dosyaya okuma ya da yazma yapıp yapamayacağımnızı merak ediyorsak bu dosyayı bu iki tanımlayıcı kümesinden birisine eklememiz yeterli, select fonksiyonu bizi hemen uyarır ve biz de hangi dosyanın ne uyarısı verdiğine bakarak okuma ya da yazma işlemimizi yapabiliriz.

Eğer listen fonksiyonu ile bir soketi dinliyorsak ve yeni bir bağlantı gelmişse bu durumda yine readfds uyarı verir. Yani readfds ye eklediğimiz soket uyarı veriyorsa bu salt yeni bir veri geldi demek değildir. Tabi bunun kontrolünü de programımızda yapacağız…

Eğer kafanız karıştıysa önemsemeyin, örneklerde daha iyi anlayacaksınız… Son olarak exceptfds ise hata kontrolü için kullanılır, dosya tanımlayıcılardan birinde harici (except) bir durum olursa bu dosya tanımlayıcı kümesi sayesinde  bunu öğreniriz.

Select fonksiyonunun dosya tanımlayıcı kümeleri dışında iki parametresi daha vardır.Bunlardan biri yine gereksiz bir parametredir, windows programlarında 0 olarak geçilir ve bsd yani unix soketlerine uyumluluk için eklenmiştir (ilk ağ sistemi ve programlaması bsd işletim sisteminde olduğu için diğer sistemlere bundan geçmiştir).

Linux da bu parametreye en büyük soket tanımlayıcısının değeri geçilir.

Select fonksiyonunun son parametresi ise zaman aşımı süresidir. struct timeval türünden bir yapıdır, bu yapının iki elemanı vardır, birisi tv_sec diğeri tc_usec. Tv_sec saniyeyi, tv_usec ise mikro saniyeyi tutar.

Biz sadece tv_sec e saniyeyi yazsak yeter, zaten bir mikro saniye 10 üzeri -6 saniye gibi çok küçük bir değerdir.

Bu parametreyi neden kullanacağımızı anlatayım, mesela bir chat sunucususunuz, gelen tüm bağlantıları okuma  soket tanımlayıcı kümesine koydunuz  ve okumak için gelecek bir veri bekliyorsunuz. Ancak ne bir şey yazan var ne de yeni gelen birileri, eğer böyle bir durumda sunucuyu kapatır çeker giderim diyorsanız bu parametreye 0 dışında bir bekleme süresi koyarsınız J

Eğer tv_sec elemanına 100 değerini verirseniz soketlerden hiç birinde 100 saniye içinde bir etkinlik olmazsa select fonksiyonu sona erer.

Eğer bu parametreyi 0 olarak geçerseniz asla zaman aşımı olmaz, dikkat edelim, yapı elemanlarının içine 0 değerini koymayacağız direkt bu parametreye 0 değerini vereceğiz…

Evet sanırım artık şu select fonksiyonunu yazabiliriz…

Select(0,*readfds,*writefds,*exceptfds,zamanasimi);

Tabi fonksiyon burda dosya tanımlayıcı kümesinin adresini alır kendisini değil, standart değişkenler dışında hiçbir değişken doğrudan parametre olmaz zaten ;)

Fonksiyonumuzun tam olarak ne işe yaradığını anladığımıza göre artık dosya tanımlayıcı kümelerini nasıl tanımlayacağımızı öğrenebiliriz

Dosya tanımlayıcı kümeleri  fd_set türündedir. Fd_set türü winsock.h içinde şu şekilde typedef edilmiştir.


typedef struct fd_set {
  u_int  fd_count;
  SOCKET fd_array[FD_SETSIZE];
}fd_set;

Gördüğünüz gibi içerisinde bir sayac ve bir de soket dizisi var. Biz dosya tanımlayıcı kümesine veri ekleyip çıkardığımızda aslında bu soket dizisine soket ekleyip çıkarmış oluyoruz ve sayac da ona göre güncelleniyor…

Hemen bir dosya tanımlayıcı kümesi tanımlayalım…

fd_set okunanlar;

Evet, artık okunanlar adında bir dosya tanımlayıcı kümemiz var, şimdi bunun içine soketlerimizi yerleştirmesi kaldı. İstediğimiz soketleri yerleştirip select fonksiyonunun 2. parametresine yani readfds ye yerleştirirsek, bu soketlerden birine veri geldiğinde select bizi uyarır ve biz de recv ile gelen veriyi alırız..  Sanırım taşlar yerine oturmaya başladı J

Şimdi tanımlayıcı kümemize nasıl soket ekleyip sileceğimizi öğrenelim. Bunlar için bazı makroları kullanacağız..

FD_SET(int fd, fd_set * okunanlar);  Bu makro ile kümemize bir soket ekleriz.

FD_CLR(int fd, fd_set * okunanlar);  Bu makro ile kümemizden bir soket siler   iz

FD_ZERO(fd_set *okunanlar);          Bu makro ile tüm kümeyi sıfırlayabiliriz.

FD_ISSET(int fd, fd_set * okunanlar); Bu makro ile soketi  kümemizde mi diye

Kontrol ederiz…

Burada ilk parametre olan int fd (file descriptor), dosya tanımlayıcısıdır, biz soketimizi yazacağız doğal olarak. Dikkat edilmesi gereken, diğer parametre dosya tanımlayıcı kümemizin kendisi değil adresi. Yani biz kümemizi parametre olarak geçerken & işlecini kullanmalıyız…

FD_SET(skt, &okunanlar);

FD_CLR(skt, &okunanlar);

FD_ZERO(&okunanlar);

FD_ISSET(skt, &okunanlar);

Şeklinde kullanacağız… Artık soket programlama ile ilgili birçok şeyi biliyoruz, uzman sayılmasak da rahatlıkla piyasadaki birçok programın biraz daha amatörünü yazabiliriz.

Sanırım artık bir örnek vermenin zamanı geldi.. İlk programımız bir chat sunucusu olsun..

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

// Yazan: KuLTigiN

// http://www.tahribat.com

#include

#include

#define PORT 5000

int main(){

WSADATA  wsdata;            // gereksiz MS işleri..

SOCKET   ss,is;             // sunucu soket ve istemci soket

fd_set   anatanim,gecici;    // ana ve geçici dosya tanimlayici kümeleri

char tmp[256];          // istemci verisi için tampon

int gelenbayt,boyut;   // recv de gelen veri, accept için boyut...

int i, j;              // döngü sayacları.

struct sockaddr_in sunucu;  // sunucu adres bilgileri

struct sockaddr_in istemci; //istemci adres bilgileri

sunucu.sin_family = AF_INET;

sunucu.sin_port = htons(PORT);

sunucu.sin_addr.s_addr = INADDR_ANY;

WSAStartup(MAKEWORD(2,0),&wsdata);

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

FD_ZERO(&gecici);

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

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");

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

FD_SET(ss, &anatanim);

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];

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

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));

}

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

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);

}

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

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);

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

}

}

WSACleanup();

return 0;

}

//Bitti 🙂

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

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.

shutdown (soket , nasil);

Burada soket parametresi kapatmak istediğimiz sokettir. Nasıl parametresi ise üç değer alabilir, bunları açıklamadan önce send ve recv fonksiyonları ile ilgili bir şey daha söyleyeyim.

Bu fonksiyonlar birbirlerine veriyi doğrudan iletmezler, send fonksiyonu veriyi ağ tamponuna yazar, ağ tamponunun doluluğuna göre istenilen veriyi tek seferde yazabilir ya da bir kısmını yazar.

Aynı şekilde recv fonksiyonu da veriyi ağ tamponundan okur ve 1 bayt bile okusa başarılı olur.

Bir soket shut down komutu ile kapatıldığında eğer nasil parametresine 0 yani SD_SEND değerini verirsek o soket ile veri gönderile bilir ancak veri alınamaz.

Eğer 1 yani SD_RECEIVE değerini verirsek o soketten veri alınabilir ancak veri gönderilemez..

Son olarak 3 yani SD_BOTH değerini verirsek soket çift yönlü olarak kapatılır ve artık veri alınamaz, gönderilemez, ancak shutdown ile kapatma yaptığımızda closesocket in aksine ağ tamponunda gönderilecek veya alınacak veri varsa bu veri gönderilir-alınır ve soket öyle kapatılır.

Son olarak pek sık kullanılmayan iki fonksiyonan bahsedeyim.. getpeername ve gethostname fonksiyonları.

Gethostname fonksiyonu sizin kendi sunucu adınızı öğrenmek için kullanabileceğiniz bir fonksiyon ,

int gethostname(char *sunucuadi, size_t boyut);

size_t standart bir typedef ismidir ve aslında long türüdür. Sunucuadi içi boş bir dizinin başlanıç adresi yani adıdır. Boyut ise sunucuadi dizisinin uzunluğudur.

İşlem bitince sunucu adınız artık sunucuadi dizisinde bulunmaktadır…

Getpeername fonksiyonu ise soketinizin diğer ucundaki istemci yada sunucunun adresini elde etmek için kullanılır.

int getpeername(soket, struct sockaddr *bilgiler, int *boyut);

parametreleri accept fonksiyonu gibidir, soketi ve bilgiler yapısını veririz ve fonksiyon bilgiler yapısını soketin diğer tarafındaki makinanın ip ve port değerleriyle doldurur.

Son parametre de tıpkı accept gibi bilgiler yapısının boyutudur ancak boyutunu tutan değişkenin adresini istediği için int boyut= sizeof(bilgiler) şeklinde değişkeni tanımlayıp &boyut şeklinde parametre olarak geçmelisiniz..

Evet sonunda bitti J artık sadece program yazıp açıklamalarını vereceğim. Bu yazdığım sunucuyu zaten dökümanın eklerinde kaynak kod ve derlenmiş hali olarak bulabilirsiniz, sakın kaybetmeyin, bu iki satırlık sunucu programıyla öyle bir chat programı yazacağız ki gözlerinize inanamayacaksınız  J

Evet arkadaslar bu vb de yazdıgım dandik chat istemcisi, siz sunuyucu öğrenseniz yeter istemci daha da basittir. Sunucu bağlantıları kontrol eder istemci sadece sunucu ile veri alış verişi yapar.

Açıkçası birkaç program örneği daha yapacaktık bu dökümanda ama tahribatta diğer programların kaynak kodunu paylaşacağım için gerel  kalmadı sanırım. Hem doküman da bir an önce çıkmış olur.

Ama burada önemli bir ayrıntıyı sonraki örneklerde anlatırız diye aktarmamıştım. Şimdi onu da anlatacağım. Hatırlarsanız send ve recv fonksiyonları istediğimiz verinin tamamını yollayamayabiliyordu.

İşte bu durumda konrolü biz alabilirdik. Bundan yukarda kabaca bahsetmiştim. İşte şimdi send ve recv ile istediğimiz kadar verinin yollanması yada alınması için iki klasikleşmiş fonksiyon yazacağız.

///////////////////////////////////////////////////////////////////////////

int recvx(SOCKET s, void *tmp, int toplam) {

int sonuc;

int i = 0;

int gelecek = toplam;

while (gelecek > 0) {

sonuc = recv(s, (char *) tmp + i , gelecek, 0);

if (sonuc == 0)

return i;

if (sonuc == -1)

return -1;

i += sonuc;

gelecek -= sonuc;

}

return i;

}

///////////////////////////////////////////////////////////////////////////

int sendx(SOCKET s, const void *tmp, int toplam) {

int sonuc;

int i = 0;

int gidecek = toplam;

while (gidecek > 0) {

sonuc = send(s, (const char *) tmp + i , gidecek, 0);

if (sonuc == 0)

return i;

if (sonuc == -1)

return -1;

i += sonuc;

gidecek -= sonuc;

}

return i;

}

Arkadaşlar zaten biraz incelerseniz hemen anlayacağınız kodlar. Recvx den başlayalım, recv alternatifi olan bu fonksiyonda  recv’in son parametresini yani 0 geçtiğimiz parametreyi kullanmıyoruz. Onun dışında parametreleri recv ile tamamen aynı..

Bu fonksiyonda öncelikle alınacak tüm veri tek seferde recv ile alınmaya çalışılıyor, eğer tamamı alındı ise alınan bayt ile geri dönülüyor, aksi halde alınan bayt alınması gereken bayttan düşülüyor ve fonksiyon birdaha çağırılıyor.

Tabi ki yeniden çağırıldığında alınan bayt eskisinin üzerine yazılmamalı, bu yüzden i += sonuc; ile aldığımız bayt kadar ilerletiyoruz tamponu.

Send fonksiyonu da bunun aynı sadece oradaki const kelimesi farklı, bu kelime verilen parametrenin yani tamponun sadece okunacağını ve yazmaya gerek olmadığını belirtmek için kullanılır. Zorunluluğu olmamakla birlikte kullanılmasında fayda vardır.

Hem programcı o parametrnin sadece okunmak için alındığını anlar, hemde fonksiyonda yanlışlıkla veriyi değiştirme tehlikesi bertaraf edilir..

Evet artık siz de bir soket programcısısınız. Hedefi gözünüzde büyütmedikten sonra bu bilgilerle rahatlıkla bir trojan, ddos attacker, chat uygulaması, dosya transferi vs yapabilirsiniz.

Hepinize iyi çalışmalar…

Bu doküman kaynak gösterilmek sureti ile istenildiği kadar çoğaltılabilir ve yayınlana bilir, kaynak göstermeyenin kendi ezikliği 🙂

Yazar: Tugberk

  1. Meka_4756
    30/05/2017, 18:42

    Görselleri yenilemelisiniz

    Beğen

  2. 21/10/2017, 22:08

    bu yazıya bayıldım gerçekten verilmesi gereken önemi vermişsiniz Allah razı olsun ..

    Beğen

  3. 19/04/2019, 18:54

    Güzel olmuş. Fakat Dev C++ çok eski (Outdated) bir derleyici ve neredeyse hiç bir yardımı yok.Visual studio 2019 veya 2017 tercih ediyorum.

    Beğen

  1. No trackbacks yet.

Yorum bırakın