Kullanıcı Adı :   Şifre:  
  Online: 240 kişi (15 kayıtlı kullanıcı, 225 konuk),   [Üye ol]   [Şifremi Unuttum
Internet Soketlerini Kullanarak
Telif hakkı: Brian "Beej" Hall
Gönderen: FresTeR   Tarih: 2007-01-31 00:10 / Sık Kullanılar'a Ekle
Yorum: (0)   Oy:
     

Özet
Bu belge bir öğretici olarak tasarlanmıştır ve tam teşekküllü bir başvuru kılavuzu değildir. Soket programlama konusuna ciddi ciddi merak salan bireyler tarafından adım adım okunursa işe yarayacaktır.

Hey! Soket programlama ile başınız belada mı? Bütün bu ayrıntılar man sayfalarından çekip çıkarmak için çok mu zor? En temel Internet programlama tekniklerini öğrenmek istiyorsunuz ama tonlarca struct ve bunları bind() işlevini çağırmadan connect(), vs., vs.ye parametre olarak nasıl geçeceğiniz konusunda binlerce ayrıntıyı öğrenmeye vaktiniz yok mu?
Hmm, bakın burada ne var? Bütün bu sinir bozucu ayrıntılarla ben zamanında boğuştum ve herkesle deneyimlerimi paylaşmak için can atıyorum! Doğru yere geldiniz. Bu belge ortalama bir C programcısına tüm bu ağ meseleleri ile ilgili temel kavramları ve pratik uygulamaları verecek düzeydedir.


Kimin İçin?
Bu belge bir öğretici olarak tasarlanmıştır ve tam teşekküllü bir başvuru kılavuzu değildir. Soket programlama konusuna ciddi ciddi merak salan bireyler tarafından adım adım okunursa işe yarayacaktır. Bu belge kesinlikle eksiksiz bir soket programlama kılavuzu değildir.
Eğer şu man sayfalarının sizin için biraz daha anlamlı hale gelmesini sağlarsa bu belge amacına ulaşmış demektir...:-)



Resmi Anasayfa
Bu belgenin resmi adresi California Devlet Üniversitesi, Chico, Linkleri görebilmek için Üye olmalısınız.


Solaris/SunOS Programcılarının Dikkat Etmesi Gerekenler
Solaris ya da SunOS için derlerken gerekli işlev kitaplıklarını programa bağlayabilmek için bazı ek parametreler vermeniz gerekebilir. Bunun için -lnsl -lsocket -lresolv parametrelerini derleme komutunuzun sonuna ekleyebilirsiniz, örnek:
$ cc -o server server.c -lnsl -lsocket -lresolv

eğer hala hata alıyorsanız şunu da ekleyin: -lxnet. Ne işe yaradığını bilmiyorum ama görünen o ki bazı durumlarda gerekebiliyor.
Problem yaşayabileceğiniz bir başka yer de setsockopt() işlevinin çağrıldığı yerdir. Bu işlevin Solaris/SunOS'taki prototipi benim Linux makinamdakinden farklıdır bu yüzden de:
int yes=1;
yerine bunu girin:
char yes='1';
Elimde bir Sun makinası yok, yukarıdakileri denemedim bu bilgiler bana deneme yapıp e-posta gönderen insanların söylediklerinden ibarettir.




Windows Programcılarının Dikkat Etmesi Gerekenler
Windows'dan pek hoşlandığım söylenemez bu yüzden de bu belgeyi okuyan tüm Windows programcılarını GNU/Linux, BSD ya da UNIX denemeye davet ediyorum. Bu laflardan sonra, evet, örnek kodları Windows üzerinde kullanma imkanınız var.
Öncelikle burada bahsettiğim birçok sistem başlık dosyasını unutun. Tek yapmanız gereken aşağıdakileri programınıza katmak:
#include <winsock.h>

Bir dakika! Aynı zamanda WSAStartup() işlevini de soket kitaplıkları ile herhangi bir iş yapmadan önce çağırmanız gerekiyor. Bunu yapmak için gerekli kod şöyle bir şey:
#include <winsock.h>

{
WSADATA wsaData; // if this doesn't work
//WSAData wsaData; // then try this instead

if (WSAStartup(MAKEWORD(1, 1), &wsaData) != 0) {
fprintf(stderr, "WSAStartup failed.\n");
exit(1);
}

Tabii derleyicinize Winsock'u da bağlamasını söylemelisiniz. Bunun için gerekli dosyanın ismi genellikle şudur: wsock32.lib veya winsock32.lib veya benzer bir şey. VC++ ortamında iseniz, bunun için Project menüsünden, Settings... kısmına gidin ve Link sekmesine gelip Object/library modules kutusunu bulun. wsock32.lib dosyasını bu listeye ekleyin.
En azından ben böyle duydum.
Ve son olarak da WSACleanup() işlevini çağırmanız gerekir soket kitaplığı ile işiniz bittiğinde. Ayrıntılı bilgi için derleyicinizin yardım belgelerine bakın.
Bunu yaptığınızda bu belgedeki örneklerin hemen hepsi genel olarak çalışabilir durumda olmalı belki birkaç küçük değişiklik yapmanız gerekebilir ama hepsi bu. Dikkat etmeniz gerekenler: Soketi kapatmak için close() işlevini kullanamazsınız -- bunun için closesocket() işlevini kullanmalısınız. Ayrıca select() işlevi sadece soket tanımlayıcılar içindir, dosya tanımlayıcılar (standart girdi için 0 kullanılması gibi) için değildir.
Aynı zamanda, CSocket isimli bir soket sınıfı da mevcuttur. Ayrıntılı bilgi için derleyicinizin belgelerini karıştırın.
Winsock hakkında ayrıntılı bilgi için şu adrese bakabilirsiniz: Winsock FAQ
Son olarak da bildiğim kadarı ile Windows ortamında fork() isimli işlev yok ve maalesef örneklerde bu işlevi kullanmak durumdayım. Belki de bir POSIX kitaplığına programınızı bağlamanız gerekebilir veya CreateProcess() işlevini kullanabilirsiniz. fork() işlevi herhangi bir argüman almaz ama CreateProcess() işlevi 48 milyar argüman alır. Eğer bu gözünüzü biraz korkuttu ise CreateThread() işlevi biraz daha kolay bir alternatif olabilir ancak "multithreading" tartışması bu belgenin sınırlarının ötesindedir. Windows konusunu böylece burada kapatıyoruz!


E-posta Politikası
Genellikle e-posta ile gönderilen sorulara cevap vermeye çalışırım, bu yüzden yazmaktan çekinmeyin, ancak bu size cevap vereceğim anlamına gelmez. Epey meşgulüm ve bazen sorunuzun cevabını tam olarak bilmiyor olabilirim. Durum bu olduğunda mesajınızı silerim lütfen bunu şahsınıza yönelik bir harekete olarak algılamayın. İnanın bana sorduğunuz her soruyu en ince ayrıntısına kadar cevaplayacak kadar vaktim olmayabilir.
Basit bir kural: Sorunuz ne kadar karmaşık ise cevap verme ihtimalim o kadar düşüktür. Eğer soru kapsamını daraltır ve derdinizle ilgili ayrıntılı bilgileri de yollarsanız (platform, derleyici, hata mesajları, vs.), cevap alma ihtimaliniz artar. Daha ayrıntılı bilgi için şu adresi tavsiye ederim: How To Ask Questions The Smart Way.
Eğer cevap alamıyorsanız biraz daha kurcalayın programınızı ve cevabı bulmaya çalışın. Hala çözemedi iseniz o zaman bana gene yazın ve belki o zaman elimizdeki ayrıntılı bilgilerle probleme bir çözüm bulmamız daha kolay olabilir.
Şimdi sizi bana nasıl yazmanız ve yazmamanız konusunda yeterince eğittiğime göre bu kılavuzla ilgili süreç içinde bana ulaştırılan övgülerin beni ne kadar motive ettiğini de itiraf etmek isterim. :-) Teşekkürler!


Yansılama
Sitemin bir yansısını barındırmak istiyorsanız memnuniyet duyarım. Eğer oluşturacağınız yansıya ana sayfamdan link vermek isterseniz <beej (at) piratehaven.org> adresine bir mesaj yollamanız yeterli olacaktır


SokeT Nedir ?
Sürekli "socket"lerden bahsedildiğini duymuşsunuzdur ve belki de bunların tam olarak ne anlama geldiğini merak ediyor olabilirsiniz. Soket kısaca şudur: Diğer programlarla standart Unix dosya tanımlayıcılarını kullanarak haberleşmenizi sağlayan bir yapı.
Ne?
Pekala -- bazı Unix hacker'larının, "Vay canına! Unix'teki hemen hemen herşey bir dosya!" dediğini duymuş olabilirsiniz. Böyle konuşan birinin kast ettiği aslında Unix programlarının, herhangi bir G/Ç işlemi yaptıklarında bunları bir dosya tanımlayıcıyı okuyarak ya da ona yazarak yaptıklarıdır. Bir dosya tanımlayıcı basitçe söylemek gerekirse açık bir dosya ile ilişkilendirilmiş bir tamsayıdır. Ancak (işin püf noktası da burası), söz konusu bu açık dosya diskteki normal bir dosya olabileceği gibi aynı zamanda bir ağ bağlantısı, bir FIFO, bir uçbirim ya da başka herhangi bir veri kaynağı olabilir. Gerçekten de Unix ortamında her şey bir dosyadır! Öyleyse Internet üzerinden başka bir programla iletişim kurmak isterseniz bunu bir dosya tanımlayıcı üzerinden yapacaksınız, inanın buna.
"Peki bay çok bilmiş, ağ iletişimi için kullanacağım bu dosya tanımlayıcı nerede?" gibi bir soru aklınıza gelmemiş olabilir ancak ben gene de cevabını vereyim ki içiniz rahat etsin: Bu dosya tanımlayıcıya ulaşmak için socket() sistem işlevini çağırmanız gerekir. Bu işlev size soket tanımlayıcıyı döndürür ve siz de bunu ve tabii send() ile recv() (man send, man recv) isimli soket işlevlerini kullanarak istediğiniz şekilde iletişimizini kurarsınız.
"Hey, bir dakika!" diyebilirsiniz şimdi. "Eğer bir dosya tanımlayıcı söz konusu ise o halde tanrı aşkına neden her zaman kullandığım normal read() ve write() işlevlerini kullanarak soketler üzerinden iletişim kuramayayım ki?" Kısa cevap: "Evet tabii ki!" Uzun cevap ise "Evet, mümkün ama send() ve recv() işlevleri veri iletişiminde çok daha fazla kontrol sağlar ve işinizi kolaylaştırır."
Sırada ne var? Buna ne dersiniz: çeşit çeşit soket vardır. Mesela DARPA Internet adresleri (Internet Soketleri), yerel bir düğümdeki (node) yol isimleri (Unix Soketleri), CCITT X.25 adresleri (X.25 Soketleri ki inanın bunları bilmeseniz de olur) ve kullandığınız Unix sürümüne bağlı daha pek çok soket tipi. Bu belge sadece birinci tür soketleri ele almaktadır, yani: Internet Soketleri.


Internet Soketlerinin İki türü
Bu da ne? İki tür Internet soketi mi var? Evet. Şey, aslında hayır. Yalan söyledim. Daha çok var ama sizi korkutmak istemedim. Size sadece iki tür soketten bahsedeceğim. Sadece "Ham Soketler"in (Raw Sockets) çok güçlü olduğunu belirttiğim bu cümle dışında yani, bir ara bunlara da göz atarsanız iyi olur.
Pekala, gelelim şu iki tür sokete. Bir tanesi "Veri Akış Soketleri"; diğeri ise "Veri Paketi Soketleri" ve artık biz bunlara sırası ile "SOCK_STREAM" ve "SOCK_DGRAM" diyeceğiz. Veri paketi soketleri bazen "bağlantısız soketler" olarak da isimlendirilir. (Her ne kadar eğer isterseniz bunlara connect() işlevi ile bağlanabilecek olsanız da. Detaylı bilgi için aşağıdaki connect() maddesine bakın.)
Veri akış soketleri güvenilir iki yönlü iletişim kanallarıdır. Eğer bu tür bir sokete sıra ile "1, 2" bilgilerini gönderirseniz, bunlar kanalın diğer ucundan "1, 2" şeklinde çıkar. Aynı zamanda bu iletişim olası hatalara karşı da korumalıdır. Karşılaşacağınız her türlü hatanın kaynağı sizin karman çorman aklınız olacaktır ve burada bunları tartışmaya niyetim yok.
Stream soketleri gerçek hayatta ne işimize yarar? Hmm, sanırım telnet diye bir program duymuşsunuzdur, değil mi? İşte bu program veri akış soketlerini kullanır. Yazdığınız tüm karakterler sizin yazdığınız sırada iletilmelidir öyle değil mi? Aynı zamanda web tarayıcılar da HTTP protokolü ile iletişim kurarken veri akış soketleri aracılığı ile sayfa bilgilerini çekerler. Gerçekten de eğer bir web sitesine telnet ile 80 numaralı port üzerinden bağlanır ve "GET /" komutunu yollarsanız web sitesi size HTML içeriğini yollayacaktır.
Veri akış soketleri bu kadar sorunsuz bir veri iletişimini nasıl gerçekleştirir? Bunun için "Aktarım Denetim Protokolü" (Transmission Control Protocol) isimli kurallar dizisinden yararlanırlar ki siz bunu "TCP" olarak da duymuş olabilirsiniz (bkz. RFC-793 bu belge TCP ile ilgili çok ayrıntılı bilgi içerir.) TCP yollanan verinin sıralı ve düzgün gitmesini sağlar. "TCP"yi daha önce "TCP/IP" kısaltmasının bir parçası olarak duymuş olabilirsiniz ki "IP" de "Internet Protocol" sözünün kısaltmasıdır (bkz. RFC-791.) IP kurallar dizisi temelde Internet yönlendirme ile ilgilidir, veri bütünlüğünün korunması ile ilgili pek kural içermez.
Harika. Peki veri paketi soketleri? Neden bunlara bağlantısız deniyor? Mesele nedir kısaca? Neden bunların "güvenilmez" olduğu söyleniyor? Bakın, bilmeniz gereken bazı gerçekler var: eğer bir veri paketi yollarsanız bu hedefine ulaşabilir de ulaşmayabilir de. Gönderdiğiniz sırada ulaşması garanti edilemez. Ancak eğer hedefe ulaşırsa paketin içerdiği bilgi hatasız olacaktır.
Veri paketi soketleri de yönlendirme için IP protokolünü kullanırlar ancak TCP'den faydalanmazlar onun yerine "Kullanıcı Veri Paketi Protokokü" (User Datagram Protocol) veya "UDP" isimli protokolü kullanırlar (bkz. RFC-768.)
Neden bağlantısızdırlar? Aslında böyledirler çünkü veri akış soketlerinde olduğu gibi bağlantıyı sürekli açık tutmanız gerekmemektedir. Sadece bir paketi oluşturur, tepesine gideceği adresi söyleyen bir IP başlığı yapıştırır ve onu yollarsınız. Herhangi bir bağlantı açmaya gerek yoktur. Bu tip soketler genellikle paket paket iletilen veri için kullanılır. Bu tip soketleri kullanan örnek uygulamalardan bazıları: tftp, bootp, vs.
"Yeter artık!" diye bağırdığınızı duyar gibiyim. "Eğer veri paketlerinin yolda kaybolma ihtimali varsa nasıl olur da yukarıda saydığın programlar çalışır?!" Bak dünyalı dostum bu saydığım programların hepsi UDP protokolü üzerine kendi protokollerini yerleştirirler. Mesela, tftp protokolüne göre gönderilen her paket için karşı tarafın "aldım!" (bir "ACK" paketi) paketini geri yollaması gerekir. Eğer orjinal paketin göndericisi mesela 5 saniye içinde cevap alamazsa o zaman paketi yeniden yollar, taa ki ACK cevabını alana kadar. İşte bu "aldım" prosedürü SOCK_DGRAM uygulamalarında çok önemlidir.

Düşük Seviye Duyarsızlığı ve Ağ Teorisi
Protokol katmanlarından bahsettiğime göre artık ağların nasıl çalıştığına dair gerçekleri öğrenmenin ve SOCK_DGRAM paketlerinin nasıl oluşturulduğuna dair örnekler vermenin zamanı geldi. Pratik olarak bu bölümü atlayabilirsiniz, ancak burayı okursanız iyi bir temel bilgiye sahip olursunuz.
Şekil 2.1. Veri Paketlemesi (data encapsulation)

Protokollerin Veriyi Paketlemesi
Hey çocuklar, Veri Paketleme konusunu öğrenme zamanı! Bu çok çok önemli. O kadar önemli o kadar önemli ki burada yani Chico Eyaletinde ağ teknolojilerine dair bir kurs alırsanız bu konu ile mutlaka karşılaşırsınız ;-). Temelde konu şu: bir paket doğar, sonra bu paket önce muhatap olduğu ilk protokol tarafından (mesela TFTP protokolü) ile bir başlık (ve ender olarak da olsa bir dipnot ile) kullanılarak paketlenir (kapsüle konur), ardından tüm bu yığın (TFTP başlığı da dahil) bir sonraki protokolün (örn. UDP) kurallarına göre paketlenir, sonra IP ve en sonundaki en alt katman olan donanım katmanındaki fiziksel protokol ile (örn. Ethernet) paketlenir.
Başka bir bilgisayar bu paketlenmiş paketi aldığında önce donanım Ethernet başlığını çıkarır, ardından işletim sistemi çekirdeği IP ve UDP başlıklarını alır ve ardından da TFTP programı TFTP başlığını alır ve içindeki veriye erişir.
Artık şu kötü üne sahip Katmanlı Ağ Modeli (Layered Network Model) kavramından bahsedebilirim. Bu ağ modeli diğer modellere pek çok üstünlüğü bulunan bir ağ işlevselliğinden bahseder. Mesela yazdığınız soket programının tek bir harfini bile değiştirmenize gerek kalmadan bu programı farklı fiziksel donanımlar üzerinde çalıştırabilirsiniz (seri, thin Ethernet, AUI, her ne ise) çünkü düşük seviyedeki programlar sizin yerinize ayrıntıları hallederler. Ağ donanımının fiziksel detayları ve topolojisi soket programcısını ilgilendirmez.
Daha fazla laf kalabalığı yapmadan bu müthiş modelin katmanlarını liste olarak vereyim, ağ ile ilgili sınava girerseniz işe yarayabilir:
Uygulama
Sunum
Oturum
Taşıma

Veri Bağlantısı
Fiziksel
Fiziksel katman donanımla ilgilidir (seri, Ethernet, vs.). Uygulama katmanı fiziksel katmandan alabildiğine uzaktır -- kullanıcılar ağ ile bu katmanda temas kurar.
Açıkçası bu model o kadar geneldir ki eğer isterseniz bu modeli arabanıza bile uygulayabilirsiniz. Unix ile daha uyumlu bir katmanlı model şöyle yazılabilir:
Uygulama Katmanı (telnet, ftp, etc.)
Konaktan Konağa Taşıma Katmanı (TCP, UDP)
Internet Katmanı (IP ve yönlendirme)
Ağa Erişim Katmanı (Ethernet, ATM ya da her ne ise)
Bu aşamada söz konusu katmanların veri paketlenmesinin hangi aşamalarına karşılık geldiğini görebiliyor olmalısınız.
Tek bir paketi oluşturmak için ne kadar çok iş yapılması gerektiğini gördünüz mü! Aman allahım! Üstelik paket başlık bilgilerini de cat komutunu kullanarak elle girmeniz gerekiyor! Şaka şaka! Tek yapmanız gereken eğer veri akış soketi kullanıyorsanız send() ile veriyi göndermek. Eğer veri paketi soketi kullanıyorsanız bu sefer de yapılması gereken paketi uygun şekilde paketleyip sendto() işlevini kullanarak bunu yollamak. Çekirdek sizin için Taşıma katmanını ve Internet katmanını kurar, donanımınız da Ağa Erişim Katmanını halleder. Ah, modern teknoloji.
Ağ teorisi ile ilgili kısa dersimiz burada sona eriyor. Ah evet tabii ki yönlendirmeyle ilgili söylemem gereken şeyler vardı: unutun! Bundan bahsetmeyeceğim. Yönlendirici (router) IP başlığını çeker yönlendirme tablosuna bakar, seçim yapar, vs. vs. vs. Eğer gerçekten meraklı iseniz IP RFC belgesine bakın. Bunu öğrenmezsiniz ölmezsiniz.


struct'lar ve Veri İle Uğraşmak
Hele şükür bu aşamaya gelebildik. Artık biraz programlamadan bahsedebiliriz. Bu bölümde soket arayüzleri tarafından kullanılan pek çok veri türünü ele alacağım çünkü bunlar gerçekten önemli.
Kolay olanlarla başlayalım: bir soket tanımlayıcı. Bir soket tanımlayıcı aşağıdaki türdendir:
int
Evet, gayet klasik, alışık olduğumuz basit bir int.
İşte bu aşamadan itibaren işler biraz garipleşmeye başlıyor bu yüzden de dikkatlice okuyun ve bana güvenin. İlk bilmeniz gereken: iki tür byte sıralaması vardır: en önemli baytın (ki buna bazen öktet de denir) önce geldiği sıralama veya en önemli baytın sonra geldiği sıralama. Bu sıralamalardan birincisine "Ağ Bayt Sıralaması" (Network Byte Order)[137] denir. Bazı makinalar içsel olarak veriyi kendi belleklerinde bu şekilde depolar, bazıları ise bu sırayı dikkate almaz. Bir şeyin Ağ Bayt Sıralaması'na göre sıralanması gerektiğini söylediğimde htons() gibi bir işlev çağırmanız gerekecek ("Konak Bayt Sıralaması"ndan [Host Byte Order] "Ağ Bayt Sıralaması"na dönüştürebilmek için). Eğer "Ağ Bayt Sıralaması"ndan bahsetmiyorsam o zaman ilgili veriyi olduğu gibi yani "Konak Bayt Sıralaması" düzeninde bırakmanız gerekir.
My First Struct™ -- struct sockaddr. Bu veri yapısı pek çok türde soket için soket adres bilgisini barındırır:
struct sockaddr {
unsigned short sa_family; // adres ailesi, AF_xxx
char sa_data[14]; // protokol adresinin 14 byte'ı
};

sa_family pek çok değerden birini alabilir ama bizim örneğimizde AF_INET olacak. sa_data ise soketle ilgili hedef adres ve port numarası bilgilerini barındırır. Bunun garip olduğunu kabul ediyorum, yani herhalde bu bilgiyi kendi ellerinizle paketleyerek sa_data değişkenine yerleştirmek istemezsiniz değil mi?
struct sockaddr ile başa çıkabilmek için programcılar buna paralel bir yapı tasarlamışlar: struct sockaddr_in ("in" "Internet" anlamına geliyor.)
struct sockaddr_in {
short int sin_family; // Adres ailesi
unsigned short int sin_port; // Port numarası
struct in_addr sin_addr; // Internet adresi
unsigned char sin_zero[8]; // struct sockaddr ile aynı boyda
};

Bu yapı soket adresi elemanlarına erişmeyi kolaylaştırır. Dikkat etmeniz gereken bir nokta: sin_zero,[138] memset() işlevi kullanılarak tamamen sıfır ile doldurulmalıdır. Buna ek ve daha da önemli olarak bir struct sockaddr_in göstergesi struct sockaddr göstergesine dönüştürülebilir ve tersi de doğrudur. Yani her ne kadar socket() işlevi struct sockaddr* şeklinde bir veri beklese de siz gene de struct sockaddr_in kullanıp son anda gerekli dönüştürmeyi yapabilirsiniz! Ayrıca sin_family değişkeninin de struct sockaddr yapısındaki sa_family değişkenine karşılık geldiğini ve "AF_INET" olarak ayarlanması gerektiğini unutmayın. Son olarak sin_port ve sin_addr değikenlerinin de Ağ Byte Sırasında bulunmaları gerektiğini unutmayın!
"Fakat," diye itiraz edebilirsiniz, "nasıl olur da tüm yapı yani struct in_addr sin_addr Ağ Bayt Sıralamasına göre dizilebilir ki?" Bu soru tüm zamanların en kötü union'larından biri olan struct in_addr yapısının dikkatli olarak incelenmesini gerektirir:
// Internet adresi (tarihi sebeplerden ötürü korunmakta)
struct in_addr {
unsigned long s_addr; // 32-bit yani 4 bytes
};

Evet, bir zamanlar kötü bir union idi. Neyse ki geçmişte kaldı. Eğer ina değişkenini struct sockaddr_in türünden tanımladı iseniz ina.sin_addr.s_addr 4 byte'lık Internet adresini gösterir (Ağ Bayt Sıralamasında). Aklınızda bulunsun eğer sizin sisteminiz kahrolası struct in_addr union'ını kullanıyor olsa bile yine de 4 byte'lık Internet adresine tamamen benim yaptığım gibi ulaşabilirsiniz (bunu #define'lara borçluyuz).


--------------------------------------------------------------------------------

[137] Meraklısına not, Ağ Bayt Sıralaması aynı zamanda "Kıymetlisi Başta Bayt Sırası" (Big-Endian) ve Konak Bayt Sıralaması da "Kıymetlisi Sonda Bayt Sırası"(Little-Endian) olarak bilinir.
[138] Burada bulunmasının sebebi içinde bulunduğu veri yapısının boyunu struct sockaddr yapısının boyuna tamamlamaktır.


Veri Türlerini Dönüştür!
Deminden beri şu Ağ ve Konak Bayt Sıralamaları ile yeterince kafa şişirdim şimdi biraz eylem zamanı!
Pekala. Dönüştürebileceğiniz iki tür vardır: short (iki byte) ve long (dört byte). Bu işlevler unsigned ve varyasyonları üzerinde çalışır. Örneğin bir short değişkenin bayt düzenini Konak Bayt Sıralamasından Ağ Bayt Sıralamasına çevirmek istiyorsunuz. "h" ile başlayalım ("host"), sonra "to" ve ardından "n" ("network") son olarak da bir "s" ("short"): h-to-n-s veya htons() ("Host to Network Short" olarak okursanız hatırlamanız kolay olur).
O kadar da zor sayılmaz değil mi...
Aptallarca olanlarını bir kenara bırakırsak "n", "h", "s" ve "l" ile her türlü birleşimi oluşturabilirsiniz. Örneğin tabii ki stolh() ("Short to Long Host") gibi bir işlev yoktur. Fakat şu işlevler vardır:
htons() -- "Host to Network Short" -- konaktan ağa short
htonl() -- "Host to Network Long" -- konaktan ağa long
ntohs() -- "Network to Host Short" -- ağdan konağa short
ntohl() -- "Network to Host Long" -- ağdan konağa long
Bu konuyu kavradığınızı düşünüyor olabilirsiniz. Mesela aklınıza şu gelebilir: "char türünde bir verinin Bayt Sıralamasını değiştirmem gerekirse ne yapmam gerekir?" Ve sonra şöyle cevap verebilirsiniz: "Aman, boşver." Ayrıca aklınıza şu da gelebilir: mesela 68000 işlemcili makinanız zaten Ağ Bayt Sıralamasını kullandığına göre htonl() işlevini IP adreslerine uygulamanıza gerek yoktur. Haklı olabilirsiniz. FAKAT eğer geliştirdiğiniz yazılımı öteki türlü Bayt Sıralamasına göre çalışan bir bilgisayara taşırsanız programınız kesinlikle çalışmaz. Taşınabilir programlar yazın! Unutmayın Unix dünyasındasınız! (Her ne kadar Bill Gates aksini düşünmek istese de.) Sakın unutmayın: baytlarınızı ağ üzerinde yolculuğa çıkarmadan önce onları Ağ Bayt Sıralaması'na göre dizeceksiniz.
Son bir noktaya daha dikkat çekmek istiyorum: neden struct sockaddr_in yapısı içindeki sin_port ve sin_addr Ağ Bayt Sıralamasında olmak zorunda iken sin_family böyle bir özelliğe sahip olmak durumunda değil? Cevabı: sin_addr ve sin_port sırası ile IP ve UDP katmanlarında paketlenirler. Bu yüzden Ağ Bayt Sıralamasında gönderilmeleri gerekir oysa ki sin_family sadece işletim sistemi çekirdeği tarafından veri yapısının barındırdığı adresin türünü tespit etmek için kullanılır. Bu yüzden de bu alanın Konak Bayt Sıralamasında bırakılması gerekir. Ayrıca sin_family ağ üzerinden bir yere yollanmadığı için Konak Bayt Sıralamasında olabilir.




IP Adresleri ve Bunlarla Uğraşma Yöntemleri
Şanslısınız çünkü IP adresleri ile uğraşmanızı sağlayacak bir grup işlev vardır. Yani elle hesap kitap yapıp sonra da bunu bir long içine << işleci ile tıkıştırmanıza gerek yok.
Önce örneğin elinizde bir struct sockaddr_in ina değişkeni ve bir de 10.12.110.57 şeklinde bir IP adresi olduğunu var sayalım. Bu adresi bu değişken içine yerleştirmek istiyorsunuz. Kullanmanız gereken işlev: inet_addr(). Bu işlev bir IP adresini yukarıdaki gibi sayılardan ve noktalardan oluşan bir biçemden alıp unsigned long türünde bir sayıya çevirir. Bu tür bir atama şu şekilde yapılabilir:
ina.sin_addr.s_addr = inet_addr("10.12.110.57");

Şuna dikkat edin: inet_addr() zaten döndürdüğü değeri Ağ Bayt Sıralamasına göre dizilmiş olarak döndürür yani htonl() işlevini burada çağırmanıza gerek yok. Harika!
Yukarıdaki kod parçası pek sağlam sayılmaz çünkü hiç hata denetimi yok. Gördüğünüz gibi inet_addr() hatalı bir durumla karşılaşınca -1 değerini döndürür. İkili sayı sistemini hatırladınız mı? (unsigned)-1 tam da şu IP adresine karşılık gelir: 255.255.255.255! Bu da yayın adresidir! Aman dikkat. Hata kontrolünü düzgün bir şekilde yapmayı sakın ihmal etmeyin.
Aslında inet_addr() yerine kullanabileceğiniz daha güzel bir işlev var: inet_aton() ("aton"u, "ascii to network" olarak okuyun):
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int inet_aton(const char *cp, struct in_addr *inp);

Bir örnek vermek gerekirse: bu işlev struct sockaddr_in yapısını paketlerken (ilerledikçe bu örnek size daha da anlamlı gelecek bind() ve connect() kısımlarına kadar sabredin.)
struct sockaddr_in my_addr;

my_addr.sin_family = AF_INET; // konak bayt sıralaması
my_addr.sin_port = htons(MYPORT); // short, ağ bayt sıralaması
inet_aton("10.12.110.57", &(my_addr.sin_addr));
memset(&(my_addr.sin_zero), '\0', 8); // geriye kalanı sıfırla

inet_aton(), diğer tüm soket-bağlantılı işlevlerden farklı olarak, sorun çıkmazsa sıfırdan farklı bir değer, sorun çıkarsa da sıfır değerini döndürür. Ve döndürülen adres inp içinde yer alır.
Maalesef her platform inet_aton() işlevini kullanmamaktadır. Bu yüzden her ne kadar bu işlevi tercih etsek de daha yaygın olması itibariyle örneklerde inet_addr() kullanılacaktır.
Artık IP adreslerini ikilik sisteme kolayca dönüştürebileceğinize göre bunun tersini yapmaya ne dersiniz? Yani mesela elinizde zaten bir struct in_addr yapısı varsa ve siz bunu alışık olduğunuz sayılı noktalı IP adresi biçeminde basmak istiyorsanız? Bu durumda kullanacağımız işlev: inet_ntoa() ("ntoa"yı "network to ascii" olarak okuyun). Şu şekilde işinizi görür:
printf("%s", inet_ntoa(ina.sin_addr));

Bu IP adresini basacaktır. Dikkat edin: inet_ntoa() argüman olarak struct in_addr alır long almaz. Bir diğer önemli nokta da: Bu işlev bir char'a işaret eden bir gösterge döndürür. Söz konusu gösterge inet_ntoa() içinde statik olarak depolanan bir karakter dizisine işaret eder ve inet_ntoa() işlevini her çağırışınızda son işlem görmüş olan IP adresinin üzerine yazılır. Örneğin:
char *a1, *a2;
.
.
a1 = inet_ntoa(ina1.sin_addr); // burada 192.168.4.14 var
a2 = inet_ntoa(ina2.sin_addr); // burada 10.12.110.57 var
printf("1. adres: %s\n",a1);
printf("2. adres: %s\n",a2);

şunu basar:
1. adres: 10.12.110.57
2. adres: 10.12.110.57

Eğer birden fazla adresle iş yapıyorsanız ve bunları yukarıda olduğu gibi kaybetmek istemiyorsanız o zaman strcpy() gibi bir işlev kullanarak uygun bir yere kopyalamayı sakın ihmal etmeyin.
Bu konu ile söyleyeceklerim şimdilik bu kadar. Daha sonra "whitehouse.gov" gibi bir karakter dizisini hangi yöntemlerle karşılık gelen IP adresine çevirebileceğinizi göstereceğim (bkz. DNS -- Sen "whitehouse.gov" de, ben de "198.137.240.92" diyeyim).


Sistem Çağrıları veya Felaketleri
Bir UNIX bilgisayardaki ağ işlevselliğine erişmenizi anlatacağım bölüme hoşgeldiniz. Bu işlevlerden birini çağırdınızda işletim sistemi çekirdeği devreye girer ve düşük seviyedeki işlemleri büyüleyici bir şekilde sizin için halleder.
İnsanların bu aşamada en çok takıldıkları nokta bu işlevleri hangi sıra ile çağıracakları sorusudur. Bu bakımdan man sayfaları bir işe yaramaz, eğer biraz uğraştıysanız ne demek istediğimi biliyorsunuzdur. Bu zorlu konu ile başa çıkabilmek için işlevleri normalde geliştirdiğiniz bir programdaki çağrılış sıralarına tam olarak (hemen hemen) uygun şekilde size sunmaya çalışacağım.
Bu açıklamalara ek olarak orada burada birkaç örnek kod parçası, biraz süt artı kurabiye (üzgünüm bu son ikisini siz tedarik etmelisiniz) ve tabii biraz da cesaret ile elinizdeki verileri Internet üzerinden her yere ışınlıyor olacaksınız.


socket() -- Al Şu Dosya Tanımlayıcıyı!
Sanırım artık daha fazla erteleyemem -- socket() sistem çağrısından bahsetmek zorundayım. İşte o işlev:
#include <sys/types.h>
#include <sys/socket.h>

int socket(int domain, int type, int protocol);

Peki ya argümanlar? Önce, domain argümanına AF_INET değeri verilmeli, tıpkı struct sockaddr_in yapısında olduğu gibi. Sonra type argümanı çekirdeğe ne tür bir soket söz konusu olduğunu söyler: SOCK_STREAM veya SOCK_DGRAM. Son olarak da protocol argümanına "0" değerini verelim ki socket(), type değişkenine karşılık gelen uygun protokolü seçebilsin. (Dikkat: Bahsettiğimden daha çok domain ve type vardır. Bkz. socket() man sayfası. Ayrıca protocol değerini öğrenmenin "daha iyi" bir yöntemi vardır. Bkz. getprotobyname() man sayfası.)
socket() işlevi size bir soket tanımlayıcı döndürür ve artık siz bunu daha sonraki işlevlerinize parametre olarak geçebilirsiniz. Eğer bir hata oluşursa işlev -1 değerini döndürür. Bu durumda errno isimli global değişken hata kodunu tutar (bkz. perror() man sayfası.)
Bazı belgelerde mistik bir PF_INET ifadesi görebilirsiniz, korkmayın. Normalde bu canavar günlük hayatta pek karşınıza çıkmaz fakat gene de kendisinden biraz bahsedeyim. Uzun zaman önce adres ailesinin (AF_INET'deki "AF" "Address Family" manasındadır) pek çok protokolü destekleyebileceği düşünülmüştü ( PF_INET'deki "PF" "Protocol Family" manasındadır). Ancak böyle bir şey olmadı. Yani doğru olan, AF_INET değerini struct sockaddr_in yapısında kullanmanız ve PF_INET'i ise socket() çağırırken kullanmanızdır. Ancak pratik olarak AF_INET'i her yerde kullanabilirsiniz. Bu işin üstadlarından W. Richard Stevens kitabında böyle yaptığı için ben de burada böyle yapacağım.
Peki tamam çok güzel de bu soket ne işe yarar? Tek başına bir işe yaramaz tabii, lütfen okumaya devam edin ve diğer sistem çağrılarını öğrenin ki taşlar yerine otursun.


bind() -- Hangi Port Üzerindeyim?
Bir soket edindikten sonra bunu makinanızdaki bir "port" ile ilişkilendirmek isteyeceksiniz[139]. Bu port numarası dediğimiz şey işletim sistemi çekirdeği tarafından gelen bir paketi belli bir sürecin soket tanımlayıcısı ile ilişkilendirebilmesi için gereklidir. Eğer tek yapacağınız bir yere connect() ile bağlanmaksa o zaman buna gerek yoktur tabii. Gene de okumaya devam edin, zevk alacaksınız.
bind() sistem çağrısının özetine man komutu ile bakacak olursanız şöyle bir şeyle karşılaşırsınız:
#include <sys/types.h>
#include <sys/socket.h>

int bind(int sockfd, struct sockaddr *my_addr, int addrlen);

sockfd denilen şey socket() tarafından döndürülen soket dosya tanımlayıcısıdır. my_addr değişkeni struct sockaddr türünde bir veriye işaret eder ve bu yapı da adresinizi yani port numaranızı ve IP adresinizi barındırır. addrlen'in değeri sizeof(struct sockaddr) olarak verilebilir.
Vay canına. Bir seferde sindirmek için biraz fazla değil mi? Bir örnek yapalım:
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define MYPORT 3490

main()
{
int sockfd;
struct sockaddr_in my_addr;

sockfd = socket(AF_INET, SOCK_STREAM, 0); // hata kontrolünü ihmal etmeyin!

my_addr.sin_family = AF_INET; // konak bayt sıralaması
my_addr.sin_port = htons(MYPORT); // short, ağ bayt sıralaması
my_addr.sin_addr.s_addr = inet_addr("10.12.110.57");
memset(&(my_addr.sin_zero), '\0', 8); // yapının geriye kalanını sıfırlayalım

// bind() için hata kontrolü yapmayı unutmayın:
bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr));
.
.
.

Burada dikkat etmeniz gereken bazı şeyler var: my_addr.sin_port Ağ Byte Sıralamasında, ve tabii my_addr.sin_addr.s_addr de öyle. Bir başka nokta ise en tepede çağrılmış olan başlık dosyaları sistemden sisteme değişiklik gösterebilir. Kesin bilgi sahibi olabilmek için sisteminizdeki man sayfalarına bakmalısınız.
bind() konusu ile ilgili son söyleyeceğim şey IP ve port edinme işlemini biraz otomaktikleştirilebilir:
my_addr.sin_port = 0; // kullanılmayan herhangi bir port'u seç
my_addr.sin_addr.s_addr = INADDR_ANY; // IP adresimi kullan

Gördüğünüz gibi my_addr.sin_port değişkeninin değerini sıfır yaparak bind() işlevine diyoruz ki "uygun olan bir port sayısını bizim için sen seç". Benzer şekilde my_addr.sin_addr.s_addr değişkeninin değerini INADDR_ANY yaparak üzerinde çalıştığı makinanın IP adresini almasını söylemiş oluyoruz.
Eğer dikkatli bir okuyucu iseniz bazı şeyleri fark etmiş olabilirsiniz, mesela INADDR_ANY değerini Ağ Bayt Sıralamasına dönüştürmedim! Ne kadar da yaramaz bir programcıyım! Ama bildiğim bir şey var: INADDR_ANY zaten SIFIR değerine karşılık geliyor! Yani hangi sırada dizerseniz dizin zaten sıfır. Ancak takıntılı olan programcılar INADDR_ANY sabitinin mesela 12 olarak belirlendiği bir paralel evrenden söz açabilirler ve orada benim kodum çalışmaz. Tamam, sorun değil, hallederiz:
my_addr.sin_port = htons(0); // kullanılmayan herhangi bir port'u seç
my_addr.sin_addr.s_addr = htonl(INADDR_ANY); // IP adresimi kullan

Sistemimiz artık o kadar taşınabilir oldu ki yani bu kadar olur. Bunu burada belirttim çünkü karşılaşacağınız kod örneklerinin çoğu INADDR_ANY sabitini htonl() işlevinden geçirmez, kafanız karışmasın.
bind() eğer bir hata çıkarsa -1 değerini döndürür ve errno isimli hata kodu değişkenine gerekli sayıyı yerleştir.
bind() işlevini çağırırken dikkat etmeniz gereken bir başka şey de şudur: port numarası olarak küçük bir değer seçmeyin. 1024'ün altındaki tüm portlar REZERVE edilmiştir (eğer superuser değilseniz!). Bu sayıdan başlayarak 65535'e kadar olan sayılardan birini port numarası olarak kullanabilirsiniz (tabii eğer seçtiğiniz numara başka bir program tarafından kullanılmıyorsa).
Bazen bir sunucuyu tekrar çalıştırmaya kalktığınızda bind() işlevinin başarısız olduğunu ve "Adres kullanımda" ("Address already in use.") mesajı verdiğiniz görürsünüz. Bu ne anlama gelir? Daha önce kullanılmış bir soket hala çekirdek seviyesinde takılı kalmıştır ve bu yüzden portun kullanılmasını engellemektedir. Ya kendiliğinden iptal olmasını beklersiniz ki bu 1 dakika kadar sürebilir ya da programınıza bu portu her halükârda kullanmasını sağlayacak kodu eklersiniz:
int yes=1;
//char yes='1'; // Solaris programcıları bunu kullanır

// "Address already in use" hata mesajından kurtul
if (setsockopt(listener,SOL_SOCKET,SO_REUSEADDR,&yes,sizeof(int)) == -1) {
perror("setsockopt");
exit(1);
}

bind() ile ilgili ek bilgi
Kimi durumlarda bu işlev ile hiç işiniz olmaz. Mesela eğer connect() ile uzaktaki bir makinaya bağlanıyor ve yerel portunuzun ne olduğu ile ilgilenmiyorsanız (telnet uygulamasında olduğu gibi önemli olan sadece uzaktaki makinadaki port ise) kısaca connect() işlevini çağırırsınız ve zaten bu işlev de soketin bağlı olup olmadığını kontrol eder ve eğer soket bağlı değil ise bind() işlevini kullanarak bunu makinanızdaki kullanılmayan bir port numarasına bağlar.



--------------------------------------------------------------------------------

[139] Yani eğer listen() kullanacaksanız genellikle böyle bir şey yapmanız beklenir zaten. Mesela bir oyun sunucusuna "telnet x.y.z port 6969" şeklinde bağlanmanız söylendiğinde karşı tarafta tam da böyle bir hazırlık yapılmıştır.


connect()--Hey, sen!
Diyelim ki bir telnet programısınız. Sizi kullanan kişi (tıpkı TRON filminde olduğu gibi) size bir soket dosya tanımlayıcısı edinmeniz için komut veriyor. Siz de bu komuta uyup socket() işlevini çağırıyorsunuz. Sonra, kullanıcı sizden "10.12.110.57" adresine ve "23" numaralı porta bağlanmanızı istiyor (standart telnet portu). Aha! Şimdi ne yapacaksınız?
Bir program olarak şanslısınız çünkü tam da connect() bölümündeyiz -- uzaktaki bir bilgisayara nasıl bağlanırız bölümü. Okumaya devam kaybedecek zaman yok, kullanıcı bekliyor!
connect() işlevi şöyle birşeydir:
#include <sys/types.h>
#include <sys/socket.h>

int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);

sockfd dediğimiz değişken socket() sistem çağrısı tarafından döndürülmüş olan soket dosya tanımlayıcısının değerini tutar. serv_addr ise struct sockaddr türünde bir değişkendir ve hedef port ile IP adres bilgilerini barındırır. addrlen değişkeni de sizeof(struct sockaddr) değerini alırsa iyi olur.
Taşlar yerine oturmaya başladı mı? Hemen bir örneğe göz atalım:
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define DEST_IP "10.12.110.57"
#define DEST_PORT 23

main()
{
int sockfd;
struct sockaddr_in dest_addr; // hedef adres

sockfd = socket(AF_INET, SOCK_STREAM, 0); // hata denetimi yapın!

dest_addr.sin_family = AF_INET; // konak bayt sıralaması
dest_addr.sin_port = htons(DEST_PORT); // short, ağ bayt sıralaması
dest_addr.sin_addr.s_addr = inet_addr(DEST_IP);
memset(&(dest_addr.sin_zero), '\0', 8); // yapının geriye kalanını sıfırla

// connect() işlevini çağırdıktan sonra hata denetimi yap!
connect(sockfd, (struct sockaddr *)&dest_addr, sizeof(struct sockaddr));
.
.
.

Sıkılsanız da söylemek zorundayım: connect() işlevinden sonra mutlaka hata denetimi yapın, eğer hata oluştu ise bu işlev -1 değerini döndürür ve errno değişkenine ilgili hata numarasını yerleştirir.
Ayrıca bind() işlevini çağırmadığımıza da dikkat edin. Yani kısaca yerel port numarası ile ilgilenmiyoruz; bizi ilgilendiren hedef bilgisayar (uzaktaki port). İşletim sistemi çekirdeği bizim için bir yerel port bulacaktır ve bağlandığımız bilgisayar da bu bilgiyi otomatik olarak bizden öğrenecektir. Siz tatlı canınızı üzmeyin.


listen() -- Biri Beni Arayabilir mi Acaba?
Pekala hızımızı biraz değiştirelim. Ya bir yere bağlanmak istemiyorsanız ne olacak? Mesela sırf zevk olsun diye size gelen bağlantıları dinlemek ve bunlarla ilgili gerekeni yapmak istediğinizi var sayalım. Bu tür bir süreç iki aşamalıdır, önce listen() ile dinler sonra da accept() ile gelen çağrıları kabul edersiniz.
Dinleme işlevi oldukça basittir ancak gene de biraz açıklama yapmak gerekiyor:
int listen(int sockfd, int backlog);

sockfd bin kere söylediğim gibi socket() sistem çağrısı tarafından döndürülmüş olan soket dosya tanımlayıcısı oluyor. backlog ise gelen çağrı kuyruğunda izin verilen bağlantı sayısını gösteriyor. Bu ne mi demek? Şey, yani gelen bağlantı talepleri siz onları accept() ile kabul edene dek bir kuyrukta bekler demek ve işte bu kuyruğun ne kadar uzun olacağını başka bir deyişle sınırını siz belirlersiniz. Pek çok sistem bu sınırı 20 olarak belirler; siz ise mesela 5 veya 10 gibi bir değer kullanabilirsiniz.
Hemen her zamanki gibi, listen() işlevi hata durumunda -1 değerini döndürür ve tabii ki errno değişkenine de ilgili hata numarasını yazar.
Evet, tahmin edebileceğiniz gibi listen() işlevinden önce bind() işlevini çağırmalıyız yoksa işletim sistemi çekirdeği bu dinleme işlemini gelişigüzel bir port üzerinden yapmaya başlar. Eğer gelen bağlantıları dinleyecekseniz çağırmanız gereken işlevler sırası ile şöyledir:
socket();
bind();
listen();
/* accept() buraya gelecek */

Burada çok açıklama ve kod yok çünkü zaten kendi kendini açıklıyor. (accept() bölümünde ele alacağımız kod tabii ki daha ayrıntılı olacak.) Bütün bu prosedürdeki en dikktali olunması gerken nokta accept() işlevinin çağrıldığı yer.


accept() -- "3490 Numaralı Portu Aradığınız İçin Teşekkürler"
Sıkı durun -- accept() işlevi biraz gariptir! Neler oluyor? Şu oluyor: çok çok uzaklardan birileri connect() işevi ile sizin makinanızda sizin listen() ile dinlemekte olduğunuz bir porta bağlanmaya çalışıyor. Bu bağlantı talebi accept() ile kabul edilene dek kuyrukta bekleyecektir. Siz accept() işlevini çağırırsınız ve ona beklemekte olan çağrıyı kabul etmesini söylersiniz. O da size yepyeni bir soket dosya tanımlayıcısı döndürür sadece ve sadece söz konusu bağlantıya özel. Evet doğru duydunuz birdenbire elinizin altında iki soket dosya tanımlayıcısı oldu! Orjinal olanı halen port üzerinden dinleme işlemini gerçekleştirmek için kullanılıyor yeni olarak yaratılmış olan ise send() ve recv() işlevlerinde kullanılmak üzere emrinize amade. Hele şükür!
İşlevi şu şekilde çağırırsınız:
#include <sys/socket.h>

int accept(int sockfd, void *addr, int *addrlen);

sockfd dediğimiz listen() ile dinlediğiniz soket tanımlayıcıdır. Basit. addr yerel olarak kullandığınız struct sockaddr_in yapısını gösteren bir göstergedir. Gelen bağlantılarla ilgili bilgiler burada barındırılacak (yani bu yapıyı kullanarak gelen bağlantının hangi bilgisayar ve hangi porttan geldiğini öğrenebilirsiniz). addrlen yerel bir tamsayı değişkendir ve kullanılmadan önce accept() sizeof(struct sockaddr_in) değerini almalıdır. accept() işlevi bu değerden daha fazla sayıda baytı addr içine yerleştirmeyecektir. Eğer söz konusu değerden daha azını yerleştirirse o zaman da addrlen değerini, bunu yansıtacak şekilde değiştirecektir.
Tahmin edin ne diyeceğim? accept() hata durumunda -1 değerini döndürür ve errno değişkenine ilgili hata kodunu yerleştirir. Tahmin etmiş miydiniz? Gerçekten?
Biliyorum, bir seferde sindirmesi kolay değil. İsterseniz aşağıdaki örnek koda bakalım:
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define MYPORT 3490 // kullanıcıların bağlanacağı port

#define BACKLOG 10 // kuyrukta bekleyecek bağlantı sayısı

main()
{
int sockfd, new_fd; // sock_fd ile dinle, new_fd yeni bağlantı için
struct sockaddr_in my_addr; // benim adres bilgim
struct sockaddr_in their_addr; // bağlananın adres bilgisi
int sin_size;

sockfd = socket(AF_INET, SOCK_STREAM, 0); // hata denetimi yap!

my_addr.sin_family = AF_INET; // konak bayt sıralaması
my_addr.sin_port = htons(MYPORT); // short, ağ bayt sıralaması
my_addr.sin_addr.s_addr = INADDR_ANY; // otomatik olarak benim IP'im
memset(&(my_addr.sin_zero), '\0', 8); // geriye kalanı sıfırla

// hata denetimi yapmayı sakın unutmayın:
bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr));

listen(sockfd, BACKLOG);

sin_size = sizeof(struct sockaddr_in);
new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size);
.
.
.

Lütfen dikkat edin: new_fd şeklindeki soket tanımlayıcıyı tüm send() ve recv() işlevleri için kullanacağız. Eğer sadece ve sadece tek bir bağlantıyı kabul edecekseniz close() ile sockfd üzerindeki dinleyici soketi kapatabilirsiniz böylece aynı port üzerinden başka bağlantı yaplamaz, eğer istediğiniz gerçekten bu ise.

send() ve recv() -- Konuş Benimle Bebeğim!
Bu iki işlev veri akış soketler veya bağlantılı veri paketi soketleri üzerinden veri göndermenizi ve veri almanızı sağlar. Eğer istediğiniz düzenli ve bağlantısız veri paketi soketleri kullanmak ise o zaman sendto() ve recvfrom() işlevleri ile ilgili aşağıdaki bölümü okumalısınız.
send() işlevi şu şekilde çağrılır:
int send(int sockfd, const void *msg, int len, int flags);

sockfd üzerinden veri göndereceğiniz sokettir (size socket() veya accept() tarafından sağlanmış olabilir). msg göndermek istediğiniz mesajı gösteren bir göstergedir ve len değişkeni bu verinin byte cinsinden uzunluğudur. flags parametresini 0 olarak bırakabilirsiniz. (Bu parametre ile ilgili ayrıntılı bilgi için bkz. send() man sayfası.)
Örnek bir kod parçası vermek gerekirse:
char *msg = "Beej buradaydi!";
int len, bytes_sent;
.
.
len = strlen(msg);
bytes_sent = send(sockfd, msg, len, 0);
.
.
.

send() değer olarak gönderilen bayt miktarını döndürür -- bu sizin gönderilmesini istediğiniz miktardan az olabilir! Gördüğünüz gibi siz ona bir yığın veri göndermesini söylersiniz ancak o bazen bunun tamamı ile başa çıkamayabilir. Elinden geldiği kadarını gönderir ve geriye kalan veriyi yeniden göndermek sizin sorumluluğunuzdadır. Unutmayın, eğer send() işlevinin döndürdüğü değer len değişkenindeki değer kadar değilse göndermek istediğiniz verinin geriye kalanını göndermek sizin işinizdir. İyi haberlere gelince: Eğer paket küçükse (1k civarı) bu işlev büyük ihtimalle tüm veriyi bir seferde gönderebilecektir. Her zamanki gibi hata durumunda -1 değerini döndürür ve errno küresel değişkenine hata kodunu yazar.
recv() işlevi da pek çok bakımdan yukarıdaki işleve benzer:
int recv(int sockfd, void *buf, int len, unsigned int flags);

sockfd üzerinden okuma işlemini gerçekleştireceğiniz sokettir, buf okunan verinin yazılacağı bellek bölgesinin başlangıç adresini gösteren göstergedir, len ise verinin yazılacağı tamponun (buffer) azami boyudur ve flags yine 0 değerini alabilir (Ayrıntılı bilgi için recv() man sayfasına bakınız).
recv() okunduktan sonra tampona yazılan bayt miktarını döndürür ya da eğer bir hata oluştu ise -1 değerini döndürüp errno değişkeninini değerini belirler.
Bir dakika! recv() 0 değerini döndürebilir! Bunun tek bir anlamı vardır: karşı taraf bağlantıyı kesmiş! Döndürülen değerin 0 olması recv() işlevinin size "karşı taraf bağlantıyı kesti" mesajını vermesi demektir.
Bütün bunları anlamak kolaydı değil mi? Artık verilerinizi soketler üzerinden yollayıp alabilirsiniz! Vay be! Artık siz bir Unix Ağ Programlama Uzmanısınız!


sendto() ve recvfrom() -- Benimle UDP'ce Konuş
"Buraya dek çok güzeldi," dediğinizi duyar gibiyim, "fakat bu bilgileri bağlantısız veri paketi soketleri üzerinde nasıl kullanabilirim ki?". No problemo, amigo. İşte derdine çare.
UDP soketleri uzaktaki bir konağa bağlı olmadığından kılavuz paketi yollamadan önce hangi bilgiyi vermemiz gerekiyor? Doğru tahmin! Hedef adres! Kısaca:
int sendto(int sockfd, const void *msg, int len, unsigned int flags,
const struct sockaddr *to, int tolen);

send() işlevine ne kadar da benziyor değil mi? Tek farkı fazladan iki bilgi parçası var. to dediğimiz struct sockaddr türünde bir değişkeni gösteren işaretçidir (normalde struct sockaddr_in türündedir ve siz son anda gerekli tür dönüşümünü yaparsınız) ve hedef IP adresi ile port numarasını barındırır. tolen değişkeni sizeof(struct sockaddr) değerini almalıdır.
Tıpkı send() gibi, sendto() işlevi de gönderilen bayt miktarını döndürür (bu beklediğinizden az olabilir tabii ki!) ve eğer hata oldu ise -1 değerini döndürür.
Tahmin edebileceğiniz gibi recv() ve recvfrom() işlevleri birbirlerine çok benzerdir. Kısaca recvfrom() işlevine bakarsak:
int recvfrom(int sockfd, void *buf, int len, unsigned int flags,
struct sockaddr *from, int *fromlen);

Benzer olduğunu söylemiştim yani: recv() işlevinde olduğu gibi, sadece birkaç ek değişkene ihtiyacı var. from yerel bir struct sockaddr türünde değişkenin adresini gösteren göstergedir ki bu değişken de mesajın geldiği ilgili makinanın IP adresini ve port numarasını barındıracaktır. fromlen yerel ve int türünde bir göstergedir ve söz konusu değişkenin alması gereken ilk değer sizeof(struct sockaddr)'dir. İşlev, çalışıp bir değer döndürünce fromlen değişkeni from değişkenindeki adresin boyunu depoluyor olacaktır.
recvfrom() işlevi okuduğu bayt sayısını veya bir hata oluşması durumunda -1 değerini döndürür (ve errno değişkenine uygun hata kodunu yerleştirir).
Unutmayın, eğer connect() ile bir UDP soketine bağlantı kurarsanız send() ve recv() işlevlerini kullanmanızda bir sakınca yoktur. Soketin kendisi hala bir bağlantısız veri paketi soketidir ve gidip gelen paketler de hala UDP protokolünü kullanır. Fakat soket arayüzü otomatik olarak sizin yerinize gerekli hedef ve kaynak bilgisini pakete ekler.


close() ve shutdown() -- Düş Yakamdan!
Vay be! Deminden beri send() ile veri gönderip recv() ile de veri okuyorsunuz ve artık yoruldunuz. Soket tanımlayıcınız ile ilişkilendirilmiş olan bağlantıyı kesmenin zamanıdır. Kolayı var. Alışık olduğunuz Unix dosya tanımlayıcı kapatma işlevi olan close() işlevini kullanın:
close(sockfd);

Böylece artık bu sokete ne yazılabilir ne de buradan veri okunabilir. Diğer uçta bunları yapmaya çalışan kişi artık bir hata mesajı ile karşılaşacaktır.
Eğer soket kapatma işlemi üzerinde biraz daha deentim sahibi olmak isterseniz o zaman shutdown() işlevini tercih edebilirsiniz. Bu işlevi kullanarak iletişim kanalını tek yönlü ya da çift yönlü olarak kapatabilirsiniz ( close() işlevi iki taraflı olarak keser). İşlev şöyledir:
int shutdown(int sockfd, int how);

sockfd kapatmak istediğiniz soket dosya tanımlayıcısıdır ve how değişkeni de şu değerlerden birini alabilir:
0 -- Bundan sonraki okumalara izin verme
1 -- Bundan sonraki göndermelere (yazmalara) izin verme
2 -- Bundan sonraki göndermelere ve okumalara izin verme (close() işlevinin yaptığı gibi)
shutdown() başarılı olarak görevini tamamlarsa 0 döndürür ama eğer bir hata ile karşılaşırsa -1 döndürür (ve errno değişkenine hata kodunu yazar).
Eğer shutdown() işlevini bağlantısız veri paketi soketleri üzerinde kullanırsanız bu soketler artık send() ve recv() işlevleri tarafından kullanılamaz hale gelirler (bunları, connect() ile bağlanmak istediğinizde kullanabileceğinizi unutmayın).
Bir başka önemli nokta da: shutdown() aslında dosya tanımlayıcısını kapatmaz sadece kullanılabilirliğini değiştirir. Soket tanımlayıcısını gerçekten iptal etmek istiyorsanız close() kullanmalısınız.

getpeername() -- Kimsiniz?
Bu işlev o kadar kolay ki!
Yani gerçekten o kadar kolay ki ayrı bir bölümü hak ediyor mu bilemiyorum.Neyse gene de yazdım işte.
getpeername() bağlantılı veri akış soketinin diğer tarafında kim olduğunu söyler:
#include <sys/socket.h>

int getpeername(int sockfd, struct sockaddr *addr, int *addrlen);

sockfd bağlantılı veri akış soketinin tanımlayıcısıdır, addr, struct sockaddr (veya struct sockaddr_in) türündeki bir göstergedir ki bu da iletişimin diğer ucundaki konak ile ilgili bilgileri tutar. addrlen, int türündeki göstergedir, alması gereken ilk değer ise sizeof(struct sockaddr) olarak verilir.
Bu işlev hata durumunda -1 değerini döndürür ve errno değişkenine gerekli değeri yazar.
Bir kere karşı tarafın adres bilgisine eriştikten sonra inet_ntoa() veya gethostbyaddr() işlevleri ile daha fazla bilgi edinebilirsiniz. Hayır, onların login ismini öğrenemezsiniz. (Tamam, tamam. Eğer diğer bilgisayar identd sürecini çalıştırıyor ise bu mümkündür. Ancak bu konumuz dışında[140]


--------------------------------------------------------------------------------


gethostname() -- Ben kimim?
getpeername() işevinden daha kolay bir işlev varsa o da gethostname() işlevidir. Programınızın üzerinde çalıştığı konağın ismini döndürür. Bu isim daha sonra gethostname() tarafından makinanızın IP adresini tespit etmek için kullanılabilir.
Bundan daha eğlenceli bir şey olabilir mi? Aslında aklıma geliyor ama soket programlama ile ilgili değil. Neyse devam edelim:
#include <unistd.h>

int gethostname(char *hostname, size_t size);

Argümanlar gayet basit: hostname işlev çağrıldıktan sonra bilgisayarın ismini barındıracak karakter dizisinin göstergesidir ve size değişkeni de hostname dizisinin bayt cinsinden uzunluğudur.
İşler yolunda giderse, işlev 0 değerini döndürür ve hata oluşursa da -1 değerini döndürüp errno değişkenini gerekli şekilde ayarlar.


DNS -- Sen "whitehouse.gov" de, ben de "198.137.240.92" diyeyim
Eğer DNS'in ne olduğunu bilmiyorsanız söyleyeyim: "Domain Name System" yani "Alan Adı Sistemi" demek. Kısaca siz ona kolayca hatırlayabildiğiniz adresi verirsiniz o da size buna karşılık gelen IP adresini verir (ki siz de bu bilgiyi bind(), connect(), sendto() ve diğer işlevleri çağırırken kullanabilesiniz). Böylece biri:
$ telnet whitehouse.gov

komutunu girdiğinde telnet yazılımı connect() işlevine verilmesi gereken "198.137.240.92" bilgisine erişebilir.
Peki bu nasıl çalışır? Bunun için gethostbyname() işlevini kullanacaksınız:
#include <netdb.h>

struct hostent *gethostbyname(const char *name);

Gördüğünüz gibi bu işlev struct hostent türünde bir gösterge döndürür. Söz konusu türün ayrıntılı yapısı da şöyledir:
struct hostent {
char *h_name;
char **h_aliases;
int h_addrtype;
int h_length;
char **h_addr_list;
};
#define h_addr h_addr_list[0]

Bu karmaşık değişkenin içindeki alanların açıklamalarına gelince:
h_name -- Konağın resmi ismi.
h_aliases -- Söz konusu konağın alternatif isimleri, NULL ile sonlandırılmış karakter dizileri şeklinde.
h_addrtype -- Dönen adresin türü, genellikle AF_INET değerini alır.
h_length -- Byte cinsinden adresin uzunluğu.
h_addr_list -- Konağın ağ adresinin sıfır sonlandılmalı dizisi. Konak adresleri ağ bayt sıralamasına sahiptir.
h_addr -- h_addr_list listesindeki ilk adres.
gethostbyname() çalıştıktan sonra içini doldurduğu struct hostent türünden bir gösterge döndürür. Eğer hata oluşursa NULL döndürür. (Fakat errno değişkeni ile ilgili herhangi bir işlem yapmaz -- bunun yerine h_errno değişkeni kullanılır. Ayrıntılı bilgi için aşağıdaki herror() işlevine bakın.)
Peki tüm bu bilgileri nasıl kullanacağız? Bazen okuyucuya bilgi yığınını verip onu bununla başbaşa bırakmak yeterli olmaz (bilgisayar belgelerini okuyanlar ne demek istediğimi anlıyorlardır). Bu işlevi kullanmak düşündüğünüzden daha kolaydır.
İşte örnek bir program:
/*
** getip.c -- konak isminden IP adresini elde edilmesi
*/

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main(int argc, char *argv[])
{
struct hostent *h;

if (argc != 2) { // komut satırında hata denetimi
fprintf(stderr,"usage: getip konak_ismi\n");
exit(1);
}

if ((h=gethostbyname(argv[1])) == NULL) { // konak bilgilerini al
herror("gethostbyname");
exit(1);
}

printf("Konak ismi: %s\n", h->h_name);
printf(" IP Adresi: %s\n", inet_ntoa(*((struct in_addr *)h->h_addr)));

return 0;
}

gethostbyname() söz konusu olduğunda perror() işlevini hata mesajını basmak için kullanamazsınız (çünkü errno gerekli değeri almaz). Bunun yerine herror() işlevini kullanmalısınız.
Açıklama basit: Doğrudan ilgilendiğiniz makinanın Internet adresini verirsiniz ("whitehouse.gov") ve gethostbyname() işlevi de çalışıp işini bitirdikten sonra size struct hostent türünde bir değişken döndürür.
Buradaki tek gariplik şudur: IP adresini basarken kullanmanız gereken h->h_addr, char* türündedir ama inet_ntoa() işlevi struct in_addr türünde bir değişken isteme konusunda ısrarcı olduğu için önce h->h_addr değiğkenini struct in_addr* türüne çevirdim ve ardından veriye ulaştım.


İstemci-Sunucu Mimarisi

İstemci-sunucu dünyasında yaşıyoruz, bunu kabul et dostum. Ağ ortamındaki hemen her süreç ya da uygulama öyle ya da böyle sunucu süreçleriyle konuşup aldıkları cevaplara göre iş yapıyor ya da tersi oluyor. Mesela telnet programını ele alalım. Siz uzaktaki bir konağın 23 numaralı portuna telnet (istemci) ile bağlandığınızda o konakta da telnetd (sunucu) isimli bir program soluk alıp vermeye başlar. Bu program gelen telnet bağlantılarını dinler, size bir "login:" istemi gönderir, vs. vs.
Şekil 5.1. Sunucu-İstemci Etkileşimi

Sunucu-İstemci Etkileşimi
İstemci ile sunucu arasındaki bilgi alışverişi özetlenmiştir.
Dikkat etmeniz gereken noktalardan biri de şudur: istemci-sunucu çifti birbirleri ile SOCK_STREAM, SOCK_DGRAM veya başka bir tarzda konuşuyor olabilirler (yeter ki aynı dili konuşsunlar). İstemci-sunucu çiftlerine birkaç meşhur örnek vermek gerekirse: telnet/telnetd, ftp/ftpd veya bootp/bootpd. Yani ne zaman bir ftp programı çalıştırsanız diğer tarafta ftpd gibi size hizmeti sunan bir süreç vardır.
Genellikle sunucu makinada ilgili hizmet için tek bir sunucu program çalışır ve bu program kendisine bağlanan birden fazla istemciye fork() işlevi aracılığı ile hizmet eder. Süreci özetlemek gerekirse: Sunucu bir bağlantı isteği için bekleyecek, accept() işlevi ile bunu kabul edecek ve fork() işlevi ile bir çocuk süreç yaratıp veri alışverişini bu sürece devredecek. Bir sonraki bölümde inceleyeceğimiz örnek sunucu yazılımın yaptığı iş tam da yukarıda anlatıldığı gibidir.


Basit Bir Veri Akış Sunucusu
Bu sunucunun yaptığı tek bir iş var: "Hello, World!\n" dizgesiniz bir veri akış bağlantısı üzerinden karşı tarafa yollamak. Sunucunun düzgün çalışıp çalışmadığını test etmek için tek yapmanız gereken derledikten sonra bir pencerede çalıştırmak ve ardından başka bir terminal penceresi açıp telnet yazılımı ile sunucuya şu şekilde erişmek:
$ telnet remotehostname 3490

Burada remotehostname sunucuyu üzerinde çalıştırdığınız makinanızın adıdır[141].
Sunucu yazılımın C dilindeki kaynak kodu aşağıdaki gibidir:: (Bilgi: Bir satırın sonundaki \ satırın alt satırda devam ettiğini ifade eder.)
/*
** server.c -- bir veri akış soketi sunucusu örneği
*/

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>

#define MYPORT 3490 // kullanıcıların bağlanacağı port

#define BACKLOG 10 // bağlantı kuyruğunda bekletileceklerin sayısı

void sigchld_handler(int s)
{
while(wait(NULL) > 0);
}

int main(void)
{
int sockfd, new_fd; // sock_fd ile dinle, yen bağlantıyı new_fd ile al
struct sockaddr_in my_addr; // adres bilgim
struct sockaddr_in their_addr; // bağlananın adres bilgisi
int sin_size;
struct sigaction sa;
int yes=1;

if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket");
exit(1);
}

if (setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&yes,sizeof(int)) == -1) {
perror("setsockopt");
exit(1);
}

my_addr.sin_family = AF_INET; // konak bayt sıralaması
my_addr.sin_port = htons(MYPORT); // short, ağ bayt sıralaması
my_addr.sin_addr.s_addr = INADDR_ANY; // otomatik olarak IP'mi kullan
memset(&(my_addr.sin_zero), '\0', 8); // geriye kalan bölgeyi sifirla

if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr))
== -1) {
perror("bind");
exit(1);
}

if (listen(sockfd, BACKLOG) == -1) {
perror("listen");
exit(1);
}

sa.sa_handler = sigchld_handler; // tüm ölü süreçleri kaldır
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
if (sigaction(SIGCHLD, &sa, NULL) == -1) {
perror("sigaction");
exit(1);
}

while(1) { // ana accept() döngüsü
sin_size = sizeof(struct sockaddr_in);
if ((new_fd = accept(sockfd, (struct sockaddr *)&their_addr,
&sin_size)) == -1) {
perror("accept");
continue;
}
printf("server: got connection from %s\n",
inet_ntoa(their_addr.sin_addr));
if (!fork()) { // çocuk süreç
close(sockfd); // çocuk sürecin dinlemesi gerekmez
if (send(new_fd, "Hello, world!\n", 14, 0) == -1)
perror("send");
close(new_fd);
exit(0);
}
close(new_fd); // ana sürecin buna ihtiyacı yok
}

return 0;
}

Meraklısına not: Evet kodu tek bir büyük main() işlevi olarak yazdım, anlaşılması basit olsun diye, ancak eğer isterseniz bunu işlevlere ayırabilirsiniz.
(Bir de şu sigaction() meselesi size yabancı gelebilir -- haklısınız. O gördüğünüz kod fork() ile oluşturulan çocuk süreçler sonlandıktan sonra kalabilecek "zombi" proseslerin icabına bakmak için var. Eğer böyle bir sürü zombi süreç olur ve siz bir şekilde onların icabına bakmazsanız bunlar gereksiz yere sistem kaynaklarını kullanır ve bu da sistem yöneticinizin size sinirlenmesine yol açar.)
Şimdi yukarıdaki sunucudan veri çekebilecek bir istemci program örneğine bakalım.


--------------------------------------------------------------------------------

[141] Sunucu makina ile telnet'i çalıştırdığınız makina aynı ise localhost ismini de kullanabilirsiniz.



Veri Paketi Soketleri
Buraya dek yazılanları anladı iseniz çok fazla açıklamaya gerek yok, örnek programların kodunu inceleyerek kavrayabilirsiniz: talker.c ve listener.c..
listener programı bir makinada çalıştıktan sonra 4950 numaralı port üzerinden gelecek paketleri dinlemeye başlar. talker ise komut satırında belirteceğiniz bir makinanın 4950 numaralı portuna yine komut satırından yazdığınız mesajı bir veri paketi olarak yollar.
listener.c programının kaynak koduna bakalım:
/*
** listener.c -- a veri paketi soketi sunucusu örneği
*/

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define MYPORT 4950 // kullanıcıların bağlanacağı port

#define MAXBUFLEN 100

int main(void)
{
int sockfd;
struct sockaddr_in my_addr; // adres bilgim
struct sockaddr_in their_addr; // bağlananın adres bilgisi
int addr_len, numbytes;
char buf[MAXBUFLEN];

if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
perror("socket");
exit(1);
}

my_addr.sin_family = AF_INET; // konak bayt sıralaması
my_addr.sin_port = htons(MYPORT); // short, ağ bayt sıralaması
my_addr.sin_addr.s_addr = INADDR_ANY; // otomatik olarak IP'mi kullan
memset(&(my_addr.sin_zero), '\0', 8); // kalanı sıfırla

if (bind(sockfd, (struct socka
Benzer Sayfalar (Şuan okuduğunuz sayfa ile benzer olan sayfalar listelenmektedir)
#BaşlıkYazanKategoriOkunmaTarih
1Python KılavuzuFresTeRDökümanlar » Programlama9312007-02-09 21:30
2Sub-Seven Gold Kullanma KılavuzuPİSKOP@T H@CKERDökümanlar » Gerekli Bilgiler30062006-10-22 03:24
3Photoshop Kullanım KılavuzuF1R4TDökümanlar » Grafik ve Resim42142006-11-06 20:36
4Programlama Nedir?F1R4TDökümanlar » Programlama26982006-10-18 09:40
5Kaç programlama dili vardır?F1R4TDökümanlar » Programlama15482006-10-18 09:46
6Perl ile Veritabanı ProgramlamaRoXeTTeDökümanlar » Programlama7892006-11-07 00:20
7Bilgisayar Programcılığı ve Programlama DilleriCooL JacKDökümanlar » Programlama40742006-10-17 16:28
8VISUAL BASIC (Programlama Mantığı)ultra@slanDökümanlar » Programlama12202007-05-24 23:19
38.107.191.119 logged
Sayfa oluşturma süresi 0.128 saniye
SQL toplam zamanı: 0.01 saniye - SQL sorgusu: 39 - Ortalama SQL zamanı: 0.00024 saniye

[ Site - Sayfa ]
Bağlantılar:
Woltran Sinema izle video izle video izle video video izle izle forum edius dersleri