> TCP/IP Socket Programming : Kursumuzun bu bölümünde IP prokol ailesi ile ağa bağlı birimler arasında haberleşmelerin nasıl yapıldığı üzerinde duracağız. Bu bağlamda TCP ve UDP protokollerini inceleyeceğiz ve bu protokolleri kullanarak soket arayüzü ile temel programların nasıl yazıldığnı açıklayacağız. Farklı makinelerin prosesleri arasında haberleşme (yani bir ağ içerisinde haberleşme), aynı makinenin prosesleri arasındaki haberleşmeye göre daha karmaşık unsurlar içermektedir. Çünkü burada ilgili işletim sisteminin dışında pek çok belirlemelerin önceden yapılmış olması gerekir. İşte ağ haberleşmesinde önceden belirlenmiş kurallar topluluğuna "protokol" denilmektedir. Ağ haberleşmesi için tarihsel süreç içerisinde pek çok protokol ailesi gerçekletirilmiştir. Bunların bazıları büyük şirketlerin kontrolü altındadır ve hala kullanılmaktadır. Ancak açık bir protokol ailesi olan "IP protokol ailesi" günümüzde farklı makinelerin prosesleri arasındaki haberleşmede hemen her zaman tercih edilen protokol ailesidir. Protokol ailesi (protocol family) denildiğinde birbirleriyle ilişkili bir grup protokol anlaşılmaktadır. Bir protokol ailesinin pek çok protokolü başka protokollerin üzerine konumlandırılmış olabilmektedir. Böylece protokol aileleri katmanlı (layered) bir yapıya sahip olmuştur. Üst seviye bir protokol alt seviye protokolün "zaten var olduğu fikriyle" o alt seviye protokol kullanılarak oluşturulmaktadır. Bu katmanlı yapıyı prosedürel programlama tekniğinde "zaten var olan bir fonksiyonu kullanarak daha yüksek seviyeli bir fonksiyon yazmaya" benzetebiliriz. Ağ haberleşmesi için katmanlı bir protokol yapısının kavramsal olarak nasıl oluşturulması gerektiğine yönelik ISO tarafından 80'li yılların başlarında "OSI Model (Open System Interconnection Model)" isimli bir referans dokümanı oluşturulmuştur. OSI model bir gerçekleştirim değildir. Kavramsal bir referans dokümanıdır. Ancak bu referans dokümanı pek çok çalışma için bir zemin oluşturmuştur. OSI referans modeline göre bir protokol ailesinde tipik olarak 7 katman bulunmalıdır. Bu katmanlar aşağıdaki gibi birbirlerini üzerine oturtulmuştur: Uygulama Katmanı (Application Layer) Sunum Katmanı (Presentation Layer) Oturum Katmanı (Session Layer) Aktarım Katmanı (Transort Layer) Network Katmanı (Network Layer) Veri Bağlantı Katmanı (Data Link Layer) Fiziksel Katman (Physical Layer) Bu katmanlardan, >> En aşağı seviyeli elektriksel tanımlamaların yapıldığı katmana "fiziksel katman (physical layer)" denilmektedir. (Örneğin kabloların, konnektörlerin özellikleri, akım, gerilim belirlemeleri vs. gibi.) Yani bu katman iletişim için gereken fiziksel ortamı betimlemektedir. >> Veri bağlantı katmanı (data link layer) artık bilgisayarlar arasında fiziksel bir adreslemenin yapıldığı ve bilgilerin paketlere ayrılarak gönderilip alındığı bir ortam tanımlarlar. Yani bu katmanda bilgilerin gönderildiği ortam değil, gönderilme biçimi ve fiziksel adresleme tanımlanmaktadır. Ağ üzerinde her birimin donanımsal olarak tanınabilen fiziksel bir adresinin olması gerekir. Örneğin bugün kullandığımız Ethernet kartları "Ethernet Protocolü (IEEE 802.11)" denilen bir protokole uygun tasarlanmıştır. Bu ethernet protokolü OSI'nin fiziksel ve veri bağlantı katmanına karşılık gelmektedir. Ethernet protokolünde yerel ağa bağlı olan her birimin ismine "MAC adresi" denilen 6 byte'lık fiziksel bir adresi vardır. Ethernet protokolünde MAC adresini bildiğimiz ağa bağlı bir birime bilgi gönderebiliriz. Bilgiler "paket anahtarlaması (packet switching)" denilen teknikle gönderilip alınmaktadır. Bu teknikte byte'lar bir paket adı altında bir araya getirilir sonra ilgili fiziksel katmanla seri bir biçimde gönderilir. Bugün kullandığımız yerel ağlarda aslında bilgi bir birimden diğerine değil hub'lar yoluyla ağa bağlı olan tüm birimleregönderilmektedir. Ancak bunlardan yalnızca biri gelen bilgiyi sahiplenmektedir. Bugün kablosuz haberleşmede kullanılan "IEEE 802.11" protokolü de tıpkı Ethernet protokolü gibi hem bir fiziksel katman hem de veri bağlantı katmanı tanımlamaktadır. Fiziksel katman ve veri katmanı oluşturulduğunda artık biz yerel ağda bir birimden diğerine paket adı altında bir grup byte'ı gönderip alabilir duruma gelmekteyiz. >> Ağ Katmanı (network layer) artık "internetworking" yapmak için gerekli kuralları tanımlamaktadır. "Internetworking" terimi "network'lerden oluşan network'ler" anlamına gelir. Aynı fiziksel ortamda bulunan ağlara "Yerel Ağlar (Local Area Networks)" denilmektedir. Bu yerel ağlar "router" denilen aygıtlarla birbirlerine bağlanmaktadır. Böylece "internetworking" ortamı oluşturulmaktadır. Tabii böyle bir ortamda artık ağa bağlı birimler için fiziksel adresler kullanılamaz. Bu ortamlarda ağa bağlı birimlere mantıksal bir adreslerin atanması gerekmektedir. İşte "network katmanı" internetworking ortamı içerisinde bir birimden diğerine bir paket bilginin gönderilmesi için gereken tanımlamaları içermektedir. Ağ katmanı bu nedenle en önemli katmandır. Ağ katmanında artık fiziksel adresleme değil, mantıksal adresleme sistemi kullanılmaktadır. Ayrıca bilgilerin paketlere ayrılarak router'lardan dolaşıp hedefe varması için rotalama mekanizması da bu katmanda tanımlanmaktadır. Yani elimizde yalnızca ağ katmanı ve onun aşağısındaki katmanlar varsa biz artık "internetworking" ortamında belli bir kaynaktan belli bir hedefe paketler yollayıp alabiliriz. >> Aktarım katmanı (transport layer) network katmanının üzerindedir. Aktarım katmanında artık kaynak ile hedef arasında mantıksal bir bağlantı oluşturulabilmekte ve veri aktarımı daha güvenli olarak yapılabilmektedir. Aynı zamanda aktarım katmanı "multiplex" bir kaynak-hedef yapısı da oluşturmaktadır. Bu sayede bilgiler hedefteki spesifik bir programa gönderilebilmektedir. Bu işleme "port numaralandırması" da denilmektedir. Bu durumda aktarım katmanında tipik şu işlemlere yönelik belirlemeler bulunmaktadır: -> Bağlantnın nasıl yapılacağına ilişkin belirlemeler -> Ağ katmanından gelen paketlerin stream tabanlı organizasyonuna ilşkin belirlemeler -> Veri aktarımını güvenli hale getirmek için akış kontrolüne ilişkin belirlemeler -> Gönderilen bilgilerin hedefte ayrıştıtılmasını sağlayan protokol port numaralandırmasına ilişkin belirlemeler >> Oturum katmanı (session) katmanı pek çok protokol ailesinde yoktur. Görevi oturum açma kapama gibi yüksek seviyeli bazı belirlemeleri yapmaktır. Örneğin bu katmanda bir grup kullanıcıyı bir araya getiren oturumların nasıl açılacağına ve nasıl kapatılacağına ilişkin belirlemeler bulunmaktadır. IP protokol ailesinde OSI'de belirtilen biçimde bir oturum katmanı yoktur. >> Sunum katmanı (presentation layer) verilerin sıkıştırılması, şifrelenmesi gibi tanımlamalar içermektedir. Yine bu katman IP protokol ailesinde OSI'de belirtildiği biçimde bulunmamaktadır. >> Nihayet protokol ailesini kullanarak yazılmış olan tüm kullanan bütün programlar aslında uygulama katmanını oluşturmaktadır. Yani ağ ortamında haberleşen her program zaten kendi içerisinde açık ya da gizli bir protokol oluşturmuş durumdadır. Örneğin IP protokol ailesindeki somut işleri yapmakta kullanılan Telnet, SSH, HTTP, POP3, FTP gibi protokoller uygulama katmanı protokolleridir. Bugün farklı makinelerin prosesleri arasında en çok kullanılan protokol ailesi IP (Internet Protocol) denilen protokol ailesidir. IP protokol ailesi temel ve yardımcı pek çok protokolden oluşmaktadır. Aileye ismini veren ailenin "ağ katmanı (network layer)" protokolü olan IP protoküdür. Pekiyi pekiyi IP ailesi neden bu kadar popüler olmuştur? Bunun en büyük nedeni 1983 yılında hepimizin katıldığı Internet'in (I'nin büyük yazıldığına dikkat ediniz) bu aileyi kullanmaya başlamasıdır. Böylece IP ailesini kullanarak yazdığımız programlar hem aynı bilgisayarda hem yerel ağımızdaki bilgisayarlarda hem de Internet'te çalışabilmektedir. Aynı zamanda IP ailesinin açık bir (yani bir şirketin malı değil) protokol olması da cazibeyi çok artırmıştır. IP ailesi 70'li yıllarda Vint Cerf ve Bob Kahn tarafından geliştirilmiştir. IP ismi Internet Protocol'den gelmektedir. Burada internet "internetworking" anlamında kullanılmıştır. Cerf ve Kahn 1974 yılında önce TCP protokolü üzerinde sonra da IP protokolü üzerinde çalışmışlar ve bu protokollerin ilk versiyonlarını oluşturmuşlardır. Bugün hepimizin bağlandığı büyük ağa "Internet" denilmektedir. Bu ağ ilk kez 1969 yılında Amerika'da Amerikan Savunma Bakanlığı'nın bir soğuk savaş projesi biçiminde başlatıldı. O zamana kadar yalnızca kısıtlı ölçüde yerel ağlar vardı. 1969 yılında ilk kez bir "WAN (Wide Area Network)" oluşturuldu. Bu proje Amerikan Savunma Bakanlığı'nın DARPA isimli araştırma kurumu tarafından başlatılmıştır ve projeye "ARPA.NET" ismi verilmiştir. Daha sonra bu ağa Amerika'daki çeşitli devlet kurumları ve üniversiteler katıldı. Sonra ağ Avrupa'ya sıçradı. 1983 yılında bu ağ NCP protokolünden IP protokol ailesine geçiş yaptı. Bundan sonra artık APRA.NET ismi yerine "Internet" ismi kullanılmaya başlandı. (Internet sözcüğü I herfi küçük harfle yazılırsa "internetworking" anlamında büyük harfle yazılırsa bugün katıldığımız dev ağ anlamında kullanılmaktadır.) Biz de IP ailesini kullanarak kendi "internetworking" ortamımızı oluşturabiliriz. Örneğin bir şirket hiç Internet'e bağlanmadan kendi internet'ini oluşturabilir. Buna eskiden "intranet" denirdi. IP protokol ailesi herkesin kendi internet'ini oluşturabilmesi için bütün gerekli protokolleri barındırmaktadır. Tabii sinerji bakımından herkes zaten var olan ve "Internet" denilen bu dev ağa bağlanmayı tercih etmektedir. IP protokol ailesi 4 katmanlı bir ailedir. Bu ailede "fiziksel ve veri bağlantı katmanı" bir arada düşünülebilir. Bugün bunlar Ethernet ve Wireless protokolleri biçiminde kullanılmaktadır. IP ailesinin ağ katmanı aileye ismini veren IP protokolünden oluşmaktadır. Aktarım katmanı ise TCP ve UDP protokollerinden oluşur. Nihayet TCP üzerine oturtulmuş olan HTTP, TELNET, SSH, POP3, IMAP gibi pek çok protokol ailenin uygulama katmanını oluşturmaktadır. Tabii IP protokol ailesinde bu hiyerarşik yapıyla ilgili olmayan irili ufaklı pek çok protokol de bulunmaktadır. +---------------------+-------------------------------+ | Application Layer | HTTP, SSH, POP3, IMAP, ... | +---------------------+---------------+---------------+ | Transport Layer | TCP | UDP | +---------------------+---------------+---------------+ | Network Layer | IP | +---------------------+-------------------------------+ | Physical/Data Link | Ethernet | | Layer | Wireless | +---------------------+-------------------------------+ IP protokolü tek başına kullanılırsa ancak ağa bağlı bir birimden diğerine bir paket gönderip alma işini yapar. Bu nedenle bu protokolün tek başına kullanılması çok seyrektir. Uygulamada genellikle "aktarım (transport) katmanına" ilişkin TCP ve UDP ptotokolleri kullanılmaktadır. IP ailesinin uygulama katmanındaki HTTP, SSH, POP3, IMAP, FTP gibi önemli protokollerinin hepsi TCP protokolü üzerine oturtulmuştur. Ailede genellikle TCP protokolü kullanıldığı için buna kısaca "TCP/IP" de denilmektedir. IP protokolü ailenin en önemli ve taban protokolüdür. IP protokolünde ağa bağlı olan ve kendisine IP adresiyle erişilebilen her birime "host" denilmektedir. IP protokolü bir host'tan diğerine bir paket (buna IP paketi denilmektedir) bilginin gönderimine ilişkin tanımlamaları içermektedir. IP protokolünde her host'un ismine "IP adresi" denilen mantıksal bir adresi vardır. Paketler belli bir IP adresinden diğerine gönderilmektedir. IP protokolünün iki önemli versiyonu vardır: IPv4 ve IPv6. Bugün her iki versiyon da aynı anda kullanılmaktadır. IPv4'te IP adresleri 4 byte uzunluktadır. (Protokolün tasarlandığı 70'li yıllarda 4 byte adres alanı çok geniş sanılmaktaydı). IPv6'da ise IP adresleri 16 byte uzunluğundadır. TCP bağlantılı (connection-oriented), UDP bağlantısız (connectionless) bir protokoldür. Buradaki bağlantı IP paketleriyle yapılan mantıksal bir bağlantıdır. Bağlantı sırasında gönderici ve alıcı birbirlerini tanır ve haberleşme boyunca haberleşmenin güvenliği için birbirleriyle konuşabilirler. Bağlantılı protokol "client-server" tarzı bir haberleşmeyi akla getirmektedir. Bu nedenle TCP/IP denildiğinde akla "client-server" haberleşme gelmektedir. TCP modelinde client önce server'a bağlanır. Sonra iletişim güvenli bir biçimde karşılıklı konuşmalarla sürdürürlür. Tabii TCP bunu yaparken IP paketlerini yani IP protokolünü kullanmaktadır. UDP protokolü bağlantısızdır. Yani UDP protokolünde bizim bir host'a UDP paketi gönderebilmemiz için bir bağlantı kurmamıza gerek kalmaz. Örneğin biz televizyon yayını UDP modeline benzemektedir. Verici görüntüyü yollar ancak alıcının alıp almadığıyla ilgilenmez. Vericinin görüntüyü yollaması için alıcıyla bağlantı kurması gerekmemektedir. TCP "stream tabanlı", UDP ise "datagram (paket) tabanlı" bir protokoldür. Stream tabanlı protokol demek tamamen boru haberleşmesinde olduğu gibi gönderen tarafın bilgilerinin bir kuyruk sistemi eşliğinde oluşturulması ve alıcının istediği kadar byte'ı parça parça okuyabilmesi demektir. Datagram tabanlı haberleşme demek ise tamamen mesaj kuyruklarında olduğu gibi bilginin paket paket iletilmesi demektir. Yani datagram haberleşmede alıcı taraf gönderen tarafın tüm paketini tek hamlede almak zorundadır. Stream tabanlı haberleşmenin oluşturulabilmesi için IP paketlerine bir numara verilmesi ve bunların hedefte birleştirilmesi gerekmektedir. Örneğin biz bir host'tan diğerine 10K'lık bir bilgi gönderelim. TCP'de bu bilgi IP paketlerine ayrılıp numaralandırılır. Bunlar hedefte birleştirilir ve sanki 10000 byte'lık ardışıl bir bilgiymiş gibi gösterilir. Halbuki UDP'de paketler birbirinden bağımsızdır. Dolayısıyla bunların hedefte birleştirilmesi zorunlu değildir. IP protokolünde bir host birtakım paketleri diğer host'a gönderdiğinde alıcı taraf bunları aynı sırada almayabilir. Bu özelliğinden dolayı TCP, ailenin en çok kullanılan aktarım (transport) katmanı protokolüdür. TCP güvenilir (reliable), UDP güvenilir olmayan (unreliable) bir protokoldür. TCP'de mantıksal bir bağlantı oluşturulduğu için yolda kaybolan paketlerin telafi edilmesi mümkündür. Alıcı taraf gönderenin bilgilerini eksiksiz ve bozulmadan aldığını bilir. Aynı zamanda TCP'de "bir akış kontrolü (flow control)" de uygulanmaktadır. Akış kontrolü sayesinde alıcı taraf tampon taşması durumuna karşı gönderici tarafı durdurabilmektedir. Halbuki UDP'de böyle bir mekanizma yoktur. Gönderen taraf alıcının bilgiyi alıp almadığını bilmez. Tüm bunlar eşliğinde IP ailesinin en çok kullanılan aktarım (transport) katmanının neden TCP olduğunu anlayabilirsiniz. Uygulama katmanındaki protokoller hep TCP kullanmaktadır. Yukarıda da belirttiğimiz gibi IP protokol ailesinde ağa bağlı olan birimlere "host" denilmektedir. Host bir bilgisayar olmak zorunda değildir. İşte bu protokol ailesinde her host'un mantıksal bir adresi vardır. Bu adrese IP adresi denilmektedir. IP adresi IPv4'te 4 byte uzunlukta, IPv6'da 16 byte uzunluktadır. Ancak bir host'ta farklı programlar farklı host'larla haberleşiyor olabilir. İşte aynı host'a gönderilen IP paketlerinin o host'ta ayrıştırılması için "protokol port numarası" diye isimlendirilen içsel bir numara uydurulmuştur. Port numarası bir şirketin içerisinde çalışanların dahili numarası gibi düşünülebilir. Port numaraları IPv4'te ve IPv6'da 2 byte'la ifade edilmektedir. İlk 1024 port numarası IP ailesinin uygulama katmanındaki protokoller için ayrılmıştır. Bunlara İngilizce "well known ports" denilmektedir. Bu nedenle programcıların port numaralarını 1024'ten büyük olacak biçimde seçmeleri gerekir. Bir host TCP ya da UDP kullanarak bir bilgi gönderecekse bilginin gönderileceği host'un IP numarasını ve bilginin orada kime gönderileceğini anlatan port numarasını belirtmek zorundadır. IP numarası ve port numarası çiftine "IP End Point" de denilmektedir. Bilgiyi almak isteyen program kendisinin hangi portla ilgilendiğini de belirtmek durumundadır. Örneğin biz bir host'ta çalışacak bir TCP/IP ya da UDP/IP program yazmak istiyorsak o host'un belli bir port numarasına gelen bilgilerle ilgileniriz. Port numarası kavramının IP protokolünde olmadığına TCP ve UDP protokollerinde bulunduğuna dikkat ediniz. TCP ve UDP protokollerinin IP protokolü üzerine oturdulduğunu belirtmiştik. Bu ne anlama gelmektedir? Biz TCP ile belli bir IP numarası ve port numarası (end point) belirterek bir grup byte'ı göndermiş olalım. Aslında bu byte topluluğu bir TCP paketi oluşturularak bir IP paketi biçiminde yola çıkarılmaktadır. Şöyle ki: IP paketlerinin yapısı şöyledir: +-------------------------+ | IP Header | +-------------------------+ | IP Data | +-------------------------+ Burada IP Header'da söz konusu IP paketinin hedefe ulaştırılabilmesi için gerekli bilgiler bulunur. Gönderilecek asıl bilgi bu paketin "IP Data" kısmındadır. İşte bir TCP paketi aslında bir IP paketi olarak IP paketinin "IP Data" kısmına gömülerek gönderilmektedir. Bu durumda TCP paketinin genel görünümü şöyledir: +-------------------------+ | IP Header | +-------------------------+ <---+ | TCP Header | | +-------------------------+ IP Data | TCP Data | | +-------------------------+ <---+ TCP paketinin de bir header ve data alanı olduğuna ancak paketin tamamının IP paketinin data alanında yolculuk ettirildiğine dikkat ediniz. Yani TCP paketinin header ve data kısmı aslında IP paketinin data kısmı gibi oluşturulmaktadır. Böylece yolculuk eden paket aslında bir TCP paketi değil IP paketidir. TCP bilgileri bu IP paketinin data kısmında bulunmaktadır. IPv4 başlık uzunluğu 20 byte'dır. IPv4 paket başlık alanları aşağıdaki verilmiştir. <------- Byte 1 -------><------- Byte 2 -------><------- Byte 3 -------><------- Byte 4 -------> +-----------+-----------+----------------------+-----------------------------------------------+ ^ | Version | IHL | Type of Service | Total Length | (4 bytes) | | (4 bits) | (4 bits) | (8 bits) | (16 bits) | | +-----------+-----------+----------------------+-----------+-----------------------------------+ | | Identification | Flags | Fragment Offset | (4 bytes) | | (16 bits) | (3 bits) | (13 bits) | | +-----------------------+----------------------+-----------+-----------------------------------+ | | Time to Live (TTL) | Protocol | Header Checksum | (4 bytes) | 20 bytes | (8 bits) | (8 bits) | (16 bits) | | +-----------------------+----------------------+-----------------------------------------------+ | | Source IP Address (32 bits) | (4 bytes) | +----------------------------------------------------------------------------------------------+ | | Destination IP Address (32 bits) | (4 bytes) | +----------------------------------------------------------------------------------------------+ v | Segment (L4 protocol (TCP/UDP) + Data) | +----------------------------------------------------------------------------------------------+ TCP header'ı 20 byte'tan oluşmaktadır ve yapısı aşağıdaki gibidir. <------- Byte 1 -------><------- Byte 2 -------><------- Byte 3 -------><------- Byte 4 -------> +----------------------------------------------+-----------------------------------------------+ ^ | Source Port | Destination Port | (4 bytes) | | (16 bits) | (16 bits) | | +----------------------------------------------+-----------------------------------------------+ | | Sequence Number | (4 bytes) | | (32 bits) | | +----------------------------------------------------------------------------------------------+ | | Acknowledgement Number | (4 bytes) | | (32 bits) | | 20 bytes +-----------+----------------+-----------------+-----------------------------------------------+ | |Header Len.| Reserved | Control Bits | Window Size | (4 bytes) | | (4 bits) | (6 bits) | (6 bits) | (16 bits) | | +-----------+----------------+-----------------+-----------------------------------------------+ | | Checksum | Urgent | (4 bytes) | | (16 bits) | (16 bits) | | +----------------------------------------------+-----------------------------------------------+ v | Options | | (0 or 32 bits) | +----------------------------------------------------------------------------------------------+ | Application Layer Data | | (Size Varies) | +----------------------------------------------------------------------------------------------+ UDP header'ı 8 byte'tan oluşmaktadır ve yapısı aşağıdaki gibidir. <------- Byte 1 -------><------- Byte 2 -------><------- Byte 3 -------><------- Byte 4 -------> +----------------------------------------------+-----------------------------------------------+ ^ | Source Port | Destination Port | (4 bytes) | | (16 bits) | (16 bits) | | +----------------------------------------------+-----------------------------------------------+ | 8 bytes | Header Length | Checksum | (4 bytes) | | (16 bits) | (16 bits) | | +----------------------------------------------+-----------------------------------------------+ v | Application Layer Data | | (Size Varies) | +----------------------------------------------------------------------------------------------+ IP haberleşmesi (yani paketlerin, oluşturulması, gönderilmesi alınması vs.) işletim sistemlerinin çekirdekleri tarafından yapılmaktadır. Tabii User mode programlar için sistem çağrılarını yapan API fonksiyonlarına ve kütüphanelerine gereksinim vardır. İşte bunların en yaygın kullanılanı "soket kütüphanesi" denilen kütüphanedir. Bu kütüphane ilk kez 1983 yılında BSD 4.2'de gerçekleştirilmiştir ve pek çok UNIX türevi sistem bu kütüphaneyi aynı biçimde benimsemiştir. Sonra bu kütüphane POSIX standartlarına da dahil edilmiştir. Microsoft Windows sistemleri için kendi soket kütüphanesini oluşturmuştur. Buna "Windows Socket API (WSA)" denilmektedir. Ancak Microsoft aynı zamanda klasik BSD soket arayüzünü de desteklemektedir. Yani biz Windows sistemlerinde hem başı WSAXXX ile başlayan Windows'a özgü soket fonksiyonlarını hem de klasik Berkeley soket fonksiyonlarını kullanabilmekteyiz. Böylece UNIX/Linux sistemlerinde yazdığımız soket programlarını küçük değişikliklerle Windows sistemlerine taşıyabilmekteyiz. Berkeley soket kütüphanesi yalnızca IP protokol ailesi için tasarlanmış bir kütüphane değildir. Bütün protokollerin ortak kütüphanesidir. Bu nedenle kütüphanedeki fonksiyonlar daha genel biçimde tasarlanmıştır. Biz soket fonksiyonlarını kullanırken aslında arka planda işlemler TCP/IP ve UDP/IP protokollerine uygun bir biçimde gerçekleştirilmektedir. Örneğin biz send soket fonksiyonu ile bir bilgiyi göndermek istediğimizde aslında bu fonksiyon arka planda bir TCP paketi dolayısıyla da bir IP paketi oluşturarak protokole uygun bir biçimde bu bilgiyi göndermektedir. Soket kütüphanesinin yalnızca bir API arayüzü olduğuna dikkat ediniz. Yukarıda da belirttiğimiz gibi Berkeley soket kütüphanesi POSIX tarafından desteklenmektedir. Yani burada göreceğimiz soket fonksiyonları aynı zamanda birer POSIX fonksiyonudur. Bir TCP/IP uygulamasında iki ayrı program yazılır: "TCP Server Program" ve "TCP Client Program". Biz önce TCP server programın daha sonra da TCP client programın yazımı üzerinde duracağız. Tabii TCP server programın üzerinde dururken zaten bazı ortak soket fonksiyonlarını da göreceğiz. >> "TCP Server" Program : Bir TCP server program tipik olarak aşağıdaki soket API'lerinin sırayla çağrılmasıyla gerçekleştirilmektedir: (Windows'ta WSAStartup --->) socket ---> bind ---> listen ---> accept ---> send/recv (ya da UNIC/Linux'ta read/write) ---> shutdown ---> close (Windows'ta closesocket) (---> Windows'ta WSACleanup) Buradan da gördüğünüz gibi her ne kadar Windows UNIX tarzı Bekeley soket kütüphanesini destekliyorsa da şu küçük farklılıklara sahiptir: -> Windows'ta soket sistemini başlatmak ve sonlandırmak için WSAStartup ve WSACleanup API fonksiyonları kullanılmaktadır. -> Windows'ta soket nesnesini yok etmek için close yerine closesocket API fonksiyonu bulunmaktadır. -> Windows'ta bazı istisnalar dışında bir soket fonksiyonu başarısız olduğunda başarısızlığın nedeni GetLastError fonksiyonuyla değil WSAGetLastError fonksiyonuyla elde edilmektedir. Bu nedenle Windows örneğimizde hataları rapor etmek için ExitSys fonksiyonunu aşağıdaki biçimde tanımlayacağız: void ExitSys(LPCSTR lpszMsg, DWORD dwLastErr) { LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } -> Windows'ta soket kütüphanesi API fonksiyonlarının bulunduğu kütüphaneler içerisinde değildir. Bu nedenle link amasında soket API'lerinin bulunduğu dinamik kütüphanenin import kütüphanesi ("Ws2_32.lib") linker ayarlarında belirtilmelidir. -> Windows sistemlerinde soket fonksiyonlarının protoipleri dosyası içerisindedir. Halbuki UNIX/Linux sistemlerinde fonksiyonların prototipleri farklı başlık dosyalarında bulunabilmektedir. Eğer dosyası da include edilecekse önce dosyası sonra dosyası include edilmelidir. Şimdi de bu fonksiyonları sırasıyla inceleyelim: >>> "WSAStartup" : Windows'ta soket sistemini başlatmak için WSAStartup fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir: #include int WSAStartup( WORD wVersionRequired, LPWSADATA lpWSAData ); Fonksiyonun birinci parametresi talep edilen soket kütüphanesinin versiyonunu belirtmektedir. Buradaki WORD değer iki ayrı byte'ın birleşiminden oluşmaktadır. MAKEWORD(high, low) makrosu ile bu parametre için argüman oluşturulabilir. Windows soket kütüphanesinin son versiyonu 2.2 versiyonudur. Dolayısıyla bu parametre için argümanı MAKEWORD(2, 2) biçiminde geçebiliriz. Eğer talep edilen versiyon yüksekse bu durum hataya yol açmamakta talep edilenden küçük olan en yüksek versiyon kullanıma hazır hale getirilmektedir. Fonksiyonun ikinci parametresi WSADATA isimli bir yapı nesnesinin adresini almaktadır. Fonksiyon bu yapıya bazı bilgiler yerleştirmektedir. Fonksiyon başarı durumunda 0 değerine başarısızlık durumunda hata kodunun kendisine geri dönmektedir. (Yani bu fonksiyon için WSAGetLastError çağrısına gerek yoktur.) Fonksiyon tipik olarak şöyle kullanılır: WSADATA wsaData; int result; ... if ((result = WSAStartup(MAKEWORD(2, 2), &wsaData)) != 0) ExitSys("WSAStartup", result); >>> "socket" : Haberleşme için öncelikle bir soket nesnesinin yaratılması gerekmektedir. Bu işlem socket isimli fonksiyonla yapılmaktadır. socket fonksiyonu bir soket nesnesi (handle alanı) yaratır ve bize handle değeri verir. Windows sistemlerinde socket fonksiyonunun geri döndürdüğü handle değeri SOCKET isimli bir türdendir. Ancak UNIX/Linux sistemlerinde bir dosya betimleyicisi biçimindedir. Yani UNIX/Linux sistemlerinde socket nesneleri tamamen bir dosya gibi kullanılmaktadır. Windows sistemlerinde socket fonksiyonunun prototipi şöyledir: #include SOCKET socket( int af, int type, int protocol ); UNIX/Linux sistemlerindeki prototipi ise şöyledir: #include int socket(int domain, int type, int protocol); Fonksiyonların parametreleri her iki sistemde de aynıdır. Yukarıda da gördüğünüz gibi tek fark Windows sistemlerinde soketin handle değerinin SOCKET türüyle UNIX/Linux sistemlerinde ise int türüyle temsil edilmesidir. socket fonksiyonunun birinci parametresi kullanılacak protokol ailesini belirtir. Bu parametre AF_XXX (Address Family) biçimindeki sembolik sabitlerden biri olarak girilir. IPv4 için bu parametreye AF_INET, IPv6 için AF_INET6 girilmelidir. UNIX domain soketler için bu parametre AF_UNIX olarak girilmelidir. Fonksiyonun ikinci parametresi kullanılacak protokolün stream tabanlı mı yoksa datagram tabanlı mı olacağını belirtmektedir. Stream soketler için SOCK_STREAM, datagram soketler için SOCK_DGRAM kullanılmalıdır. Ancak başka soket türleri de vardır. TCP protokolünde bu parametre SOCK_STREAM biçiminde UDP protokülünde ise bu parametre SOCK_DGRAM biçiminde girilmelidir. Fonksiyonun üçüncü parametresi aktarım (transport) katmanındaki protokolü belirtmektedir. Ancak zaten ikinci parametreden aktarım protokolü anlaşılıyorsa üçüncü parametre 0 olarak geçilebilmektedir. Örneğin IP protokol ailesinde üçüncü parametreye gerek duyulmamaktadır. Çünkü ikinci parametredeki SOCK_STREAM zaten TCP'yi, SOCK_DGRAM ise zaten UDP'yi anlatmaktadır. Fakat yine de bu parametreye istenirse IP ailesi için IPPROTO_TCP ya da IPPROTO_UDP girilebilir. (Bu sembolik sabitler UNIX/Linux sistemlerinde içerisindedir.) socket fonksiyonu başarı durumunda Windows'ta soket handle değerine UNIX/Linux sistemlerinde ise soket betimeleyicisine geri dönemktedir. Fonksiyon başarısızlık durumunda Windows'ta SOCKT_ERROR değerine UNIX/Linux sistemlerinde ise -1 değerine geri dönmektedir. Windows sistemlerindeki SOCKET_ERROR sembolik sabiti de zaten -1 biçiminde define edilmiştir. Ancak Windows sistemlerinde SOCKET_ERROR sembolik sabitinin kullanılması gerekir. Örneğin, Windows sistemlerinde socket nesnesi şöyle yaratılabilir: SOCKET serverSock; ... if ((serverSock = socket(AF_INET, SOCK_STREAM, 0)) == SOCKET_ERROR) ExitSys("socket", WSAGetLastError()); Aynı işlem UNIX/Linux sistemlerinde şöyle yapılabilir: int server_sock; ... if ((server_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); >>> "bind" : Server program soketi yarattıktan sonra onu bağlamalıdır (bind etmelidir). bind işlemi sırasında server'ın hangi portu dinleyeceği ve hangi network arayüzünden (kartından) gelen bağlantı isteklerini kabul edeceği belirlenir. Ancak bind fonksiyonu dinleme işlemini başlatmaz. Yalnızca soket nesnesine bu bilgileri yerleştirir. Fonksiyonun Windows sistemlerindeki prototipi şöyledir: #include int bind( SOCKET s, const sockaddr *addr, int namelen ); UNIX/Linux sistemlerindeki prototipi ise şöyledir: #include int bind(int socket, const struct sockaddr *addr, socklen_t addrlen); Görüldüğü gibi fonksiyonların iki sistemde de prototipi aynıdır. Ancak Windows sistemlerinde soket nesnesi SOCKET türüyle temsil edilmektedir. Biz kursumuzda anlatımı kolaylaştırmak için her iki sistemde de socket handle değerine soket betimelyicisi diyeceğiz. bind fonksiyonunun birinci parametresi yaratılmış olan soket betimleyicisini belirtir. İkinci parametre her ne kadar sockaddr isimli bir yapı türünden gösterici ise de de aslında her protokol için ayrı bir yapı nesnesinin adresini almaktadır. Yani sockaddr yapısı burada genelliği (void gösterici gibi) temsil etmek için kullanılmıştır. IPv4 için kullanılacak yapı sockaddr_in, IPv6 için sockaddr_in6 ve örneğin UNIX domain soketler için ise sockaddr_un biçiminde olmalıdır. Üçüncü parametre, ikinci parametredeki yapının uzunluğu olarak girilmelidir. sockaddr_in yapısı UNIX/Linux sistemlerinde dosyası içerisindedir. Windows sistemlerinde bu yapı şöyle bildirilmiştir: #include typedef struct sockaddr_in { short sin_family; u_short sin_port; struct in_addr sin_addr; char sin_zero[8]; } SOCKADDR_IN, *PSOCKADDR_IN, *LPSOCKADDR_IN; UNIX/Linux sistemlerinde ise bu yapı şöyle bildirilmiştir: #include struct sockaddr_in { sa_family_t sin_family; in_port_t sin_port; struct in_addr sin_addr; }; Yapı elemanlarının türleri için iki sistemde farklı typedef isimleri kullanılmış olasa da elemanlar aynı anlamdadır. Yapının sin_family elemanına protokol ailesini belirten AF_XXX değeri girilmelidir. Bu eleman tipik olarak short biçimde bildirilmiştir. Yapının sin_port elemanı her iki sistemde de unsigned short türdendir. Server programın hangi portu dinleyeceği bu elemanla belirlenmektedir. Yapının sin_addr elemanı IP numarası belirten bir elemandır. Bu eleman in_addr isimli bir yapı türündendir. Bu yapı Windows sistemlerinde şöyle bildirilmiştir: struct in_addr { union { struct { u_char s_b1; u_char s_b2; u_char s_b3; u_char s_b4; } S_un_b; struct { u_short s_w1; u_short s_w2; } S_un_w; u_long S_addr; } S_un; }; #define s_addr S_un.S_addr UNIX/Linux sistemlerinde ise in_addr yapısı dosyası içerisinde şöyle bildirilmiştir: #include struct in_addr { in_addr_t s_addr; }; Aslında her iki sistemde de yapının s_addr elemanı IPV4 için 4 byte'lık işaretsiz tamsayı türü belirtmektedir. İşte bu 4 byte'lık işaretsiz tamsayı türü IPV4 için IP adresini belirtmektedir. Eğer buradaki IP adresi INADDR_ANY biçiminde geçilirse bu durum "herhangi bir network kartından gelen bağlantı isteklerinin kabul edileceği" anlamına gelmektedir. Yukarıda da belirttiğimiz gibi IP ailesinde tüm sayısal değerler "big endian" formatıyla belirtilmek zorundadır. Bu ailede "network byte ordering" denildiğinde "big endian" format anlaşılır. Oysa makinelerin belli bir bölümü (örneğin Intel ve default ARM) "little endian" kullanmaktadır. İşte elimizdeki makinenin endian'lığı ne olursa olsun onu big endian formata dönüştüren htons (host to network byte ordering short) ve htonl (host to network byte ordering long) isimli iki fonksiyon vardır. Bu işlemlerin tersini yapan da ntohs (network byte ordering to host short) ve ntohl (network byte ordering to host long) fonksiyonları da bulunmaktadır. Fonksiyonların Windows sistemlerindeki prototipleri şöyledir: #include u_short htons( u_short hostshort ); u_long htonl( u_long hostlong ); u_short ntohs( u_short netshort ); u_long ntohl( u_long netlong ); Fonksiyonların UNIX/Linux sistemlerindeki prototipleri şöyledir: #include uint32_t htonl(uint32_t hostlong); uint16_t htons(uint16_t hostshort); uint32_t ntohl(uint32_t netlong); uint16_t ntohs(uint16_t netshort); Bu durumda sockaddr_in yapısı her iki sistemde de tipik olarak şöyle doldurulabilir: struct sockaddr_in sinaddr; sinaddr.sin_family = AF_INET; sinaddr.sin_port = htons(SERVER_PORT); sinaddr.sin_addr.s_addr = htonl(INADDR_ANY); bind fonksiyonu başarı durumunda sıfır değerine, başarısızlık durumunda Windows sistemlerinde SOCKET_ERROR değerine, UNIX/Linux sistemlerinde ise -1 değerine geri dönmektedir. Örneğin Windows sistemlerinde fonksiyon şöyle çağrılabilir: if (bind(serverSock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == SOCKET_ERROR) ExitSys("bind", WSAGetLastError()); UNIX/Linux sistemlerinde de çağrı şöyle olabilir: if (bind(server_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == -1) exit_sys("bind"); >>> "listen" : server program bind işleminden sonra soketi aktif dinleme konumuna sokmak için listen fonkiyonunu çağırmalıdır. Fonksiyonun Windows sistemlerindeki prototipi şöyledir: #include int listen( SOCKET s, int backlog ); UNIX/Linux sistemlerindeki prototipi ise şöyledir: #include int listen(int socket, int backlog); Fonksiyonun birinci parametresi soket betimleyicisini, ikinci parametresi kuyruk uzunluğunu belirtir. listen işlemi blokeye yol açmamaktadır. İşletim sistemi listen işleminden sonra ilgili porta gelen bağlantı isteklerini uygulama için oluşturduğu bir bağlantı kuyruğuna yerleştirmektedir. Kuyruk uzunluğunu yüksek tutmak meşgul server'larda bağlantı isteklerinin kaçırılmamasını sağlayabilir. Linux'ta default durumda verilebilecek en yüksek değer 128'dir. Ancak "/proc/sys/net/core/somaxconn" dosyasındaki değer değiştirilerek bu default uzunluk artırılabilir. Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda Windows sistelerinde SOCKET_ERROR değerine, UNIX/Linux sistemlerinde ise -1 değerine geri dönmektedir. Örneğin Windows sistemlerinde fonksiyon şöyle çağrılabilir: if (listen(serverSock, 8) == SOCKET_ERROR) ExitSys("listen", WSAGetLastError()); UNIX/Linux sistemlerinde de çağrı şöyle yapılabilir: if (listen(server_sock, 8) == -1) exit_sys("listen"); Bu fonksiyon işletim sistemlerinin "firewall mekanizması" tarafından denetlenebilmektedir. Eğer çalıştığınız sistemde söz konusu port firewall tarafından kapatılmışsa bunu açmanız gerekir. (Windows sistemlerinde listen fonksiyonu bir pop pencere çıkartarak uyarı mesajı görüntülemektedir.) Yukarıda da belirttiğimiz gibi listen fonksiyonu herhangi bir blokeye yol açmaz. Bu fonksiyon çağrıldıktan sonra işletim sistemi ilgili porta gelecek bağlantı isteklerini prosese özgü bir bağlantı kuyruğuna yerleştirmektedir. Bu bağlantı kuyruğundan bağlantı isteklerini alarak bağlantısıyı sağlayan asıl fonksiyon accept fonksiyonudur. >>> "accept" : accept fonksiyonu bağlantı kuyruğuna bakar. Eğer kuyrukta bir bağlantı isteği varsa onu alır ve hemen geri döner. Eğer orada bir bağlantı isteği yoksa default durumda blokede bekler. Ancak accept fonksiyonu blokesiz modda da kullanılabilmektedir. Fonksiyonun Windows sistemlerindeki prototipi şöyledir: #include SOCKET accept( SOCKET s, struct sockaddr *addr, int *addrlen ); UNIX/Linux sistemlerindeki prototipi ise şöyledir: #include int accept(int socket, struct sockaddr *address, socklen_t *address_len); Fonksiyonların birinci parametreleri dinleme soketinin dosya betimleyicisini almaktadır. İkinci parametre bağlanılan client'a ilişkin bilgilerin yerleştirileceği sockaddr_in yapısının adresini almaktadır. Bu parametre yine genel bir sockaddr yapısı türünden gösterici ile temsil edilmiştir. Bizim bu parametre için IPv4'te sockaddr_in türünden, IPv6'da sockaddr_in6 türünden bir yapı nesnesinin adresini argüman olarak vermemiz gerekir. sockaddr_in yapısının üç elemanı olduğunu anımsayınız. Biz bu parametre sayesinde bağlanan client programın IP adresini ve client makinedeki port numarasını elde edebilmekteyiz. Client program server programa bağlanırken bir IP adresi ve port numarası belirtir. Ancak kendisinin de bir IP adresi ve port numarası vardır. Client'ın port numarası kendi makinesindeki (host'undaki) port numarasıdır. Client'ın IP adresine ve oradaki port numarasına "remote end point" de denilmektedir. Örneğin 178.231.152.127 IP adresinden bir client programın 52310 port'u ile server'ın bulunduğu 176.234.135.196 adresi ve 55555 numaralı portuna bağlandığını varsayalım. Burada remote endpoint "178.231.152.127:52310" biçiminde ifade edilmektedir. İşte biz accept fonksiyonunun ikinci parametresinden client hakkında bu bilgileri almaktayız. Client (178.231.152.127:52310) ---> Server (176.234.135.196:55555) accept fonksiyonunun üçüncü parametresi yine ikinci parametredeki yapının (yani sockaddr_in yapısının) byte uzunluğunu belirtmektedir. Ancak bu parametre bir adres olarak alınmaktadır. Yani programcı Windows'ta int türünden UNIX/Linux sistemlerinde socklen_t türünden bir nesne tanımlamalı, bu nesneye bu sizeof değerini yerleştirmeli ve nesnenin adresini de fonksiyonun üçüncü parametresine geçirmelidir. Fonksiyon bağlanılan client'a ilişkin soket bilgilerinin byte uzunluğunu yine bu adrese yerleştirmektedir. Tabii IP protokol ailesinde her iki taraf da aynı yapıyı kullanıyorsa fonksiyon çıkışında bu sizeof değerinde bir değişiklik olmayacaktır. Ancak tasarım genel yapıldığı için böyle bir yola gidilmiştir. accept fonksiyonu başarı durumunda bağlanılan client'a ilişkin yeni bir soket betimleyicisine geri dönmektedir. Artık bağlanılan client ile bu soket yoluyla konuşulacaktır. accept fonksiyonu başarısızlık durumunda Windows sistemlerinde SOCKET_ERROR değerine UNIC/Linux sistemlerinde ise -1 değeri ile geri dönmektedir. Örneğin Windows sistemlerine accpet fonksiyonu şöyle kullanılabilir: struct sockaddr_in sin_client; int sin_len; SOCKET serverSock; ... sin_len = sizeof(sin_client); if ((clientSock = accept(serverSock, (struct sockaddr *)&sin_client, &sin_len)) == SOCKET_ERROR) ExitSys("accept", WSAGetLastError()); UNIX/Linux sistemlerinde de fonksiyon şöyle kullanılabilir: struct sockaddr_in sin_client; socklen_t sin_len; int client_sock; ... sin_len = sizeof(sin_client); if ((client_sock = accept(server_sock, (struct sockaddr *)&sin_client, &sin_len)) == -1) exit_sys("accept"); Server tarafta temelde iki soket bulunmaktadır. Birincisi bind, listen, accept işlemini yapmakta kullanılan sokettir. Bu sokete TCP/IP terminolojisinde ""pasif soket (passive socket)" ya da "dinleme soketi (listening socket)" denilmektedir. İkinci soket ise client ile konuşmakta kullanılan accept fonksiyonunun geri döndürdüğü sokettir. Buna da "aktif soket (active socket)" denilmektedir. Tabii server program birden fazla client ile konuşacaksa accept fonksiyonunu bir kez değil, çok kez uygulamalıdır. Her accept o anda bağlanılan client ile konuşmakta kullanılabilecek yeni bir soket vermektedir. bind, listen işlemleri bir kez yapılmaktadır. Halbuki accept işlemi her client bağlantısı için ayrıca uygulanmalıdır. accept fonksiyonu default durumda blokeli modda çalışmaktadır. Eğer accept çağrıldığında o anda bağlantı kuyruğunda hiç bir client isteği yoksa accept fonksiyonu blokeye yol açmaktadır. Eğer accept çağrıldığında zaten bağlantı kuyruğunda bağlantı için bekleyen client varsa accept bloke olmadan bağlantıyı gerçekleştirir ve geri döner. accept fonksiyonu ile elde edilen client bilgilerindeki IP adresini ve port numaraları "big endian" formatında yani "network byte ordering" formatındadır. Bunları sayısal olarak görüntülemek için ntohl ve ntohs fonksiyonlarının kullanılması gerekir. Tabii izleyen paragrafta ele alacağımız gibi aslında IP adresleri genellikle "noktalı desimal format (dotted decimal format)" denilen bir format ile yazı biçiminde görüntülenmektedir. IPV4 adresini alarak noktalı desimal formata dönüştüren inet_ntoa isimli bir fonksiyon vardır. Fonksiyonun prototipi her iki sistemde de şöyledir: #include #include char *inet_ntoa(struct in_addr in); Fonksiyon parametre olarak sockaddr_in yapısının içerisindeki IP adresinin bulunduğu in_addr yapısını (adresini değil) parametre olarak alır ve noktalı desimal format yazısının adresiyle geri döner. Bu durumda accept işlemindne elde edilen client bilgileri aşağıdaki gibi ekrana yazdırabilir: printf("waiting for client...\n"); sin_len = sizeof(sin_client); if ((clientSock = accept(serverSock, (struct sockaddr *)&sin_client, &sin_len)) == SOCKET_ERROR) ExitSys("accept", WSAGetLastError()); printf("Connected %s:%d\n", inet_ntoa(sin_client.sin_addr), ntohs(sin_client.sin_port)); inet_nto fonksiyonunun tersini yapan inet_aton isimli bir fonksiyon da bulunmaktadır. Bu fonksiyon noktalı desimal formattaki ip adresini in_addr yapısınn içerisine yerleştirmektedir. Bu fonksiyon Windows sistemlerinde yoktur. Yalonızca UNIX/Linux sistemlerinde bulunmaktadır: #include int inet_aton(const char *cp, struct in_addr *inp); Fonksiyonun birinci parametresi noktalı desimal formattaki ip adresini almaktadır. İkinci paranetresi ise ip adresinin yerleştirileceği in_addr yapısının adresini almaktadır. Fonksiyon başarı durumunda 0 değerine başarısızlık durumunda ise -1 değerine geri dönmektedir. inet_ntoa ve inet_aton fonksiyonlarının inet_ntop ve inet_pton isimli versiyonları da vardır. Aslında artık inet_ntoa ve inet_aton fonksiyonları "deprecated" yapılmıştır. Ancak inet_ntop ve inet_pton fonksiyonlarının kullanımı biraz zordur. Biz kursumuzda "deprecated" olmasına karşın kolaylığı nedeniyle inet_ntoa ve inet_aton fonksiyonlarını kullanacağız. >>> "WSACleaanup" : Windows'ta WSACleaanup fonksiyonun prototipi ise şöyledir: #include int WSACleanup(void); Fonksiyon başarı durumunda 0 değerine, başarısızlık durumunda SOCKET_ERROR özel değerine (bu değer genellikle -1 olarak define edilmektedir) geri dönmektedir. Hatanın nedeni için WSAGetLastError fonksiyonuna başvurulmalıdır. Aşağıda Windows ve UNIX/Linux sistemlerinde geldiğimiz noktaya kadar server programların kodları bir bütün olarak verilmiştir. * Örnek 1, /* Windows ""server.c" */ #include #include #include #include #define PORT_NO 55555 void ExitSys(LPCSTR lpszMsg, DWORD dwLastErr); int main(void) { WSADATA wsaData; int result; SOCKET serverSock, clientSock; struct sockaddr_in sinServer, sinClient; int sin_len; if ((result = WSAStartup(MAKEWORD(2, 2), &wsaData)) != 0) ExitSys("WSAStartup", result); if ((serverSock = socket(AF_INET, SOCK_STREAM, 0)) == SOCKET_ERROR) ExitSys("socket", WSAGetLastError()); sinServer.sin_family = AF_INET; sinServer.sin_port = htons(PORT_NO); sinServer.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(serverSock, (struct sockaddr *)&sinServer, sizeof(sinServer)) == SOCKET_ERROR) ExitSys("bind", WSAGetLastError()); if (listen(serverSock, 8) == SOCKET_ERROR) ExitSys("listen", WSAGetLastError()); printf("waiting for client...\n"); sin_len = sizeof(sinClient); if ((clientSock = accept(serverSock, (struct sockaddr *)&sinClient, &sin_len)) == SOCKET_ERROR) ExitSys("accept", WSAGetLastError()); printf("Connected %s:%d\n", inet_ntoa(sinClient.sin_addr), ntohs(sinClient.sin_port)); WSACleanup(); printf("Ok\n"); return 0; } void ExitSys(LPCSTR lpszMsg, DWORD dwLastErr) { LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } /* UNIX/Linux "server.c" */ #include #include #include #include #define PORT_NO 55555 void exit_sys(const char* msg); int main(void) { int server_sock, client_sock; struct sockaddr_in sin_server, sin_client; socklen_t sin_len; if ((server_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); sin_server.sin_family = AF_INET; sin_server.sin_port = htons(PORT_NO); sin_server.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(server_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == -1) exit_sys("bind"); if (listen(server_sock, 8) == -1) exit_sys("listen"); printf("waiting for client..\n"); sin_len = sizeof(sin_client); if ((client_sock = accept(server_sock, (struct sockaddr *)&sin_client, &sin_len)) == -1) exit_sys("accept"); printf("Client connected %s:%d\n", inet_ntoa(sin_client.sin_addr), ntohs(sin_client.sin_port)); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } >> "TCP Client" Program : TCP client program, server programa bağlanabilmek için tipik bazı adımları uygulamak zorundadır. Bu adımlar sırasında çağrılacak fonksiyonlar şunlardır: (Windows'ta WSAStartup) ---> socket ---> bind (isteğe bağlı) ---> gethostbyname (isteğe bağlı) ---> connect ---> send/recv (ya da read/write) ---> shutdown ---> close (Windows'ta closesocket) (---> Windows'ta WSACleanup) Şimdi de bu fonksiyonları sırasıyla inceleyelim: >>> Client taraf önce yine socket fonksiyonuyla bir soket yaratır. Soketin bind edilmesi gerekmez. Zaten genellikle client taraf soketi bind etmez. Eğer client taraf belli bir port'tan bağlanmak istiyorsa bu durumda bind işlemini uygulayabilir. Eğer client bind işlemi yapmazsa zaten işletim sistemi connect işlemi sırasında sokete boş bir port numarasını atamaktadır. İşletim sisteminin bind edilmemiş client programa connect işlemi sırasında atadığı bu port numarasına İngilizce "ephemeral port (ömrü kısa olan port)" denilmektedir. Seyrek olarak bazı server programlar client için belli bir remote port numarası talep edebilmektedir. Bu durumda client'ın bu remote port'a sahip olabilmesi için bind işlemini uygulaması gerekir. Client bağlantı için server'ın IP adresini ve port numarasını bilmek zorundadır. IP adreslerinin akılda tutulması zordur. Bu nedenle IP adresleri ile eşleşen "host isimleri" oluşturulmuştur. Ancak IP protokol ailesi host isimleriyle değil, IP numaralarıyla çalışmaktadır. İşte host isimleriyle IP numaralarını eşleştiren ismine DNS (Domain Name Server) denilen özel server'lar bulunmaktadır. Bu server'lar IP protokol ailesindeki DNS isimli bir protokol ile çalışmaktadır. Dolayısıyla client programın elinde IP adresi yerine host ismi varsa DNS işlemi yaparak o host ismine karşı gelen IP numarasını elde etmesi gerekir. >>>> "DNS Servers" : DNS server'lar dağıtık biçimde bulunmaktadır. Bir kayıt bir DNS server'da yoksa başka bir DNS server'a referans edilmektedir. DNS server'larda host isimleriyle IP numaraları bire bir karşılık gelmemektedir. Belli bir host ismine birden fazla IP numarası eşleştirilmiş olabileceği gibi belli bir IP numarasına da birden fazla host ismi eşleştirilmiş olabilmektedir. DNS işlemleri yapan iki geleneksel fonksiyon vardır: gethostbyname ve gethostbyaddr. Bu fonksiyonların kullanımları kolaydır. Ancak bu fonksiyonlar artık "deprecated" yapılmış ve POSIX standartlarından da silinmiştir. Bunların yerine getnameinfo ve getaddrinfo fonksiyonları oluşturulmuştur. Bu fonksiyonlar POSIX standartlarında bulunmaktadır. Biz kursumuzda artık "deprecated" hale getirilmiş "gethostbyname" ve "gethostbyaddress" kullanmayacağız. Bunun yerine "getaddrinfo" fonksiyonunu açıklayıp onu kullanacağız. >>> "getaddrinfo" : getaddrinfo isimli fonksiyon inet_addr ve gethosybyname fonksiyonlarının IPv6'yı da içerecek biçimde genişletilmiş bir biçimidir. Yani getaddrinfo hem noktalı desimal formatı nümerik adrese dönüştürür hem de eğer geçersiz bir noktalı desimal format söz konusuysa (bu durumda server isimsel olarak girilmiş olabilir) DNS işlemi yaparak ilgili host'un IP adresini elde eder. Maalesef fonksiyon biraz karışık tasarlanmıştır. Fonksiyonun Windows sistemlerindeki prototipi şöyledir: #include INT getaddrinfo( PCSTR pNodeName, PCSTR pServiceName, const ADDRINFOA *pHints, PADDRINFOA *ppResult ); UNIX/Linux sistemlerindeki prototipi ise şöyledir: #include int getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res); Aslında her iki sistemde de typedef isimleri farklı olmasına karşın fonksiyon aynı biçimde kullanılmaktadır. Fonksiyonun birinci parametresi "noktalı desimal formatlı IP adresi" ya da "host ismini" belirtmektedir. İkinci parametre NULL geçilebilir ya da buraya port numarası girilebilir. Ancak bu parametreye port numarası girilecekse yazısal biçimde girilmelidir. Fonksiyon bu port numarasını htons yaparak "big endian" formata dönüştürüp bize verecektir. Bu parametreye aynı zamanda IP ailesinin uygulama katmanına ilişkin spesifik bir protokolün ismi de girilebilmektedir (Örneğin "http" gibi, "ftp" gibi). Bu durumda bu protokollerin port numaraları bilindiği için sanki o port numaraları girilmiş gibi işlem yapılır. Eğer bu parametreye NULL girilirse bize port olarak 0 verilecektir. Port numarasını biz yerleştiriyorsak bu parametreye NULL girebiliriz. Fonksiyonun üçüncü parametresi nasıl bir adres istediğimizi anlatan filtreleme seçeneklerini belirtir. Bu parametre addrinfo isimli bir yapı türündendir. Bu yapının yalnızca ilk dört elemanı programcı tarafından girilebilmektedir. Ancak POSIX standartları bu yapının elemanlarının sıfırlanmasını öngörmektedir (buradaki sıfırlanmak terimi normal türdeki elemanlar için 0 değerini, göstericiler için NULL adres değerini belirtmektedir). addrinfo yapısı şöyledir: struct addrinfo { int ai_flags; int ai_family; int ai_socktype; int ai_protocol; socklen_t ai_addrlen; struct sockaddr *ai_addr; char *ai_canonname; struct addrinfo *ai_next; }; Yapının ai_flags elemanı pek çok bayrak değeri alabilmektedir. Bu değer 0 olarak da geçilebilir. Yapının ai_family elemanı AF_INET girilirse host'a ilişkin IPv4 adresleri, AF_INET6 girilirse host'a ilişkin IPv6 adresleri, AF_UNSPEC girilirse hem IPv4 hem de IPv6 adresleri elde edilir. Yapının ai_socktype elemanı 0 girilebilir ya da SOCK_STREAM veya SOCK_DGRAM girilebilir. Fonksiyonun ayrıntılı açıklaması için dokümanlara başvurunuz. Bu parametre NULL adres de girilebilir. Bu durumda ilgili host'a ilişkin tüm adresler elde edilir. getaddrinfo fonksiyonunun son parametresine bir bağlı listenin ilk elemanını gösteren adres yerleştirilmektedir. Buradaki bağlı listenin bağ elemanı addrinfo yapısının ai_next elemanıdır. Bu bağlı listenin boşaltımı freeaddrinfo fonksiyonu tarafından yapılmaktadır. getaddrinfo fonksiyonu başarı durumunda 0 değerine, başarısızlık durumunda doğrudan hata koduna geri döner. Bu hata kodları Windows şistemlerinde WSAGetLastError fonksiyonuyla elde ettiğimiz hata kodlarıdır. Ancak UNIX/Linux sistemlerinde bu hata kodları errno kodları değildir. Bu hata kodlarının UNIX/Linux sistemlerinde gai_strerror fonksiyonuyla yazıya dönüştürülmesi gerekir. Bağlı listenin düğümlerini free hale getirmek için freeaddrinfo fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir: #include void freeaddrinfo(struct addrinfo *ai); Fonksiyon getaddrinfo fonksiyonunun verdiği bağlı listenin ilk düğümünün (head pointer) adresini parametre olarak alır ve tüm bağlı listeyi boşaltır. gai_strerror fonksiyonunun prototipi de şöyledir: #include const char *gai_strerror(int ecode); getaddrinfo fonksiyonunun client programda tipik kullanımı aşağıda verilmiştir. Bu fonksiyon bize connect için gereken sockaddr_in ya da sockadd_in6 yapı nesnelerini kendisi oluşturup sockaddr türünden bir adres gibi vermektedir. Örneğin biz "microsoft.com" host isminin bütün IPV4 AIP adreslerini aşağıdaki gibi elde edebiliriz: struct addrinfo *ainfoHead, *ainfo; struct sockaddr_in *sinHost; struct addrinfo hints = {0, AF_INET, SOCK_STREAM}; ... if ((result = getaddrinfo(HOST_NAME, "55555", &hints, &ainfoHead)) != 0) ExitSys("bind", result); for (ainfo = ainfoHead; ainfo != NULL; ainfo = ainfo->ai_next) { sinHost = (struct sockaddr_in *)ainfo->ai_addr; printf("%s\n", inet_ntoa(sinHost->sin_addr)); } freeaddrinfo(ainfoHead); Programcı host isminden elde ettiği tüm IP numaralarını bağlantı için deneyebilir. getaddrinfo fonksiyonunun tersini yapan getnameinfo isminde bir fonksiyon da sonraları soket kütüphanesine eklenmiştir. Fonksiyon temel olarak ilgili host'un IP adresini alıp bize aynı biçimde host isimlerini vermektedir. Biz burada bu fonksiyonu açıklamayacağız. Eğer elimizde zaten server'ın IP adresi noktalı desimal formatta varsa biz getaddrinfo fonksiyonunu kullanmak yerine inet_addr ile de bu noktalı desimal formatı IP adresine dönüştürebiliriz. Ancak genel olarak getaadrinfo fonksiyonu daha genel ve daha yeteneklidir. Bu fonksiyonun kullanılması tavsiye edilebilir. inet_addr fonksiyonunun Windows sistemlerindeki prototipi şöyledir: #include unsigned long inet_addr(const char *cp); UNIX/Linux sistemlerindeki prototipi ise şöyledir: #include in_addr_t inet_addr(const char *cp); Fonksiyonlar noktasıl desimal formattaki IP adresini 4 byte'lık IPV4 adresine dönüştürmektedir. Fonksiyon başarısızlık durumunda Windows sistemlerinde INADDR_NONE değerine, UNIX/Linux sistemlrinde ise -1 değerine geri dönmektedir. >> "connect" : Artık client program connect fonksiyonuyla TCP bağlantısını sağlayabilir. connect fonksiyonunun Windows sistemlerindeki prototipi şöyledir: #include int WSAAPI connect( SOCKET s, const sockaddr *name, int namelen ); UNIX/Linux sistemlerindeki prototipi ise şöyledir: #include int connect(int socket, const struct sockaddr *address, socklen_t address_len); Fonksiyonun birinci parametresi soket betimleyicisini belirtir. İkinci parametre bağlanılacak server'a ilişkin sockaddr_in yapı nesnesinin adresini belirtmektedir. Fonksiyonun üçüncü parametresi, ikinci parametredeki yapının uzunluğunu almaktadır. Fonksiyon başarı durumunda sıfır değerine, başarısızlık durumunda -1 değerine geri dönmektedir. Eğer connect fonksiyonu çağrıldığında server program çalışmıyorsa ya da server programın bağlantı kuyruğu doluysa connect belli bir zaman aşımı süresi kadar bekler ve sonra başarısız olur ve errno değeri ECONNREFUSED ("Connection refused") ile set edilir. Örneğin: if (connect(client_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == -1) exit_sys("connect"); Aşağıda örnektlerde bağlantı için gereken minimum client program verilmiştir. Burada henüz görmediğimiz işlemleri hiç uygulamadık. Client programda bind işlemini yorum satırı içerisine aldık. Yukarıda da belirttiğimiz gibi eğer client program bind işlemi yapmazsa (ki genellikle yapmaz) bu durumda işletim sistemi client program için o programın çalıştığı makinede boş bir port numarası atamaktadır. Ayrıca biz bu örneklerde inet_addr fonksiyonunun nasıl kullanılacağını da yorum satırları içerisinde gösterdik. * Örnek 1, /* Windows "client.c" */ #include #include #include #include #include #define PORT_NO "55555" #define HOST_NAME "127.0.0.1" void ExitSys(LPCSTR lpszMsg, DWORD dwLastErr); int main(void) { WSADATA wsaData; int result; SOCKET clientSock; struct addrinfo *ainfoHead, *ainfo; struct addrinfo hints = {0, AF_INET, SOCK_STREAM}; if ((result = WSAStartup(MAKEWORD(2, 2), &wsaData)) != 0) ExitSys("WSAStartup", result); if ((clientSock = socket(AF_INET, SOCK_STREAM, 0)) == SOCKET_ERROR) ExitSys("socket", WSAGetLastError()); /* { #define CLIENT_PORT_NO 50000 struct sockaddr_in sinClient; sinClient.sin_family = AF_INET; sinClient.sin_port = htons(CLIENT_PORT_NO); sinClient.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(clientSock, (struct sockaddr *)&sinClient, sizeof(sinClient)) == SOCKET_ERROR) ExitSys("bind", WSAGetLastError()); } */ if ((result = getaddrinfo(HOST_NAME, PORT_NO, &hints, &ainfoHead)) != 0) ExitSys("getaddrinfo", result); for (ainfo = ainfoHead; ainfo != NULL; ainfo = ainfo->ai_next) if (connect(clientSock, ainfo->ai_addr, sizeof(struct sockaddr_in)) == 0) break; if (ainfo == NULL) ExitSys("connect", WSAGetLastError()); freeaddrinfo(ainfoHead); /* { struct sockaddr_in sinServer; sinServer.sin_family = AF_INET; sinServer.sin_port = htons(55555); if ((sinServer.sin_addr.s_addr = inet_addr(HOST_NAME)) == INADDR_NONE) ExitSys("inet_addr", WSAGetLastError()); if (connect(clientSock, (struct sockaddr *)&sinServer, sizeof(sinServer)) == SOCKET_ERROR) ExitSys("connect", WSAGetLastError()); } */ printf("connected...\n"); WSACleanup(); return 0; } void ExitSys(LPCSTR lpszMsg, DWORD dwLastErr) { LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } /* Unix/Linux "client.c" */ #include #include #include #include #include #define PORT_NO "55555" #define HOST_NAME "127.0.0.1" void exit_sys(const char* msg); int main(void) { int client_sock; struct addrinfo *ainfo_head, *ainfo; struct addrinfo hints = {0, AF_INET, SOCK_STREAM}; int result; if ((client_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) exit_sys("socket"); /* { #define CLIENT_PORT_NO 50000 struct sockaddr_in sin_client; sin_client.sin_family = AF_INET; sin_client.sin_port = htons(CLIENT_PORT_NO); sin_client.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(client_sock, (struct sockaddr *)&sin_client, sizeof(sin_client)) == -1) exit_sys("bind"); } */ if ((result = getaddrinfo(HOST_NAME, PORT_NO, &hints, &ainfo_head)) != 0) exit_sys("getaddrinfo"); for (ainfo = ainfo_head; ainfo != NULL; ainfo = ainfo->ai_next) if (connect(client_sock, ainfo->ai_addr, sizeof(struct sockaddr_in)) == 0) break; if (ainfo == NULL) exit_sys("connect"); freeaddrinfo(ainfo_head); /* { struct sockaddr_in sin_server; sin_server.sin_family = AF_INET; sin_server.sin_port = htons(55555); if ((sin_server.sin_addr.s_addr = inet_addr(HOST_NAME)) == -1) exit_sys("inet_addr"); if (connect(client_sock, (struct sockaddr *)&sin_server, sizeof(sin_server)) == -1) exit_sys("connect"); } */ printf("connected...\n"); return 0; } void exit_sys(const char* msg) { perror(msg); exit(EXIT_FAILURE); } Bağlantı sağlandıktan sonra artık gönderme ve alma işlemleri yapılabilir. Soketten karşı tarafa byte göndermek için send karşı taraftan byte okumak için recv fonksiyonları kullanılmaktadır. UNIX/Linux sistemlerinde soketler de dosya gibi betimleyici oldukları için bu sistemlerde send yerine write, recv yerine read POSIX fonksiyonları da kullanılabilir. TCP full duplex bir haberleşme sunmaktadır. Yani client ve server programlar aynı soket ile hem bilgi gönderip hem de bilgi alabilmektedir. recv fonksiyonunun Windows sistemlerindeki prototipi şöyledir: #include int recv( SOCKET s, char *buf, int len, int flags ); UNIX/linux sistemlerindeki prototipi ise şöyledir: #include ssize_t recv(int socket, void *buffer, size_t length, int flags); Fonksiyonların birinci parametresi aktif soketin betimleyicisini belirtmektedir. İkinci parametre alınacak bilginin yerleştirileceği dizinin adresini almaktadır. Üçüncü parametre ise okunmak istenen byte sayısını belirtmektedir. Fonksiyonun son parametresi aşağıdaki üç sembolik sabitin bit OR işlemine sokulmasıyla oluşturulabilir: MSG_PEEK MSG_OOB MSG_WAITALL Biz şimdilik bu değerlerin anlamlarını açıklamayacağız. Ancak MSG_PEEK değeri bilginin network tamponundan alındıktan sonra oradan atılmayacağını belirtmektedir. Bu parametre 0 da geçilebilir. Zaten recv fonksiyonunun read fonksiyonundan tek farkı bu son parametredir. Bu son parametrenin 0 geçilmesiyle read kullanılması arasında hiçbir farklılık yoktur. recv fonksiyonu blokeli modda (default durum blokeli moddur) tıpkı borularda olduğu gibi eğer hazırda en az 1 byte varsa okuyabildiği kadar bilgiyi okur ve okuyabildiği byte sayısına geri döner. Eğer o anda network tamponunda hiç byte yoksa recv fonksiyonu en az 1 byte okuyana kadar blokede bekler. (Yani başka bir deyişle recv tıpkı borularda olduğu gibi eğer okunacak bir şey yoksa blokede bekler, ancak okunacak en az 1 byte varsa okuyabildiğini okur ve beklemeden geri döner.) recv fonksiyonu başarı durumunda okunabilen byte sayısına, başarısızlık durumunda Windows sistemlerinde SOCKET_ERROR, UNIX/Linux sistemlerinde -1 değerine geri dönmektedir. Eğer karşı taraf soketi (peer socket) kapatmışsa bu durumda tıpkı borularda olduğu gibi recv fonksiyonu 0 ile geri dönmektedir. Soketlerle boruların kullanımlarının birbirlerine çok benzediğine dikkat ediniz. Soketten bilgi göndermek için send ya da UNIX/Linux sistemleribde write fonksiyonu kullanılmaktadır. send fonksiyonunun Windows sistemlerindeki prototipi şöyledir: #include int send( SOCKET s, const char *buf, int len, int flags ); UNIX/Linux sistemlerindeki prototipi ise şöyledir: #include ssize_t send(int socket, const void *buffer, size_t length, int flags); Fonksiyoların birinci parametresi aktif soketin betimleyicisini belirtmektedir. İkinci parametre gönderilecek bilgilerin bulunduğu dizinin adresini belirtir. Üçüncü parametre ise gönderilecek byte miktarını belirtmektedir. Son parametre aşağıdaki sembolik sabitlerin bit düzeyinde OR işlemine sokulmasıyla oluşturulabilir: MSG_EOR MSG_OOB MSG_NOSIGNAL Bu parametre 0 da geçilebilir. Biz şimdilik bu bayraklar üzerinde durmayacağız. send fonksiyonu bilgileri karşı tarafa o anda göndermez. Onu önce network tamponuna yerleştirir. İşletim sistemi o tampondan TCP (dolayısıyla IP) paketleri oluşturarak mesajı göndermektedir. Yani send fonksiyonu geri döndüğünde bilgiler network tamponuna yazılmıştır, ancak henüz karşı tarafa gönderilmemiş olabilir. Pekiyi o anda network tamponu doluysa ne olacaktır? İşte UNIX/Linux sistemlerinde send fonksiyonu, gönderilecek bilginin tamamı network tamponuna aktarılana kadar blokede beklemektedir. Ancak bu konuda işletim sistemleri arasında farklılıklar olabilmektedir. Örneğin Windows sistemlerinde send fonksiyonu eğer network tamponununda gönderilmek istenen kadar yer yoksa ancak en az bir byte'lık boş bir yer varsa tampona yazabildiği kadar byte'ı yazıp hemen geri dönmektedir. Diğer UNIX/Linux sistemleri arasında da send fonksiyonunun davranışı bakımından bu yönde farklılıklar olabilmektedir. Ancak POSIX standartları blokeli modda tüm bilginin network tamponuna yazılana kadar send fonksiyonunun bloke olacağını belirtmektedir. Linux çekirdeği de buna uygun biçimde çalışmaktadır. send fonksiyonu network tamponuna yazılan byte sayısı ile geri dönmektedir. Blokeli modda bu değer UNIX/Linux sistemlerinde yazılmak istenen değerle aynı olur. Ancak Windows sistemlerinde daha az olabilmektedir. send fonksiyonu Windows'ta başarısızlık durumunda SOCKET_ERROR değeri ile UNIX/Linux sistemlerinde ise -1 değeri ile geri döner.Tıpkı borularda olduğu gibi UNIX/Linux sistemlerinde send fonksiyonunda da eğer karşı taraf soketi kapatmışsa send fonksiyonu default durumda SIGPIPE sinyalinin oluşmasına yol açmaktadır. Eğer bu sinyalin oluşturulması istenmiyorsa bu durumda send fonksiyonunun son parametresi (flags) MSG_NOSIGNAL olarak geçilmelidir. Bu durumda karşı taraf soketi kapatmışsa send fonksiyonu başarısız olur ve errno değeri EPIPE olarak set edilir. send fonksiyonunun soketlerdeki davranışının borulardaki davranışa çok benzediğine dikkat ediniz. send fonksiyonunun son parametresi 0 geçildiğinde bu fonksiyonun davranışı tamamen write fonksiyonunda olduğu gibidir. Şimdi de örnekler üzerinden ilerleyelim: * Örnek 1.0, Aşağıdaki iskelet bir TCP/IP client-server uygulama verilmiştir. Burada client klavyeden birtakım yazılar girer. Bunu server'a gönderir. Server da bu yazının tersini client'a geri yollamaktadır. Client "exit" yazdığında her iki taraf da işlemini sonlandırmaktadır. /* Server.c */ #include #include #include #define SERVER_PORTNO 55000 #define BUFFER_SIZE 1024 void ExitSys(LPCSTR lpszMsg, DWORD dwLastErr); int main(void) { WSADATA wsaData; DWORD dwResult; SOCKET serverSock, clientSock; struct sockaddr_in sinServer, sinClient; int addrLen; char buf[BUFFER_SIZE]; char *str; int result; if ((dwResult = WSAStartup(MAKEWORD(2, 2), &wsaData)) != 0) ExitSys("WSAStartup", dwResult); if ((serverSock = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET) /* third parameter IPPROTO_TCP */ ExitSys("socket", WSAGetLastError()); sinServer.sin_family = AF_INET; sinServer.sin_port = htons(SERVER_PORTNO); sinServer.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(serverSock, (struct sockaddr *)&sinServer, sizeof(sinServer)) == SOCKET_ERROR) ExitSys("bind", WSAGetLastError()); if (listen(serverSock, 8) == SOCKET_ERROR) ExitSys("listen", WSAGetLastError()); printf("Waiting for client connection....\n"); addrLen = sizeof(sinClient); if ((clientSock = accept(serverSock, (struct sockaddr *)&sinClient, &addrLen)) == INVALID_SOCKET) ExitSys("accept", WSAGetLastError()); printf("Connected: %s:%d\n", inet_ntoa(sinClient.sin_addr), ntohs(sinClient.sin_port)); for (;;) { if ((result = recv(clientSock, buf, BUFFER_SIZE - 1, 0)) == SOCKET_ERROR) ExitSys("recv", WSAGetLastError()); if (result == 0) break; buf[result] = '\0'; if (!strcmp(buf, "exit")) break; printf("%d bytes received: %s\n", result, buf); _strrev(buf); if (send(clientSock, buf, strlen(buf), 0) == SOCKET_ERROR) ExitSys("send", WSAGetLastError()); } shutdown(clientSock, SD_BOTH); closesocket(clientSock); closesocket(serverSock); WSACleanup(); return 0; } void ExitSys(LPCSTR lpszMsg, DWORD dwLastErr) { LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } /* Client.c */ #include #include #include #include #define SERVER_PORTNO 55000 #define CLIENT_PORTNO 62000 #define BUFFER_SIZE 1024 #define SERVER_NAME "127.0.0.1" void ExitSys(LPCSTR lpszMsg, DWORD dwLastErr); int main(void) { WSADATA wsaData; DWORD dwResult; SOCKET clientSock; struct sockaddr_in sinServer; struct hostent *hostEnt; char buf[BUFFER_SIZE]; char *str; int result; if ((dwResult = WSAStartup(MAKEWORD(2, 2), &wsaData)) != 0) ExitSys("WSAStartup", dwResult); if ((clientSock = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET) ExitSys("socket", WSAGetLastError()); /* { struct sockaddr_in sinClient; sinClient.sin_family = AF_INET; sinClient.sin_port = htons(CLIENT_PORTNO); sinClient.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(clientSock, (struct sockaddr *)&sinClient, sizeof(sinClient)) == SOCKET_ERROR) ExitSys("bind", WSAGetLastError()); } */ sinServer.sin_family = AF_INET; sinServer.sin_port = htons(SERVER_PORTNO); sinServer.sin_addr.s_addr = inet_addr(SERVER_NAME); if (sinServer.sin_addr.s_addr == INADDR_NONE) { if ((hostEnt = gethostbyname(SERVER_NAME)) == NULL) ExitSys("gethostbyname", WSAGetLastError()); memcpy(&sinServer.sin_addr.s_addr, hostEnt->h_addr_list[0], hostEnt->h_length); } if (connect(clientSock, (struct sockaddr *)&sinServer, sizeof(sinServer)) == SOCKET_ERROR) ExitSys("connect", WSAGetLastError()); printf("Connected...\n"); for (;;) { printf("Text:"); fgets(buf, BUFFER_SIZE, stdin); if ((str = strchr(buf, '\n')) != NULL) *str = '\0'; if (send(clientSock, buf, strlen(buf), 0) == SOCKET_ERROR) ExitSys("send", WSAGetLastError()); if (!strcmp(buf, "exit")) break; if ((result = recv(clientSock, buf, BUFFER_SIZE - 1, 0)) == SOCKET_ERROR) ExitSys("recv", WSAGetLastError()); if (result == 0) break; buf[result] = '\0'; printf("%d bytes received: %s\n", result, buf); } shutdown(clientSock, SD_BOTH); closesocket(clientSock); WSACleanup(); return 0; } void ExitSys(LPCSTR lpszMsg, DWORD dwLastErr) { LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); }