Biraz garip olmakla birlikte bu işlevin epey faydalı olduğu
söylenebilir. Şu durumu ele alalım: siz bir sunucu programsınız ve
bir yandan gelen bağlantıları dinlerken öte yandan da açık olan
bağlantılardan akmakta olan verileri okumak istiyorsunuz.
Sorun değil, diyorsunuz, bir accept() ve birkaç
tane de recv() işimizi görür. Ağır ol dostum!
Ya çağırdığın accept() işlevi bloklayan durumda
ise? O zaman aynı anda recv() ile nasıl oluyor
da veri okumayı düşünebiliyorsun? "O halde bloklamayan soketleri
kullanırım!" Hiç yakıştıramadım senin gibi bir programcıya! İşlemci
zamanını deliler gibi harcamak mı istiyorsun? E peki nasıl yapacağız
öyleyse?
select() aynı anda birden fazla soketi gözetleme
imkânı sunar. Bununla kalmaz aynı zamanda hangi soketin okumak için
hazır olduğunu, hangisine yazabileceğiniz, hangisinde istisnai durumlar
oluştuğunu da söyler.
Laf kalabalığını kesip hemen işleve geçiyorum,
select():
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int numfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
Bu işlev dosya tanımlayıcı kümelerini gözlemler. Özel olarak
ilgilendikleri ise readfds,
writefds ve exceptfds'dir.
Mesela standart girdiden ve sockfd gibi bir
soket tanımlayıcıdan veri okuyup okuyamayacağınızı merak ediyorsanız
tek yapmanız gereken 0 ve
sockfd'yi readfds
kümesine eklemek. numfds parametresi azami
dosya tanımlayıcı artı bir olarak ayarlanmalıdır. Bu örnekte
sockfd+1 olmalıdır. Çünkü açıktır ki yukarıdaki
değer standart girdiden (0) daha büyük olacaktır.
select() çalıştırıldıktan sonra readfds
seçmiş olduğunuz dosya tanımlayıcılardan hangisinin okumak için hazır
olduğunu yansıtacak şekilde güncellenir. Bunları aşağıdaki
FD_ISSET() makrosu ile test edebilirsiniz.
Daha fazla ilerlemeden bu kümeler ile nasıl başa çıkacağınızı anlatayım.
Her küme fd_set türündedir. Bu tür üzerinde aşağıdaki
makroları kullanabilirsiniz:
FD_ZERO(fd_set *set) -- dosya tanımlayıcı
kümesini temizler.
FD_SET(int fd, fd_set *set) -- kümeye
fd'yi ekler.
FD_CLR(int fd, fd_set *set) --
fd'yi kümeden çıkarır.
FD_ISSET(int fd, fd_set *set) --
fd'ni küme içinde olup olmadığına bakar.
Son olarak, nedir bu struct timeval? Bazen birilerinin
size veri göndermesini sonsuza dek beklemek istemezsiniz. Belki de her
96 saniyede bir ekrana "Hala çalışıyor..." mesajı basmak istersiniz
(program o esnada bir şey yapmıyor olsa bile). Bu zaman yapısı sizin bir
sonlandırma süresi ("timeout period") belirlemenizi sağlar. Eğer bu süre
geçildi ise ve select() hala hazır bir dosya
tanımlayıcı bulamadı ise o zaman işlev (select) döner ve böylece de siz
de işinize devam edebilirsiniz.
struct timeval yapısının elemanları aşağıdaki gibidir:
struct timeval {
int tv_sec; // seconds
int tv_usec; // microseconds
};
Tek yapmanız gereken tv_sec'i kaç saniye
bekleneceğine ayarlamak ve tv_usec'i de kaç
mikrosaniye bekleneceğine ayarlamak. Evet, doğru okudunuz,
mikrosaniye, milisaniye değil. Bir milisaniye içinde 1,000
mikrosaniye vardır ve bir saniye içinde de 1,000 milisaniye vardır.
Yani bir saniye içinde 1,000,000 mikrosaniye vardır. Tamam da neden
"usec"? Buradaki "u" harfinin Yunan alfabesindeki Mü harfine benzediği
düşünülmüştür ve bu yüzden "mikro"yu temsilen kullanılmıştır.
Bunlara ek olarak işlev döndüğünde timeout
ne kadar zaman kaldığını gösterecek şekilde güncellenmiş olabilir.
Bu hangi tür UNIX kullandığınıza bağlıdır.
Vay canına! Mikrosaniye hassasiyetinde bir zamanlayıcımız var! Gene de
siz buna çok güvenmeyin. Standart Unix zamandilimi yaklaşık 100
milisaniyedir, yani struct timeval yapısını nasıl
ayarlarsanız ayarlayın en az bu kadar beklemek durumunda kalabilirsiniz.
Meraklısına not: Eğer struct timeval yapısındaki
değişkeninizin alanlarınız 0 yaparsanız,
select() işlevi anında sonlanacak ve böyle kümenizin
içindeki tüm dosya tanımlayıcıları taramış olacaktır. Eğer
timeout parametresini NULL yaparsanız bu sefer
de işlev asla zaman aşımına uğramayacak ve ilk dosya tanımlayıcı hazır
olana dek bekleyecektir. Son olarak: Eğer belli bir küme için beklemek
sorun teşkil etmiyorsa o zaman select() işlevini
çağırırken onu NULL olarak ayarlayabilirsiniz.
/*
** select.c -- bir select() örneği
*/
#include <stdio.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#define STDIN 0 // standart girdi için dosya tanımlayıcı
int main(void)
{
struct timeval tv;
fd_set readfds;
tv.tv_sec = 2;
tv.tv_usec = 500000;
FD_ZERO(&readfds);
FD_SET(STDIN, &readfds);
// writefds ve exceptfds ile ilgilenmiyoruz:
select(STDIN+1, &readfds, NULL, NULL, &tv);
if (FD_ISSET(STDIN, &readfds))
printf("Bir tusa basildi!\n");
else
printf("Zaman doldu.\n");
return 0;
}
Üzerinde çalıştığınız uçbirim türüne bağlı olarak bir tuşa bastıktan
sonra bunun algılanması için ENTER tuşuna basmanız gerekebilir. Aksi
takdirde "Zaman doldu." mesajı alırsınız.
Şimdi bazılarınız bir veripaketi soketi üzerinden veri beklemek için
bu yöntemin mükemmel bir yöntem olduğunu düşünebilir -- evet haklısınız:
bu yötem gerçekten de mükemmel olabilir . Ancak bazı UNIX türevleri
select'i bu şekilde kullanabilirken bazıları
kullanamaz. Bu yüzden de pratik olarak kullanmaya başlamadan önce
sisteminizde bu konu ile ilgili man sayfalarını okuyun.
Bazı UNIX türevleri de struct timeval yapısındaki
değişkeninizi, zamanaşımına ne kadar süre kaldığını gösterecek şekilde
güncelleyebilir. Bazıları ise bunu yapmaz. Eğer taşınabilir programlar
yazmak istiyorsanız buna güvenmeyin. (gettimeofday()
işlevinden faydalanın. Saçma geliyor değil mi evet ama böyle ben ne
yapabilirim.)
Peki ya okuma kümesindeki soketlerden biri bağlantıyı kesmiş ise? Bu
durumda select() bu soket için "okumaya hazır"
mesajı verir ve siz recv() ile okumaya
kalktığınızda da recv() size 0
değerini döndürür. Böylece istemcinin bağlantıyı kesmiş olduğunu
anlarsınız.
Meraklısına select() ile ilgili bir bilgi daha: eğer
listen() ile dinlemekte olan bir soketiniz varsa
bu soketin dosya tanımlayıcısını readfds kümesine
yerleştirerek yeni bir bağlantı olup olmadığını öğrenebilirsiniz.
İşte dostlarım, süper güçlü select() işlevinin özeti
bu kadar.
Ancak gelen yoğun istek üzerine yukarıdaki bilgileri pratiğe dökeceğimiz
bir örnek sizi bekliyor.
Bu program basit bir çok kullanıcılı "chat" sunucu olarak
davranır. Derledikten sonra bunu bir pencerede çalıştırın ve ardından
telnet ile programa bağlanın
("
telnet hostname 9034"). Farklı farklı
pencerelerden programa aynı anda birden fazla sayıda bağlantı
açabilirsiniz. Bir kere bağlanıp da bulunduğunuz
telnet
ortamından bir şeyler yazıp yolladığınızda, mesajınız diğer bağlı
telnet pencerelerinde de görünmeli.
/*
** selectserver.c -- keyifli bir cok kullanicili chat sunucu
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 9034 // dinledigimiz port
int main(void)
{
fd_set master; // na dosya tanimlayici listesi
fd_set read_fds; // select() icin gecici dosya tanimlayici listesi
struct sockaddr_in myaddr; // sunucu adresi
struct sockaddr_in remoteaddr; // istemci adresi
int fdmax; // azami dosya tanimlayici numarası
int listener; // dinlenen soket tanımlayıcı
int newfd; // yeni accept()lenecek soket tanımlayıcı
char buf[256]; // istemci verisi için tampon
int nbytes;
int yes=1; // setsockopt() SO_REUSEADDR için, aşağıda
int addrlen;
int i, j;
FD_ZERO(&master); // ana listeyi ve gecici listeyi temizle
FD_ZERO(&read_fds);
// dinleyiciyi yarat
if ((listener = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket");
exit(1);
}
// "adres zaten kullanımda" mesajından kurtul
if (setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &yes,
sizeof(int)) == -1) {
perror("setsockopt");
exit(1);
}
// bind
myaddr.sin_family = AF_INET;
myaddr.sin_addr.s_addr = INADDR_ANY;
myaddr.sin_port = htons(PORT);
memset(&(myaddr.sin_zero), '\0', 8);
if (bind(listener, (struct sockaddr *)&myaddr, sizeof(myaddr)) == -1) {
perror("bind");
exit(1);
}
// listen
if (listen(listener, 10) == -1) {
perror("listen");
exit(1);
}
// dinleyici soketi ana listeye ekle
FD_SET(listener, &master);
// en büyük dosya tanimlayicisi hatirla
fdmax = listener; // so far, it's this one
// ana döngü
for(;;) {
read_fds = master; // copy it
if (select(fdmax+1, &read_fds, NULL, NULL, NULL) == -1) {
perror("select");
exit(1);
}
// mevcut baglantilari tarayip okumaya hazir olanlari tespit et
for(i = 0; i <= fdmax; i++) {
if (FD_ISSET(i, &read_fds)) { // bir tane yakaladik!!
if (i == listener) {
// handle new connections
addrlen = sizeof(remoteaddr);
if ((newfd = accept(listener, (struct sockaddr *)&remoteaddr,
&addrlen)) == -1) {
perror("accept");
} else {
FD_SET(newfd, &master); // ana listeye ekle
if (newfd > fdmax) { // azami miktarı güncelle
fdmax = newfd;
}
printf("selectserver: new connection from %s on "
"socket %d\n", inet_ntoa(remoteaddr.sin_addr), newfd);
}
} else {
// istemciden gelen veri icin gerekeni yap
if ((nbytes = recv(i, buf, sizeof(buf), 0)) <= 0) {
// bir hata var ya da istemci baglantiyi kesti
if (nbytes == 0) {
// baglanti kesilmis
printf("selectserver: socket %d hung up\n", i);
} else {
perror("recv");
}
close(i); // bye!
FD_CLR(i, &master); // ana listeden cikar
} else {
// istemciden bir miktar veri geldi
for(j = 0; j <= fdmax; j++) {
// gelen veriyi herkese yolla!
if (FD_ISSET(j, &master)) {
// dinleyici ve kendimiz haric
if (j != listener && j != i) {
if (send(j, buf, nbytes, 0) == -1) {
perror("send");
}
}
}
}
}
} // O KADAR CIRKIN ki!
}
}
}
return 0;
}
Lütfen dikkat edin: Yukarıdaki kod içinde iki dosya tanımlayıcı
listem var: master ve read_fds.
Birincisi yani, master o esnada bağlı olan
tüm soket tanımlayıcılarını tutuyor ve buna ek olarak bir de
dinleme görevini üstlenmiş olan soketi de bünyesinde barındırıyor.
master diye bir liste oluşturmamın sebebi
select() işlevinin sizin ona verdiğiniz soket
listesini değiştirmesi ve hangilerinin okunmaya hazır olduğunu
gösterecek şekilde güncellemesi. select()
çağrıları arasında bağlantı kümesinin bozulmamış bir kopyasına
ihtiyacım olduğu için böyle bir şey yaptım. Yani son anda
master kümesini read_fds
kümesine kopyalıyor ve select() işlevini
çağırıyorum.
İyi de bu aynı zamanda her yeni bağlantı talebinde bu bağlantıyı
master listesine eklemem gerektiği anlamına
gelmiyor mu? Tabii ki! Ve her bağlantı kesilmesinde de kesilen
bağlantı ile ilgili tanımlayıcıyı da master
listesinden çıkarmam gerekmiyor mu? Elbette!.
Farkında iseniz listener soketinin ne zaman
hazır olduğunu kontrol ediyorum. Hazır olduğunda bu demek oluyor ki
yeni bir bağlantı talebi var ve ben de bunu görünce bağlantıyı
accept() ile kabul ediyor ve master
listesine ekliyorum. Benzer şekilde bir istemci bağlantısı hazır
olduğunda ve recv() işlevi 0
döndürdüğünde anlıyorum ki istemci bağlantıyı kapatmış ve bu yüzden onu
master listesinden çıkarmalıyım.
Ancak eğer istemci recv() işlevi ile sıfırdan
farklı bir değer döndürdüğünde de biliyorum ki okunmuş olan bir veri
yığını var. Bu veriyi alıyorum ve sonra da master
listesi üzerindeki elemanlar üzerinden tek tek dolaşıp almış olduğum
veriyi diğer istemcilere yolluyorum.
Ve dostlarım işte size süper güçlü select() işlevinin
o kadar da basit olmayan açıklaması.