Gyorstalpaló hálózatprogramozás C nyelven UNIX környezetben Ha jba Gábor László
2007. február 20.
Tartalomjegyzék
1. A legfontosabb parancs
4
2. TCP alapú kliens m¶ködése
4
3. Kliens socket létrehozása
5
4. Kapcsolódás a szerverhez
6
5. TCP alapú szerver m¶ködése
9
6. Szerver socket porthoz kötése
10
7. Szerver socket felkészítése kliens fogadására
11
8. Kapcsolat felvétele a klienssel
11
9. Példa a szerver kapcsolatra kész állapotra hozására
12
10.Kommunikáció
14
11.Kapcsolat lezárása
15
12.Kapcsolat felépítése a kliens szemszögéb®l
16
13.Kapcsolat felépítése a szerver szemszögéb®l
16
14.Kapcsolat bontása az aktív oldalon
17
15.Kapcsolat bontása a passzív oldalon
17
16.Egy korszer¶ kliens-szerver megoldás
18
17.A legegyszer¶bb megoldás
18
18.Kliensek párhuzamos kiszolgálása gyermek folyamatok segítségével
20
19.Folyamatok közötti kommunikáció pipe-ok segítségével
22
1
20.Kliensek kiszolgálása szálak segítségével
23
21.Signalok
23
22.Események, amelyek signalt válthatnak ki
23
23.A session fogalma
23
24.Daemon megvalósítása
23
25.Szolgáltatás nyújtása inetd segítségével
23
2
El®szó Ez ahogy a címe is jelzi egy gyorstalpaló és összefoglaló C nyelven történ® hálózatprogramozáshoz. Így feltételez egy meglév® programozási hátteret. Nem tér ki a téma egészére, és nem fed le minden említett témát teljes egészében. Ellenben arra éppen elég, hogy ne okozzon nagy nehézséget a
Számítógéplapbor 2.
elnevezés¶
tantárgy sikeres elvégzése (a program megvédésével együtt).
Írhatnék még egy általános bevezet®t is, de szeritnem azt már mindenki átlapozza, ezért ismertnek tételezem fel a hálózati kommunikáció alapjait. A két lehetséges kommunikációs csatorna közül a
TCP-vel foglalkozunk.
A legtöbb helyen vázlatpontokat írok fel, ez is b®ven elég a megértéshez, illetve a lényeg is könnyebben olvasható mint folyószövegben.
3
1. A legfontosabb parancs A programok fordítása a legfontosabb lépés a programozásban, így a legfontosabb parancs az, amelyikkel rá tudjuk venni a fordítót, hogy forrásfájljainkból bájtkódot generáljon, és futtathassuk alkalmazásunk.
Ha megvan a kódunk, a következ® paranccsal fordíthatjuk le:
gcc -o programNeve forrás1.c forrás2.c
...
Ha C++ nyelven írjuk a programot:
g++ -o programNeve forrás1.cpp forrás2.cpp
...
Amennyiben a program már tartalmaz hálózat kezelést, úgy szükséges még némi kiegészítés a fordításban:
gcc -o programNeve forrás1.c forrás2.c
...
g++ -o programNeve forrás1.cpp forrás2.cpp
Ezek után a
./programNeve parancs kiadása
-lnsl -lsocket -lresolv ...
-lnsl -lsocket -lresolv
után el is indul a program ha mindent jól
csináltunk. Amennyiben mégsem, úgy hibajelzéssel tér vissza a fordító, melyet ki kell javítanunk. Nem kell meglep®dni, ha más fordítón lefordul a program, míg a célrendszeren nem: el®fordulnak eltérések a rendszer kongurációjából adódóan (f®leg az URAL2-n).
2. TCP alapú kliens m¶ködése
•
A program TCP-re épül® szolgáltatást próbál elérni.
•
A végrehajtás lépései:
1. socket létrehozása, 2. porthoz kötés,
4
3. kapcsolódás, 4. kommunikáció a szerverrel, 5. kapcsolat bezárása.
A fent említett lépések közül a porthoz kötéssel nem foglalkozunk, annak a kliensnél nincs sok szerepe.
3. Kliens socket létrehozása
A
•
Független attól, hova akarunk csatlakozni.
•
Nem kell tudnunk a szerverprocessz címét.
•
Vigyázni kell, hogy a megadott tartomány és protokoll helyes legyen.
•
Létrehozásához a
socket
socket
függvényt használjuk.
metódus:
#include
<s y s / t y p e s . h>
#include
<s y s / s o c k e t . h>
int
socket (
int
domain ,
int
int
type ,
protocol );
A függvény visszatérsi értéke egy socket leíró, hiba esetén A
A
domain
−1.
paraméter értéke a következ® lehet:
•
AF_UNIX ha helyi hálózatra kívánunk csatlakozni,
•
AF_INET ha az internetre kívánunk csatlakozni.
type
paraméter értéke a következ®k egyike lehet:
•
SOCK_STREAM TCP kapcsolat létrehozásához,
•
SOCK_DGRAM UDP kapcsolat létrehozásához,
5
•
SOCK_RAW_IP bele lehet nyúlni az üzenet fejrészébe címet, csak
A
protocol
root
→
át lehet írni az IP
jogokkal futtatható (ezt nem használjuk).
0-t
paraméter meghatározza a protokolt, ha
az alapértelmezett protokollt fogja használni (a
type
adunk meg értékeként, akkor
paraméterben megadott kapcsolat
típushoz igazodva).
4. Kapcsolódás a szerverhez
A
connect
•
A
•
TCP-nél kötelez® a használata, UDP-nél nem.
connect
metódussal történik.
metódus:
#include
<s y s / t y p e s . h>
#include
<s y s / s o c k e t . h>
int
connect (
int
sd ,
const struct
A függvény visszatérési értéke Az
sd
Az
addr
0
sockaddr
siker esetén,
paraméterben kell átadni a
socket
−1,
∗
ad dr ,
int
addrlen ) ;
ha valamilyen hiba lépett fel.
függvény által visszaadott socket leírót.
mutatóban kell megadni a szerver processz címét (IP-cím + port) tároló struk-
túra hivatkozását. Err®l kés®bb olvashatunk b®vebben. Az A
addrlen
connect
paraméter pedig a szerver processz címét tároló struktúra mérete.
metódus (sikeres) visszatérésével felépül a kapcsolat.
A szerver processz címét egy
struct
sockaddr
típusú struktúrába kell elhelyezni:
in_addr { u_long
s_addr ;
};
struct
sockaddr_in {
6
u_short
sin_family ;
u_short
sin_port ;
struct char
in_addr
sin_addr ;
sin_zero [ 8 ] ;
// nem h a s z n á l t
};
A
sin_family
A
sin_port
A
sin_addr a szerver processz IP címét tárolja → struktúra, ennek a s_addr részébe kell
a socket tartománya (esetünkben AF_INET).
a szolgáltatás portja.
az IP-t elhelyezni.
Felmerül a kérdés: honnan tudjuk az IP-t? Van, amikor alapból így hívjuk meg a programot, mert ismert a szerver processz IP címe, másik lehet®ség pedig, hogy a neve alapján lekérjük az IP címet egy DNS szervert®l. Ebben nyújt segítséget a
gethostbyname metó-
dus.
#include struct A
hostent
hostname
gethostbyname (
const char ∗
paraméter a szerver processz címét jelenti, pl.
A függvény egy
struct
∗
hostent
hostname ) ;
ural2.bme.hu.
struktúrával tér vissza siker esetén, egyébként
hostent {
// h o s t neve
char ∗
h_name ;
// 0− v a l l e z á r t l i s t a a h o s t t ö b b i n e v é r ® l
char ∗∗
h_aliases ;
// a cím t í p u s a ( e s e t ü n k b e n AF_INET)
int
h_addrtype ;
// a cím h o s s z a b á j t b a n 7
0-t
ad vissza.
int
h_length ;
// 0− v a l l e z á r t l i s t a az IP−c í m e k r ® l
char ∗∗
h_addr_list ;
//a l i s t a e l s ® eleme a k o m p a t i b i l i t á s m i a t t #d e f i n e
h_addr
h_addr_list [ 0 ]
}; Az IP címek listájának els® elemét (h_addrlist[0]) bemásoljuk a struktúra
sin_addr
tagjába. A
htons paranccsal a hálózati bájtsorrendet módsíthatjuk, ha esetleg rossz formában vol-
na (azaz mindenképp használjuk).
Kommunikációt a
read
és
write
A kapcsolat végén a socketet a lunk, akkor pedig az
fclose
metódusokkal valósítjuk meg.
close
függvénnyel zárhatjuk le; ha stream-eket haszná-
1
metódust kell meghívni.
Példa kapcsolat felépítésére:
#include
<s y s / t y p e s . h>
#include
<s y s / s o c k e t . h>
#include
< n e t i n e t / i n . h>
#include
int
socket ;
struct
hostent
struct
sockaddr_in
int
// htons −hoz s z u k s e g e s
∗ he ; their_addr ;
kapcsolatLetesitese (
void )
1 Nálam a stream-ek használata nem m¶ködött az URAL2-n.
8
{
if
( ( he = g e t h o s t b y n a m e ( " u r a l 2 . bme . hu " ) == NULL)
// megkapja a h o s t i n f o r m a c i o t
{
exit (1); }
if
( ( s o c k e t = s o c k e t (AF_INET, SOCK_STREAM,
0 ) ) ==
−1)
{ exit (1); } t h e i r _ a d d r . s i n _ f a m i l y = AF_INET ; their_addr . sin_port = htons ( 2 3 4 5 6 ) ; their_addr . sin_addr =
∗ ( ( struct
// h o s t b a j t s o r r e n d // a h t o n s parametere a p o r t
in_addr
∗ ) he−>h_addr ) ;
// k i n u l l a z z a a s t r u k t u r a t o b b i r e s z e t memset (&( t h e i r _ a d d r . s i n _ z e r o ) ,
if
8);
( connect ( socket , (
{
' \0 ' ,
struct
sockaddr
∗)& t h e i r _ a d d r , s i z e o f ( struct
s o c k a d d r ) ) ==
// k a p c s o l o d a s exit (1);
}
return
0;
}
5. TCP alapú szerver m¶ködése Egy szerver is hasonló m¶veleteket végez el, mint a kliens:
1. Socket létrehozása a
socket
függvénnyel (azonosan m¶ködik, mint a kliensnél),
9
−1)
2. socket hozzákötése egy helyi porthoz a
bind
függvény segítségével ezen a porton
keresztül tud csatlakozni a kliensünk,
3. socket felkészítése a kliensek fogadására a
listen
4. a klienst®l érkez® kapcsolatkérés elfogadása az
5. kommunikáció a
read
6. kapcsolat lezárása a
és a
close
write vagy
függvény segítségével,
accept
függvénnyel,
függvényekkel,
shutdown
függvények valamelyikével.
Most nézzük meg egyesével a lépéseket.
6. Szerver socket porthoz kötése
#include
<s y s / t y p e s . h>
#include
<s y s / s o c k e t . h>
int
bind (
int
sd ,
const struct
A függvény visszatérési értéke Az
sd
Az
addr
0,
sockaddr
∗
ad dr ,
int
addrlen ) ;
ha minden rendben van, hiba esetén pedig
paraméter egy socket leíró, melyet a
socket
−1.
metódussal hoztunk létre.
paraméter egy pontos cím, ahova kötjük a socketet (lásd a
sockaddr
struktúra
korábbi leírását). Az
addrlen
paraméter a struktúra méretét (hosszát) tárolja.
Ebben az esetben nincs értelme a 0 portot megadni, hisz akkor mi magunk sem tudjuk, melyik porton gyel a szerverünk. Talán akkor volna esély a használatára, mikor mi magunk nem tudjuk, melyik port lehet szabad (például sok hálózati alkalmazást egyszerre futtató gépünk van), és a kliens is broadcast kéréssel keresi meg a szervert. A laborfeladatban ez nem szükséges (ha valaki olyan forgatja ezt a jegyzetet, aki ilyesmit ír, akkor biztos jobban ért hozzá, mint ahogy itt leírom a dolgokat).
10
INADDR_ANY
A számítógép összes IP címét az fordítani a
htons
paraméter használatával.
paraméter jelenti, ezeket is sorrendbe kell
(Tehát nem korlátozzuk be a klienst, hogy
épp melyik IP címet kell feltárcsáznia, hogy elérje szerverszolgáltatásunk.)
Ha egy socket már hozzá van kötve egy porthoz, akkor a
getsockname függvénnyel kérdez-
hetjük le (ami azt takarja, hogy megadunk a függvénynek paraméterként egy
sockaddr
struktúrát és egy socketet, ® pedig visszaadja a struktúrában a megadott socket adatait).
7. Szerver socket felkészítése kliens fogadására Ezt a részt nevezhetjük
A
listening nek az alkalmazott módszer után.
listen metódust csak a socket porthoz kötése után lehet meghívni ez eléggé nyilván-
való; illetve csak kapcsolat orientált kapcsolatnál m¶ködik (tehát nekünk pont megfelel, hisz mi TCP kapcsolatot alkalmazunk).
#include int
<s y s / s o c k e t . h>
listen (
int
sd ,
int
A függvény visszatérési értéke Az A
sd
qlen ) ;
0,
ha minden rendben, ellenkez® esetben
−1.
paraméter a socket leírója.
qlen
paraméter a várósor hosszát reprezentálja.
A várósor azt jelenti, mennyi klienst engedünk egyszerre várakozni, míg felépül a kapcsolat. Ha túl nagy ez a szám, sok kliens várakozhat; ha pedig kicsi ez a szám, akkor csak kevés kliens fér be, a többieknek pedig nem m¶köd®nek látszódik a szolgáltatás.
8. Kapcsolat felvétele a klienssel A kapcsolatot az
accept függvény meghívásával (pontosabban visszatérésével) építhetjük
fel szerverünk és egy kliens között.
11
Ezt a függvényt csak a
listen
függvény kiadása után hívhatjuk meg (természetesen ez
az általunk használt kapcsolatorientált esetben szükséges). A függvény addig várakozik (ez által blokkolja a program továbbfutását), míg egy kliens nem kezdeményez kapcsolódást.
Ekkor a kapcsolat felépítésével egy új socket jön létre
(visszatérési érték). Ebb®l következik, hogy az eredeti (szerver socket) megmarad, így az
accept
metódus kívánalmainknak megfelel® számban meghívható; másik következménye
pedig, hogy több kapcsolat lehet egyszerre, ezt pedig kezelni kell erre több módszer van, melyeket a kés®bbiekben ismertetek.
#include
<s y s / s o c k e t . h>
#include
<s y s / t y p e s . h>
int
accept (
int
fd ,
struct
A függvény visszatérési értéke
−1
sockaddr
∗
addrp ,
int
addrlen ) ;
hiba esetén, egyébként pedig egy socket leíró (a kliens
socketé). Az
fd
paraméter egy fájl leíró, ami a mi esetünkben a
listen-nel
el®készített (szerver)
socket leíró. Az
addrp
struktúrába kerül bele a kliens IP címe és a port, amelyen keresztül kapcsolat
fel van építve.
Itt jegyzem meg, hogy az itt eltárolt port nem egyezik meg azzal a porttal,
amelyre a kapcsolat-kérés érkezett, hiszen ezen továbbra is a szerver gyeli a beérkez® kéréseket. Így a szerver egy másik (éppen szabad) portot rendel hozzá a kapcsolathoz. Az
addrlen
paraméter pedig szokás szerint a fent említett címstruktúra méretét jelenti.
A függvény visszatérése után a socket leírón (visszaadott érték) keresztül tudunk kommunikálni a klienssel (a
read
és
write
függvények felhasználásával).
9. Példa a szerver kapcsolatra kész állapotra hozására A következ®kben megnézünk egy egyszer¶ példát, ahol a szerverünk az 12345-ös porton gyel, és a várósor mérete 5 (tehát egyszerre 5 klienst engedünk várakozni a kapcsolatra).
12
#include
<s y s / t y p e s . h>
#include
<s y s / s o c k e t . h>
void
kapcsolat_letesitese ()
{
int
listen_sock ,
struct
client_sock ;
sockaddr_in
// a s o c k e t l e í r ó k
other_addr ;
// k l i e n s címe
listen_sock = createListenSocket (12345);
/ ∗ v a r a k o z o s o r meretenek b e a l l i t a s a ∗ /
if
( l i s t e n ( listen_sock ,
5) < 0)
{ exit (1); }
/ ∗ K a p c s o l a t r a várunk . . . ∗ /
while ( 1 ) // FIGYELEM ! ! ! v é g t e l e n c i k l u s { client_sock = accept ( listen_sock , (
struct
sockaddr
∗)& o t h e r _ a d d r
i f ( client_sock
==
,
&s i n _ s i z e ) ;
−1)
{
continue ; // e m i a t t nem k e l l m e g á l l n i }
/ ∗ K a p c s o l a t f e l é p í t v e , i n n e n t ® l jön a kommunikáció ∗ / } }
13
Látható, hogy a szerver kapcsolat felépítése a
createListenSocket metódus meghívásával
történik. A paramétere az a port, amelyen a kapcsolatokat várjuk. A visszatér® értéke pedig egy leíró socket, melyet
listen_sock
névvel jelölünk a minta programban.
10. Kommunikáció Ebben a részben gyorsan megtekintjük, hogyan kommunikálhatunk mind a klienssel, mind pedig a szerverrel.
A kommunikáció a socketeken keresztül történik, mivel a socketek egyfajta fájlleírók, melyeket használhatunk a
read
és
write
függvényekkel.
Ezen kívül stream-eket is rá lehet húzni, mellyel valamennyire kultúráltabban tudjuk kezelni az üzeneteket.
Igaz, ez nem m¶ködik az Ural2-n, így az egyszer¶ read és write
függvényeket kell alkalmaznunk.
Az olvasó függvény meghívásával a program további futása blokkolódik, így amíg nem érkezik semmilyen válasz az olvasott socketr®l, programunk áll. Természetesen ha valami hiba történik a távoli oldalon (például megszakad a kapcsolat), akkor err®l kapunk visszajelzést, és a
read
metódus blokkolódása feloldódik. A hibás üzenet lekezelése már a mi
feladatunk.
Most nézzünk egy-egy (egyszer¶) példát mind a stream-ekkel mind pedig a hagyományos módszerekkel történ® kommunikációra.
FILE
int char
∗ socket_stream ; socket_leiro ; buffer [100];
... s o c k e t _ s t r e a m = f d o p e n ( s o c k e t _ l e i r o , " r+" ) ; f p r i n t f ( " h e l l o \n" ,
socket_stream ) ;
14
// e l k ü l d ü n k egy ü z e n e t e t
f s c a n f ( "%s " , &b u f f e r ) ;
int char
// ü z e n e t e t várunk
socket_leiro ; buffer [100];
... write ( socket_leiro , read ( socket_leiro ,
" h e l l o \ r \n" , s t r l e n ( " h e l l o \ r \n" ) + 1 ) ; buffer ,
100);
Stream-ek használatával küldött üzenetek a másik oldalon ugyanúgy jelennek meg, még ha ott hagyományos módszerrel is fogadják ®ket, tehát a stream-ek használata egyik oldalon nem kötelezi a másik oldal készít®jét stream-ek alkalmazására.
11. Kapcsolat lezárása A kapcsolatot a
A
close
close
vagy
shutdown
metódusokkal lehet lezárni.
függvény használata:
close ( socket_leiro );
Szerintem ez nem szorul b®vebb magyarázatra. Ha stream-eket használunk, akkor a lezáráshoz az
A
shutdown
metódus hasonló a fent említett
fclose
close
metódust kell használnunk.
metódushoz, azzal a különbséggel,
hogy ezzel a socketet külön le tudjuk zárni írás, olvasás vagy mindkett® szemszögéb®l. Például: lezárjuk a socket-ünket írásra (ez azt takarja, hogy nem tudunk rajta keresztül üzeneteket küldeni a másik oldalra); amíg a másik oldal észreveszi ezt a lezárást, addig mi tudjuk olvasni a küldött üzenetet. Amikor a másik oldal érzékeli a lezárást, a kapcsolat megszakat, a mi oldalunk ún. fájlvége üzentetet kap, mi is lezárhatjuk a kapcsolatot.
int
shutdown (
int
sd ,
int
action ) ;
15
A függvény visszatérési értéke
0,
hiba esetén pedig
−1.
Az
sd
Az
action paraméterrel azt adhatjuk meg, hogy mire zárjuk a socket-ünket (0:
paraméter egy socket leíró. olvasásra,
1: írásra, 2: mindkett®re).
12. Kapcsolat felépítése a kliens szemszögéb®l Ez egy áttekint® rész, melyben címszavakban áttekintjük a kapcsolat felépítését a kliens szemszögéb®l:
•
socket elkészítése,
•
kapcsolat kezdeményezése
connect függvénnyel (ezt nevezzük active open -nek:
min-
dig a kliens oldalán történik a kapcsolat kezdeményezése, mivel ® tudja a szerver címét;
SYN
connect
jelet küld a szervernek, majd
SYN_SENT
állapotba kerül,
•
a
•
a szerver válasza tartalmazza a kérés nyugtáját, és jó esetben engedélyt ad a csat-
metódus nem tér vissza, a szerver válaszára vár,
lakozásra, ez esetben pedig
ESTABLISHED
állapotba kerül a socket.
13. Kapcsolat felépítése a szerver szemszögéb®l Ebben az áttekint® részben megnézzük, hogyan épül fel a kapcsolat a szerver szemszögéb®l:
•
socket létrehozása,
•
socket hozzákötése egy lokális (és természetesen szabad) porthoz,
•
felkészül a kapcsolat fogadására a
listen metódussal, és LISTEN állapotba kerül (ez
a passzív nyitás),
• SYN
üzenet fogadása után
SYN_RCVD
állapotba kerül a socket,
16
•
nyugta küldése a kliensnek, kapcsolat engedélyezése,
•
nyugta fogadása, majd a socket
ESTABLISHED
állapotba kerül.
14. Kapcsolat bontása az aktív oldalon Ebben az áttekint® részben megnézzük, mi is történik az aktív (a kapcsolat bontását kezdeményez®) oldalon a kapcsolat bontásakor:
• close • FIN
metódus meghívása,
üzenet küldése a kliens felé,
•
nyugta visszatérésekor a kapcsolat bontása
•
a socket lehetséges állapotai:
FIN_WAIT_1 állapot:
meger®sítésre vár,
FIN_WAIT_2 állapot: FIN jelet vár, FIN_WAIT_3
állapot: vár pár másodpercet, majd állapota
CLOSED
lesz, vagyis
rezárult a kapcsolat.
15. Kapcsolat bontása a passzív oldalon Ebben az áttekint® részben megnézzük, mi is történik a kapcsolat bontásakor a passzív oldalon:
• FIN •
CLOSE_WAIT
nyugtát küld a kapcsolat bontásáról,
• FIN •
üzenetet kap, hatására
close
küldése az aktív oldalnak, állapota
nyugta érkezésekor állapota
CLOSED
állapotba kerül,
LAST_ACK-ra
lesz.
17
metódus meghívása,
változik,
16. Egy korszer¶ kliens-szerver megoldás Egy korszer¶ klienssel szemben nincs sok elvárásunk - hálózati szemszögb®l, hisz ® a szerverre van utalva, nélküle nem képes m¶ködni (tekinthetjük vékony kliensnek, de egy vastag kliensnek is szüksége van távoli kommunikációra). Feladat olyan szerverek készítése, melyek egyszerre több klienst tudnak kiszolgálni, amennyiben lehetséges (például er®forrásigényes feladatoknál nincs értelme több klienst egyszerre kiszolgálni, de ahhoz, hogy ezt a labort sikeresen elvégezzük, nincs szükség ilyen igény¶ feladatokra, és azt hiszem, elvárásként is szerepel több kliens egyszerre történ® kiszolgálása). Ezt több folyamat (
process ) vagy több szál (thread ) alkalmazásával tehetjük meg.
A szálprogramozás mint általában minden programnyelvben nem könny¶ feladat, így azt tanácsolom, hogy ne ezzel próbálkozzon meg az, aki ezt a jegyzetet olvassa. Természetesen megemlítem a szálak kezelését minimális szinten.
Mint már említettem, a program futása blokkolódik mind az
accept,
mind pedig a
read
függvén meghívása után. Így felmerül az ötlet: mi volna, ha meg tudnánk nézni, van-e valami (valaki) a csatornán?
17. A legegyszer¶bb megoldás A legegyszer¶bb megoldást a
select
#include
<s y s / t i m e . h>
#include
<s y s / t y p e s . h>
#include
int
select (
int
fd_set
n,
∗
fd_set
∗
exceptfds ,
függvény adja.
readfds ,
struct
18
fd_set
∗
writefds ,
timeval
∗
timeout ) ;
A függvény visszatérési értékei:
0,
ha lejárt a megadott id®; pozitív érték, ha valamelyik
leírón adott m¶velet végezhet®; hiba esetén pedig negatív. A függvényben leíró csoportokat adunk meg:
readfds
Ennél a csoportnál arra vár, hogy legyen rajtuk olvasható adat, azaz az egyik
leíróra meghívott metódus
writefds
0-val
read (fscanf)
függvény meghívódjon.
Ha nincs adat, a
read
tér vissza.
Ez a csoport arra ügyel, hogy a
write (fprintf)
függvény ne blokkolódjon
íráskor.
exceptfds
Ez a csoport a leírók kivételeire vár.
Amelyik leírócsoportra nincs szükségünk, oda
NULL-t
adunk meg a metódus meghívása-
kor. A
timeout
paraméter a várakozási id®t jelenti, melynek leteltével mindenképp visszatér
a metódus. Ha
NULL-t
adunk meg, akkor addig vár, míg valami nem történik.
A függvény a paraméterként átadott leírócsoportokat törli, és a válaszát ezekbe helyezi el, tehát visszatérés után a
readfds
változó fogja tartalmazni az olvasható leírókat.
A függvény a gyel® socketre érkezett kapcsolódásokat és a már kapcsolódott kliensek kéréseit is gyeli, azonban az esetleges hibákat nem, amire külön ügyelni kell.
A leírócsoportok kezelésére léteznek speciális makrók:
• FD_ZERO(fd_set * set)
csoport el®készítése,
• FD_SET(int fd, fd_set * set)
leírót ezzel tesszük be a csoportba,
• FD_CLR(int fd, fd_set * set)
leíró kivétele a csoportból,
• FD_ISSET(int fd, fd_set * set)
megnézi, hogy a leíró a csoportban van-e.
19
18. Kliensek párhuzamos kiszolgálása gyermek folyamatok segítségével Folyamaton a program egy futó példányát értjük. Egy multitasking operációs rendszer több folyamatot képes egyszerre m¶ködtetni. (Szerencsénkre az Ural2 képes egyszerre több folyamat kiszolgálására, és mivel nem cél belebonyolódni a szálakba, így azt tanácsolom, próbáljuk meg ezt a módszert alkalmazni. Igaz, vannak olyan feladatok, melyek csak szálakkal valósíthatóak meg.) Egy folyamat további folyamatokat (alfolyamatokat) indíthat, így gyakran használják több klienst egyszerre kiszolgálni tudó szerverekhez.
PID
(Process ID): a folyamat azonosítója (jól jön akkor, ha valamiért nem tudjuk nor-
mális módon lekapcsolni).
GID (Group ID): csoportazonosító; minden folyamat beletartozik egy csoportba a shellr®l egy sorban indítottak egy csoportba kerülnek, tehát ha mi magunk gépeljük be a program nevét a konzolban, akkor egyedüli lesz a csoportban.
Alfolyamatokat a
#include
pid_t
fork
függvénnyel indíthatunk.
fork (
void ) ;
A metódus meghívásával a folyamat duplikálja magát, így a szül® és gyermek folyamatok nem látják egymás változóit; hiába fut ugyanaz a programkód, mégis más adatokon történik. Ebb®l látható, hogy memóriaigényes programokat nem érdemes multiplikálni. A gyermek folyamat a szül® csoportjába fog tartozni. A szül® folyamat futása a ral folytatódik. Mivel a
fork metódus visszatérése után a függvény után következ® sor-
fork függvény a gyerek folyamat csoportazonosítójával tér vissza
a szül® folyamatban, a gyerek folyamatban pedig
0-val (természetesen egy új alfolyamatot
indító gyerek folyamatot ebben az esetben szül®nek tekintünk), ezért az új folyamatnak
20
lehet speciális, a szül®t®l eltér® parancsokat adni:
int
pid = f o r k ( ) ;
i f ( pid
== 0 )
{
// parancsok i d e // f o l y a m a t megsz¶nik
exit (0); }
A kliensek csatlakozására különböz® módokon (különböz® helyekr®l) várhatunk: a sz¶l® folyamatban, vagy pedig a gyerek folyamatokban. Az els® esetben csak akkor hozzuk létre a gyerekfolyamatokat, ha már kiépült a kapcsolat a klienssel. Ekkor, ha több kliens akar csatlakozni, várakozni kényszerülnek, míg az el®ttük lév® összes kérésnek elkészül a gyerek folyamat; ám el®nye, hogy csak a szükséges mennyiség¶ er®forrást foglaljuk le. A második esetben el®re létrehozzuk a gyerek folyamatokat, így több kliens egyszerre csatlakozásakor azonnal sorra kerülnek, senki sem várakozik a
fork
metódus visszatérésére.
Ennek azonban megvan a hátránya: több er®forrást kötünk le, mint amire szükségünk lehet.
A gyermek folyamatokra (pontosabban azok megsz¶nésére) a sokkal várhatunk.
és
waitpid
metódu-
Ezek a metódusok operációs rendszerenként eltérnek, ezért nézzünk
man wait
utána a manual-ban ( A
wait
és
man waitpid ).
wait függvény hatására a szül® folyamat felfüggeszti a futását, míg egy gyerek folyamat
vissza nem tér (meg nem sz¶nik).
A függvény visszatérési értéke a megsz¶nt folyamat
azonosítója. A
waitpid
metódus meghívásakor a szül® folyamat futását felfüggeszti, míg az adott
folyamat azonosítójú (PID) gyermek vissza nem tér (meg nem sz¶nik). gyermek folyamat visszatérése megfelel nekünk, módszernek megadható a
WNOHANG
0-t
Ha bármelyik
is megadhatunk a függvénnynek. A
opció, mellyel a függvény akkor tér vissza, ha egy fo-
lyamat sem ér véget, tehát nem blokkolódik a program futása.
21
#include
<s y s / t y p e s . h>
#i n c l u s e
<s y s / w a i t . h>
pid_t
wait (
int ∗
status );
#include
<s y s / t y p e s . h>
#i n c l u s e
<s y s / w a i t . h>
pid_t
w a i t p i d ( pid_t
pid ,
int ∗
status ,
int
options ) ;
19. Folyamatok közötti kommunikáció pipe-ok segítségével Vannak esetek, mikor a gyerek adatokat kér a szül® folyamattól, ami nehéz feladat. Megoldása az inter-process communication (IPC).
A
pipe
m¶ködése FIFO (First In First Out) elv¶; pozícionálásra nincs lehet®ség; nincs
neve, hanem két fájlleíróval (a vezeték eleje és vége) azonosítjuk, amelyek közül az egyik ír, a másik olvas. Mivel a gyerek folyamatok öröklik a szül® folyamatuk leíróit, így a vezetéket is öröklik, tehát kapcsolatot tudnak tartani.
#include int
pipe (
int
filedes [2]);
A paraméterben lév® tömb els® (azaz
0.)
elemére lehet írni, a második elemér®l pedig
csak olvasni lehet. Mivel minden gyermek örököl, ezért több eleje és vége lesz a csatornáknak, a felesleges
22
végeket pedig le kell zárni mindkét oldalon. függvény
0-val
Az olvasható végek lezárásával a
read
tér vissza, míg az írási végek lezárása után íráshiba lép fel. Egy vezeték
addig él, míg legalább egy eleje és egy vége van. Amennyiben a
pipe
üres, a
read
függvény blokkolódik (mint ahogy azt a hálózati kom-
munikációnál már láttuk).
20. Kliensek kiszolgálása szálak segítségével
21. Signalok
22. Események, amelyek signalt válthatnak ki
23. A session fogalma
24. Daemon megvalósítása
25. Szolgáltatás nyújtása inetd segítségével
23