yorumluyor ve size dosyayı ftp protokolune uygun paketler halinde gönderiyor. Artık siz okumaya
başlıyorsunuz. Paketleri alıp birleştirip yerel diske yazıyorsunuz. Bu aşamadan sonra istediğinizi
yaptırabilirsiniz.
istemci.c
Aşağıdaki bir SMTP sunucuya bağlanıp smtp komutları gönderir ve cevaplarını alır. Tam olarak bir
SMTP istemci değildir. SMTP protokolunun detaylari için RFC 821'e bakabilirsiniz.
#include
#include
#include
#include
#include
#include
#include
#define SMTP_PORT 25
int main(int argc, char *argv[])
{
struct hostent *h_name;
int sd;
char cmd[512];
struct sockaddr_in serv_addr;
printf("checkd - SMTP sunucuyu kontrol eder.\n");
printf("http://www.acikkod.org/\n\n");
if(argc < 2) {
printf("Kullanımı: %s hostname\n",argv[0]);
exit(1);
} else {
h_name = gethostbyname(argv[1]);
}
bzero((char *) &serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = *(u_long *) h_name->h_addr;
serv_addr.sin_port = htons(SMTP_PORT);
printf("> Soket açılıyor... ");
if( (sd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
printf("hata!\n");
perror("socket");
exit(1);
} else printf("tamam\n");
printf("> %s bağlanılıyor... ",inet_ntoa(serv_addr.sin_addr));
if(connect(sd,(struct sockaddr*)&serv_addr,sizeof(serv_addr)) < 0) {
printf("hata!\n");
perror("connect");
exit(1);
} else printf("tamam\n");
printf("> Karşılama mesajı alınıyor... ");
memset(cmd, 0, 256);
if((recv(sd, cmd, 256, 0)) <= 0) {
printf("hata!\n");
perror("recv");
exit(1);
} else printf("tamam\n");
printf(" %s\n", cmd);
printf("> Selam veriliyor... ");
memset(cmd, 0, 256);
strcpy(cmd, "helo world\n");
if((send(sd, cmd, 256, 0)) <= 0) {
printf("hata!\n");
perror("send");
exit(1);
} else printf("tamam\n");
printf("> Cevap alınıyor... ");
memset(cmd, 0, 256);
if((recv(sd, cmd, 256, 0)) <= 0) {
printf("hata!\n");
perror("recv");
exit(1);
} else printf("tamam\n");
printf(" %s\n\n", cmd);
printf("SMTP sunucunuz çalışıyor.\n\n");
return 0;
}
6. Gelişmiş G/Ç
6.1. Bloksuz G/Ç
Giriş/Çıkış (I/O); bir dosyaya, pipe'a, terminale veya ağ aygıtına yazmak veya bu aygıtlardan
okumak gibi işlemleri içermektedir.
Okunacak veri hazır değilse veya yazılacak veri o an kabul edilmiyorsa bu işlemleri yapan süreçler
bloklanacaktır. Bloksuz G/Ç (Nonblocking I/O), verinin veya aygıtın hazır olmaması gibi
durumlarda bloklanmayı tamamen ortadan kaldıran bir özelliktir. Bu konunun detayları dökümanın
kapsamı dışındadır. (man 2 fcntl)
6.2. select()
Soket program aynı anda birden fazla soketten veri okumak veya yazmak durumunda kalabilir.
Bunu tek soket tanımlayıcı ile sağlayamazsınız. Çünkü soketiniz blok durumuna geçtiğinde (örneğin
accept(), bağlantı gelene kadar programın blok olmasına neden olur) kodunuzun geri kalan kısmı
çalışmaz, bloktan çıkmayı bekler.
Şöyle bir senaryoyu hayal edelim. İki adet soket tanımlayıcı var ve bu ikisi ile karşı uçtan dosya veri
alacaksınız. 1. uç henüz veriyi hazırlamadı ancak 2. uç hazırladı varsayalım. Eğer program 1. soket
tanımlayıcı ile ilk önce 1. uçtan veri çekmeye çalışırsa blok olacaktır. 2. ucun verisi hazır olduğu
halde veri alınamayacaktır. Oysa select() ile bu iki uç kontrol edilip hazır olan uçtan -senaryomuzda
2. uç- veri okunsa idi program blok olmayacak ve verisi hazır olan işlerini yapmaya devam edecekti.
Bu şekilde bloklanma riski taşımayan bir program yazılabilir.
Tek bir süreç içerisinde bloksuz G/Ç kullanarak bu sorun çözülebilir. Bütün dosya tanımlayıcılar
bloksuz G/Ç yapacak şekilde set edilir. Eğer veri hazır değilse read() hemen sonlanır. Aynı işlemi
ikinci dosya tanımlayıcısı için de yaparız. Belli bir süre sonra tekrar ilk tanımlayıcıyı kontrol ederiz.
Buna 'polling' deniliyor. Bunun dezavantajı, gereksiz yere CPU zamanı harcamaktadır.
Aynı problemi çözmek için kullanılabilecek bir diğer teknik de "asenkron G/Ç". Bu yöntemde,
dosya tanımlayıcımız G/Ç için hazır olduğunda çekirdek, G/Ç yapacak süreci haberdar edecektir.
Ancak burada standart problemleri ve sinyalleri işleme (signal handling) ile ilgili problemler vardır.
Bunlardan daha iyi bir teknik ise G/Ç çoğullama (I/IO multiplexing) olarak adlandırılmaktadır.
İlgilendiğimiz tanımlayıcıların eklendiği bir küme vardır ve tanımlayıcılardan biri G/Ç için hazır
olmadıkça çıkmayan bir sistem çağrısı yapılır. Sistem çağrısından çıkıldığında hangi
tanımlayıcıların G/Ç için hazır olduğu sorulabilir.
select birden fazla soket tanımlayıcının durumunu takip eden ve BSD4.2 ile gelen bir sistem
çağrısıdır. Burada şunu belirtmeliyim ki, select() yalnızca soketler için değil genel olarak dosya ve
G/Ç işlemleri için kullanılan bir sistem çağrısıdır.
#include
#include
#include
int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct
timeval *timeout);
n: En büyük soket tanımlayıcının bir fazlası (tanımlayıcılar 0'dan başladığından)
readfds: Okumak için izlenecek (hazır olup olmadığı bakılacak) soket tanımlayıcı
writefds: Yazmak için izlenecek soket tanımlayıcı
exceptfds: İstisnai durumlar için izlenecek soket tanımlayıcı
timeval, aşağıdaki şekilde bir veri yapısıdır:
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
timeout eğer NULL olarak verilirse bir dosya hazır olana kadar blokda bekler. Hazır olmazsa
sonsuza kadar bekleyecektir. Tabi sinyal(signal) gonderilirse sonlanır. Eğer sinyal ile sonlandırılırsa
select() -1 döndürecektir ve errno=EINTR olacaktır.
'0' a setlenirse hiç beklemez. Bütün belirtilen tanımlayıcılar test edilir ve çağrıdan hemen çıkılır. Bu,
select içinde blocklanmadan birden fazla tanımlayıcıyı test etmek için kullanılır.
0'dan büyük bir değere setlenirse o değer kadar bekler. Belirtilen tanımlayıcılardan biri hazır
olduğunda veya zamanaşımına uğradığında sistem çağrısı return yapar.
Gözetlenecek tanımlayıcılar tanımlayıcı setine (descriptor set) eklenir. Tanimlayıcı seti, fd_set veri
yapısı şeklinde kayıt edilmiş veridir. fd_set, her tanımlayıcı için bir bit tutar ve geçerli boyutu 1024
bittir (sys/types.h içinde tanımlı). Set üzerinde işlem yapmak için bazı makrolar tanımlanmıştır:
fd_set aşağıdaki gibi tanımlanır:
fd_set dset;
Seti sıfırlamak için:
FD_ZERO(&dset);
Takip etmek istediğimiz tanımlayıcıları eklemek için:
FD_SET(fd, &dset);
select() seti değiştirmektedir. select sistem çağrısından sonra bir tanımlayıcının hala sette olup
olmadığını test etmek için:
if (FD_ISSET(fd, &dset)) {
...
}
Bir tanımlayıcıyı setten çıkartmak için:
FD_CLR(fd, &dset);
Bu bilgiler ışığında bir uygulama geliştirebiliriz. Standart giriş (stdin), dosya tanımlayıcısı 0 olan bir
dosyadır. Eğer bir PC kullanıyorsanız klavye standart giriştir. Aşağıdaki kod, standart girişi
izlemektedir. Eğer standart giriş okumak için hazırsa (bunun anlamı klavyeden birşey yazıp enter'a
basmışsak) hazır olduğunu ekrana basacak. FD_ISSET ile de verinin hazır olduğunu göreceğiz
(Hazır olduğunda FD_ISSET, true olacaktır. Eğer hazır değilse false olacaktır.).
/*
* select.c - I/O multiplexing
*
* Baris Simsek,
* http://www.acikkod.org
* 07/07/2004
*
*/
#include
#include
#include
#include
int
main(void) {
fd_set dset;
struct timeval tv;
int ret;
FD_ZERO(&dset);
FD_SET(0, &dset); /* stdin'i gozlemeye aldik */
tv.tv_sec = 10; /* 10 sn giris icin bekle */
tv.tv_usec = 0;
ret = select(1, &dset, NULL, NULL, &tv);
if (ret == -1)
perror("select()");
else if (ret) {
printf("Standart input okumak için hazir.\n");
if(FD_ISSET(0, &dset))
printf("Standart input icin dset true\n");
}
else {
printf("10 saniye icinde standart giristen veri girilmedi.\n");
if(!FD_ISSET(0, &dset))
printf("Standart input icin dset false\n");
}
return 0;
}
7. Sık Sorulan Sorular
7.1. Kitap önerebilir misiniz?
W. Richard Stevens'ın
"UNIX Network Programming - Volume 1"
kitabı bu alanda başlıca referans
kitaptır.
7.2. Soketler Nasıl Çalışır?
Soketler (özelikle connection oriented soketler) dosyalar veya
PIPE
gibi çalışır. Pipe'dan farkı iki
yönlü olmasıdır. Dosyalardan farkı ise beklediğiniz kadar veri okuyamayabilir veya istediğiniz
kadar veri yazamayabilirsiniz.
7.3. select() veri hazır dediği halde, 0 byte neden okunur?
select() verinin hazır olduğunu söyledikten sonra karşı taraf bağlantıyı koparmıştır. Bu da read() 'in
0 döndürmesine neden olur.
7.4. Soket seçeneklerini nasıl değiştiririm?
int setsockopt(int s, int level, int optname, const void *optval, socklen_t optlen);
int flag = 1;
int result = setsockopt(sock, /* socket affected */
IPPROTO_TCP, /* seçeneği TCP seviyesinde set et. */
TCP_NODELAY, /* seçenek */
(char *) &flag,
sizeof(int)); /* seçenek değerinin büyüklüğü */
Seçenekler hakkında detay için "man 2 setsockopt"
7.5. SO_KEEPALIVE seçeneği neden kullanılır?
Oturum açmış iki bilgisayar arasında belli bir süre (RFC1122 de 2 saat olarak tanımlandı) veri
alışverişi olmazsa, karşı tarafı yoklamak için (ACK isteyerek) kullanılır. Zira karşı taraf ulaşılamaz
durumda olabilir. Bu durumu algılamak için kullanılır.
7.6. Soketi tam olarak nasıl kapatırım?
Soket kapatıltıktan sonra "netstat -na" ile bakıldığında sisteminizde TIME_WAIT'de duran soketler
hala gözükebilir. Bu normaldir. Çünkü TCP, bütün verinin karşılıklı transfer edildiğinden emin
olmak istiyor. Soket kapatıldığında her iki taraf da başka veri transferi olmayacağı konusunda
anlaşmış demektir. Böyle bir anlaşmadan sonra socket rahatlıkla kapatılabilir. Ancak burada iki
problem sözkonusu olacak. Bunlardan biri son ACK'in ulaşıp ulaşmadığı bilinemeyecek. (Bu ACK
için tekrar ACK istense bunun da ulaşıp ulaşmadığı belli olmayacak. Kısır döngü olur.). Eğer bu
ACK ağda kaybolmuş ise sunucu bunu bekliyor olacaktır. Bir diğer problem de eğer bir paket
router'in birinde herhangi bir nedenle bekliyorsa ve alıcı taraf bunu belli bir süre içerisinde
alamamışsa paketi yeniden talep edecektir. Ancak diğer paket gerçekte kaybolmamıştır ve belli bir
süre ağda yeniden ortaya çıkacaktır. Bu kaybolma ve yeniden ortaya çıkması süresi içerisinde
bağlantı koparsa ve aynı host aynı porttan yeni bir bağlantı açarsa göndereceği paketin sıra numarası
ağdaki ile üst üste binecektir. Çünkü eski oturumdan kalma bir paket yeni oturumda transfer
edilmiştir. Bundan kurtulmak için TIME_WAIT durumu ortadan kalkmadan yeni bir oturum
açmamalı.
Bütün bunlar düşünüldüğünde TIME_WAIT'in programcı için bir yardımcı olduğu anlaşılır. Ancak
TIME_WAIT olduğu sürece programınız aynı soketi yeniden bind() edemeyecektir. 7.4. anlatıldığı
şekilde SO_REUSEADDR seçeneği set edilerek bu sorunu çözebilirsiniz. Öte yandan
TIME_WAIT'te bekleyen soketler bir süre sonra (Linux'lerde bu 60 sn.dir.) close() olacaktır. sysctl
ile bu süre değiştirilebilir.
close() doğru kapatma yöntemi ise de shutdown() daha kullanışlıdır. Çünkü tek yönlü soketi kapama
olanağı da sunar.
int shutdown(int s, int how);
İkinci parametre ile kapa yönünü verebilirsiniz:
SHUT_RD: Veri alımı kesilecektir.
SHUT_WR: Veri gönderimi kapatılacaktır.
SHUT_RDWR: İki yönlü veri alışverişi durdurulacaktır. (close)
close(), o süreç için soketi kapatır ancak eğer socketi başka bir süreçle paylaşıyorsa socket hala açık
duracaktır. shutdown() bütün süreçler için soketi kapatır.
7.7. String halindeki bir adresi internet adresine nasıl çevirim?
struct in_addr *atoaddr(char *address) {
struct hostent *host;
static struct in_addr saddr;
/* Önce IP formatında deniyoruz. */
saddr.s_addr = inet_addr(address);
if (saddr.s_addr != -1) {
return &saddr;
}
/* IP formatında değilse FQDN olarak deniyoruz. */
host = gethostbyname(address);
if (host != NULL) {
return (struct in_addr *) *host->h_addr_list;
}
return NULL;
}
7.8. Soketlerde dinamik buffer kullanmanın bir yolu var mı?
Soketten okuyacağınız veri miktarı belli olmadığında böyle bir ihtiyaç doğuyor. Bu durumda malloc
() ile mümkün olan en büyük tampon belleği ayırırsınız. Okunan verinin büyüklüğüne göre tampon
bellek realloc() ile yeniden boyutlandırılır. Zaten pek çok UNIX'de malloc() fiziksel bellekten yer
ayırmaz. Sadece adres uzayını belirler. Tampona veri yazdığınızda gerçek bellek sayfaları kullanılır.
Bu nedenle büyük buffer ayırmakla gereksiz kaynak kullanımına neden olunmaz.
7.9. "address already in use" hatasını neden alırım?
Port kullanılıyordur veya sunucu bir programı henüz sonlandırdınız ve socket TIME_WAIT'dedir.
İkinci durum için 7.4. ve 7.5. sorularının çözümlerine bakınız. Birinci durum için aynı portta çalışan
diğer socket programı durdurmanız gerekmektedir.
7.10. Programımı nasıl daemon yapabilirim?
En kolay yolu inetd ile kullanmanızdır. Diğer bir yöntem ise fork() ederek isteklere cevap
vermekdir. Detay icin
http://www.enderunix.org/docs/daemontr.html
Programin temel iskeleti
asagidaki yapiya cevirilmeli:
ret = fork ();
if (ret == -1) { /* fork hata verdi */
perror ("fork()");
exit (3);
}
if (ret > 0) exit(0); /* Ana süreç çıkar */
if (ret == 0) { /* Alt süreç devam eder */
close (STDIN_FILENO);
close (STDOUT_FILENO);
close (STDERR_FILENO);
if (setsid () == -1) exit(1);
/* Alt sürece ait işler */
}
7.11. Aynı anda birden fazla soketi nasıl dinlerim?
select() kullanın. Hangi socket veri için hazır ise onun, kullanmanıza olanak sağlar.
6.2.
select()
bölümüne bakınız.
7.12. 1024 ten küçük portları neden bind edemiyorum?
Güvenlik nedenleri ile 1024'ten küçük portları yalnızca yetkili kullanıcı (root) açabilir.
9. Belge Tarihçesi
VI.sürümde "7. Sık Sorulan Sorular" bölümü eklendi. (1 Ağustos 2004)
V.sürümde Soket Türleri ve Veri Dönüşümleri başlıkları eklendi. Sarmalama(Encapsulation) tanımı
eklendi. (8 Temmuz 2004)
IV.sürümde Gelişmiş G/Ç başlığı eklendi. (28 Haziran 2004)
III.sürümde RFC'ler gözden geçirilerek yeniden düzenlenmiştir. İlk sürümlerdeki bazı yanlışlar
giderildi. (5 Haziran 2004)
II.sürüm, Linux Kullanıcıları Derneği Liste üyeleri için yeniden gözden geçirildi. (2001)
İlk sürüm: 10 Kasım 1998 (KTÜ Bilgisayar Klubü Dergisi için yazıldı.)
Hataları çekinmeden bana bildirebilirsiniz. b$
Dostları ilə paylaş: |