Vodič za mrežno programiranje Korištenje Internet soket-a

68
BEEJ-OV VODIČ ZA MREŽNO PROGRAMIRANJE Beej-ov vodič za mrežno programiranje Korištenje Internet soket- aBrian "Beej" Hall 2012 IT Sec Sandro

Transcript of Vodič za mrežno programiranje Korištenje Internet soket-a

Page 1: Vodič za mrežno programiranje Korištenje Internet soket-a

BEEJ-OV VODIČ ZA MREŽNO PROGRAMIRANJE Beej-ov vodič za mrežno programiranje Korištenje Internet soket-aBrian "Beej" Hall

2012

IT Sec

Sandro

Page 2: Vodič za mrežno programiranje Korištenje Internet soket-a

Beej-ov vodič za mrežno programiranjeKorištenje Internet soket-aBrian "Beej" Hall

[email protected] © 1995-2001 by Brian "Beej" Hall

Sadržaj

1. Uvod1.1. Saslušanje1.2. Platforma i kompajler1.3. Zvanična glavna strana1.4. Primjedba za Solaris/SunOS programere1.5. Primjedba za Windows programere1.6. U vezi elektronske pošte1.7. Mirroring1.8. Primjedba za prevodioce1.9. Pravo kopiranja i raspodjele2. Sta je soket?2.1. Dva tipa internet soketa2.2. Besmislice o niskom nivou i teorija mreža3. Strukture i rukovanje podacima3.1. Prevedi primitive!3.2. IP Adrese i kako rukovati njima4. Sistemski pozivi4.1. socket() - Daj mi fajl-deskriptor!4.2. bind() - Na kom sam portu?4.3. connect() - Hej, ti!4.4. listen() - Molim vas, hoće li me neko nazvati?4.5. accept() - "Hvala vam što ste zvali port 3490."4.6. send() i recv() - Pričaj sa mnom, lutko!4.7. sendto() i recvfrom() - Pričaj sa mnom, DGRAM-stil4.8. close() i shutdown() - Bježi mi s očiju!4.9. getpeername() - Ko si sad pa ti?4.10. gethostname() - Ko sam ja!?4.11. DNS - ti kažeš "bijelakuca.gov", ja kažem "198.137.240.92"5. Pozadina klijent-servera5.1. Primjer jednostavnog stream servera5.2. Primjer jednostavnog stream klijenta5.3. Datagram soketi6. Nešto naprednije tehnike6.1. Blokiranje6.2. select() - Sinhrono (paralelno, istovremeno (prim. prev.)) U/I multipleksiranje6.3. Rukovanje parcijalnim send() funkcijama6.4. O enkapsulaciji podataka7. Više podataka o ovoj temi

Page 3: Vodič za mrežno programiranje Korištenje Internet soket-a

7.1. man stranice7.2. Knjige7.3. Reference na internetu7.4. RFC-i8. Često postavljena pitanja9. Objava i poziv u pomoć

Page 4: Vodič za mrežno programiranje Korištenje Internet soket-a

1. UvodHej! Soket programiranje te umorilo? Čini ti se da je malo preteško ove stvari shvatiti iz man stranica? Htio bi da praviš cool internet programe, ali nemaš vremena da prolaziš kroz gomilu struktura pokušavajući da shvatiš da li treba da pozoveš bind() prije connect(), itd., itd.

E pa, znaš šta! Već sam sâm uradio sav taj prljavi posao, i umirem od želje da podijelim te informacije sa svima! Došao si na pravo mjesto. Ovaj bi dokument trebao dati prosječnom C stručnjaku neko dovoljno znanje za hvatanje u koštac sa mrežnim programiranjem.

1.1. Saslušanje

Ovaj je dokument napravljen kao udžbenik, ne kao priručnik. Vjerovatno je najbolji za one koji tek počinju sa internet programiranjem, i potreban im je oslonac. Ovo, naravno, nije kompletan vodič kroz soket programiranje.

Ipak, nadam se da će biti dovoljan da one man stranice počnu da zvuče smisleno... :-)

1.2. Platforma i kompajler

Kôd napisan u ovom dokumentu je kompajliran na Linux PC-u koristeći GNU gcc kompajler. Ipak, trebao bi da radi na bilo kojoj platformi koristeći gcc. Prirodno, ovo se ne odnosi na situaciju kada programiraš za Windows, vidi sekciju za Windows programere, ispod.

1.3. Zvanična glavna strana

Zvanična lokacija ovog dokumenta je na Kalifornijskom Univerzitetu, Chico, na http://www.ecst.csuchico.edu/~beej/guide/net/.

1.4. Primjedba za Solaris/SunOS programere

Kad programiraš za SunOs i Solaris, moraš da staviš neke dodatne parametre komandne linije, za povezivanje sa odgovarajućim bibliotekama. U svrhu toga, jednostavno dodaj "-lnsl -lsocket -lresolv" na kraj komande za kompajliranje, kao ispod:

$ cc -o server server.c -lnsl -lsocket -lresolv

Ako još uvijek dobijaš poruke o greškama, pokušaj dodati "-lxnet" na kraj komandne linije. Ne znam za šta služi, tačno, ali izgleda da treba nekim ljudima.

Još jedno mjesto gdje možeš naići na problem je mjesto gdje pozivaš funkciju setsockopt(). Deklaracija se razlikuje od one na mojoj Linux mašini, tako da umjesto:

Page 5: Vodič za mrežno programiranje Korištenje Internet soket-a

int yes=1;

unesi ovo:

char yes='1';

Pošto nemam SunOS mašinu, nisam testirao ništa od gore-spomenutih informacija – to je sve samo ono što sam ja dobio elektronskom poštom od drugih ljudi.

1.5. Primjedba za Windows programere

Lično mi se ne dopada Windows, i preporučujem ti da probaš Linux, BSD, ili Unix. Pošto sam to rekao, ipak možeš sve ovo da koristiš i pod Windowsom.

Prvo, preskoči sve sistemske .h datoteke koje sam ovde spomenuo. Sve što ti je potrebno da uključiš je:

#include <winsock.h>

Čekaj! Takođe moraš da pozoveš WSAStartup() prije nego što počneš bilo šta drugo da radiš sa soketima. Evo kôda koji to radi:

#include <winsock.h>  { WSADATA wsaData; // ako ovo ne radi //WSAData wsaData; // probaj ovo  if (WSAStartup(MAKEWORD(1, 1), &wsaData) != 0) { fprintf(stderr, "WSAStartup failed.\n"); exit(1); }

Takođe moraš da kažeš kompajleru da se povezuje sa Winsock bibliotekom, koja se obično nalazi u datoteci wsock32.lib ili winsock32.lib ili nešto slično. U Visual C++-u, ovo se radi kroz Project meni, pod opcijom Settings.... Klikni na Link jezičak, i potraži listu "Object/library modules". Dodaj "wsock32.lib" toj listi.

Ili tako sam ja bar čuo.

Na kraju, moraš da pozoveš funkciju WSACleanup() kad više ne koristiš biblioteku za rad sa soketima. Pogledaj na mreži detalje o ovoj temi.

Ako sve ovo uradiš, primjeri iz ove knjige bi trebali da funkcionišu uglavnom, sa izuzetkom par stvari. Pod jedan, ne možeš koristiti close() da zatvoriš soket – moraš da koristiš closesocket(), umjesto toga. Takođe, select() radi samo sa soket-deskriptorima, ne i sa fajl-deskriptorima (poput 0 za stdin (standardni ulaz)).

Page 6: Vodič za mrežno programiranje Korištenje Internet soket-a

Takođe, postoji klasa za rad sa soketima, CSocket. Provjeri help stranice svog kompajlera radi više informacija.

Više informacija o Winsock-u ima na Winsock FAQ.

Konačno, dočuo sam da Windows nema fork() sistemskog poziva kog sam, nažalost, koristio u nekim svojim primjerima. Probaj da koristiš CreateProcess() umjesto njega. fork() ne prima argumente, a CreateProcess() prima oko četrdeset milijardi argumenata. Ako ti se to ne dopada, CreateThread() je nešto lakša za probavu... nažalost rasprava o nitima u programiranju ne ulazi u sastav ovog teksta. Rekao sam što sam mogao.

1.6. U vezi elektronske pošte

Generalno, mogu odgovarati na pitanja postavljena elektronskom poštom, ali ne garantujem. Vodim život sa malo slobodnog vremena, i postoje trenuci kad jednostavno ne mogu odgovarati na pitanja. Kada je to slučaj, obično samo obrišem poruku. Ništa lično; jednostavno, nikad neću imati vremena da ti dam detaljno objašnjenje svega što ti treba.

Kao uopšteno pravilo, što je složenije pitanje koje postaviš – manja je vjerovatnoća da ću odgovoriti. Ako suziš pitanje prije nego što ga pošalješ i uključiš sve umjesne informacije (kao platforma, kompajler, poruke o greškama, i bilo šta drugo), veća je vjerovatnoća da ćeš dobiti odgovor. Pročitaj ESR-ov dokument, Kako postaviti pametna pitanja.

Ako ne dobiješ odgovor, probaj sam nešto da središ, da nađeš odgovor, a ako ne uspiješ, pošalji mi pismo opet – sa informacijama koje si dodatno uspio da pronađeš.

Sad kad sam te ugnjavio kako da mi pišeš a kako ne, samo da ti dam do znanja da mnogo cijenim svu podršku i zahvalnost koju sam primio tokom godina, povodom vodiča. To je prava moralna podrška, i drago mi je da čujem da ga ljudi mnogo koriste! :-) Hvala!

1.7. Mirroring

Više si nego dobrodošao da takođe iskopiraš ovaj sajt drugdje, privatno ili javno. Ako ga objaviš javno, pošalji mi link da ga spojim sa svoje glavne strane; pošalji ga na <[email protected]>.

1.8. Primjedba za prevodioce

Ako hoćeš da prevedeš vodič na drugi jezik, piši mi na <[email protected]> i staviću hiperlink ka tvom prevodu na svoju glavnu stranu.

Slobodno stavi svoju email adresu i ime u prevod.

Žao mi je, ali zbog kvote (ograničenog prostora), ne mogu stavljati prevod na svoj sajt.

Page 7: Vodič za mrežno programiranje Korištenje Internet soket-a

1.9. Pravo kopiranja i raspodjele

Beej-ov vodič za mrežno programiranje je zaštićen. Copyright © 1995-2001 Brian "Beej" Hall.

Ovaj vodič se slobodno može štampati i kopirati, ako se njegov prvobitni sadržaj ne promijeni, i ako bude kompletan, i takođe se prikažu informacije o njegovoj zaštićenosti i pravu kopiranja i raspodjele.

Predavačima se naročito preporučuje da preporučuju ili dijele kopije ovog vodiča svojim studentima.

Dokument se slobodno može prevoditi na druge jezike, ako je prevod precizan i tačan, i obuhvata kompletan prvobitni dokument. Prevod takođe može da sadrži informacije o imenu i načinu kontaktiranja prevodica.

Izvorni C kôd u ovom dokumentu je ovim odobren za javnost.

Kontaktiraj <[email protected]> za više informacija.

2. Šta je soket?Čuo si da se priča o "soketima" čitavo vrijeme, a možda se cijelo vrijeme pitaš šta je to ustvari tačno. Pa, oni su sledeće: način komuniciranja sa ostalim programima koristeći standardne UNIX-ove fajl-deskriptore.

Dobro, možda si čuo izjavu nekog UNIX hakera: "Bog te, sve u UNIX-u je datoteka!" Ono što je ta osoba ustvari pričala je činjenica da kada UNIX programi čine bilo kakav U/I, čine ga čitanjem ili pisanjem po fajl-deskriptoru. Fajl-deskriptor je mali cio broj pridružen otvorenom fajlu (datoteci). Ali (i baš tu je caka), datoteka može biti mrežna konekcija, FIFO, cijev, terminal, prava datoteka na disku, ili uostalom bilo šta. Sve u UNIX-u je datoteka! Na taj način, komunikacija sa drugim programom preko interneta se obavlja preko fajl-deskriptora, vjerovao ili ne.

"Kako dobijem ovaj deskriptor za mrežnu komunikaciju, Gosp. Pametni?" je vjerovatno poslednje pitanje koje ti je sad na umu, ali svejedno ću da odgovorim na njega: Koristiš poziv sistemske funkcije socket(). Ona vraća soket-deskriptor, i onda komuniciraš preko njega koristeći specijalizovane soket pozive send() i recv() (man send, man recv).

"Hej, čekaj!" mogao bi sad da uzvikuješ. "Ako je to fajl-deskriptor, zašto ne bih mogao, u ime Neptuna, jednostavno koristiti read() i write() da komuniciram preko soket-deskriptora?"

Page 8: Vodič za mrežno programiranje Korištenje Internet soket-a

Kratak odgovor je "Pa mogao bi!" Dug odgovor je, "Mogao bi, ali send() i recv() pružaju mnogo veću kontrolu razmjene podataka."

Šta dalje? Šta misliš o ovome: Ima mnogo vrsta soketa. Postoje DARPA Internet adrese (internet soketi), staze do datoteka kao nodova na disku (UNIX soketi), CCITT X.25 adrese (X.25 soketi koje slobodno možeš da ignorišeš), i vjerovatno mnogi drugi zavisno od toga kakav UNIX koristiš. Ovaj dokument obrađuje samo one prve: internet sokete.

2.1. Dva tipa internet soketa

Šta je ovo? Postoje dva tipa internet soketa? Da. Pa, dobro, ne. Lažem. Ima ih više, ali nisam htio da te preplašim. Ovdje ću pričati samo o dva tipa. Osim u ovoj rečenici kad ću da kažem da su "sirovi soketi (raw sockets)" takođe jako moćni i trebalo bi da ih pogledaš.

Da počnemo već jednom. Koja su to dva tipa? Prvi je "stream socket"; drugi je "datagram socket", koje ćemo ubuduće vjerovatno zvati "SOCK_STREAM" i "SOCK_DGRAM", tim redom. Datagram soketi se ponekad nazivaju "nepovezani soketi". (Iako mogu biti connect()-ovani ako to stvarno hoćeš. Pogledaj connect(), dalje dole.)

Stream soketi su pouzdani dvosmjerni povezani komunikacioni provodnici. Ako pošalješ dva objekta u soket, rasporedom "1, 2", oni će i da stignu u rasporedu "1, 2" na odredište. Takođe, ti objekti tamo neće sadržati greške. Sve greške s kojim se suočiš su plod tvog sopstvenog poremećenog uma, i neće se o njima ovdje raspravljati.

Šta koristi stream soket? Pa vjerovatno si čuo za program telnet? On koristi stream soket. Sve što otkucaš treba da stigne u istom rasporedu na drugi kraj, je li tako? Takođe, svi web brauzeri koriste HTTP protokol koji koristi stream sokete da dođu do internet stranica. Zaista, ako se "telnetuješ" na web-site na portu 80, i otkucaš "GET /", dobićeš zauzvrat HTML stranu!

Kako stream soketi dobijaju ovaj visoki nivo kvaliteta protoka podataka? Oni koriste protokol "The Transmission Control Protocol (Protokol za kontrolu prenosa podataka)", poznat u narodu kao "TCP" (vidi RFC-793 za izvanredno detaljne podatke o TCP-u.) TCP osigurava da podaci stignu u istom rasporedu i bez grešaka. Možda si već čuo za "TCP" kao dio skraćenice "TCP/IP" gdje "IP" znači "Internet Protocol" (vidi RFC-791.) IP prvenstveno radi sa Internet usmjeravanjem i nije odgovoran za integritet podataka.

Super. Šta ima da se kaže za datagram sokete? Zasto se kaže za njih da su "nepovezani"? O čemu se tu, uopšte, radi? Zašto oni nisu pouzdani? Pa evo nekih činjenica: Ako pošalješ datagram, možda i stigne. Možda stigne u drugačijem rasporedu. Ako stigne, podaci u paketu će biti bez grešaka, ipak.

Datagram soketi takođe koriste IP za usmjeravanje, ali oni ne koriste TCP; oni pak koriste"User Datagram Protocol (Protokol za korisničke datagrame)", ili "UDP" (vidi RFC-768.)

Zašto su oni nepovezani? Pa, u osnovi, to je zbog toga što ne moraš da održavaš otvorenu konekciju kao što moraš sa stream soketima. Samo napraviš paket, udariš mu IP zaglavlje koje

Page 9: Vodič za mrežno programiranje Korištenje Internet soket-a

sačinjava adresa odredišta, i pošalješ ga. Nije potreban spoj. Uglavnom se koriste za paket-po-paket protoke informacija. Neke aplikacije koje ih koriste: tftp, bootp, itd.

"Dosta!" možda vrištiš. "Kako ovi programi uopšte rade ako se paketi mogu izgubiti na mreži?!" Pa, zemljanine, svaki od njih ima sopstveni protokol na vrhu UDP-a. Na primjer, protokol tftp kaže da za svaki paket koji se pošalje, prijemnik mora da pošalje natrag drugi paket koji kaže, "Imam ga!" ("ACK" paket.) Ako pošiljalac prvobitnog paketa ne dobije odgovor u roku, recimo, pet sekundi, slaće paket ponovo dok konačno ne dobije ACK. Ovakva procedura je vrlo bitna kad se ostvaruju SOCK_DGRAM aplikacije.

2.2. Besmislice o niskom nivou i teorija mreža

Pošto sam upravo spomenuo nivoe protokola, vrijeme je da pričamo kako mreže stvarno rade, i da pokažem neke primjere kako se SOCK_DGRAM paketi izgrađuju. Praktično, vjerovatno možete da preskočite ovu lekciju. Ipak, ovo gradivo čini dobru pozadinu za sledeće.

Slika 1. Enkapsulacija podataka.

Hej, djeco, vrijeme je da se uči o enkapsulaciji podataka! Ovo je jako bitno. Toliko je bitno da se o tome već uči ovdje na univerzitetu «Čiko» ;-). U principu, tvrdi se sledeće: paket je rođen, paket je umotan ("enkapsuliran") u zaglavlje (i rijetko u podnožje) od strane prvog protokola (npr, TFTP protokola), a onda se sve to (skupa sa TFTP zaglavljem) enkapsulira ponovo od sledećeg protokola (npr. UDP-a), onda ponovo od sledećeg (IP), i onda ponovo od poslednjeg protokola na hardverskom (fizičkom) sloju (npr, Ethernet-u).

Kad drugi računar primi paket, hardver skida IP i UDP zaglavlja, TFTP program skida TFTP zaglavlje, i taj računar konačno ima prave podatke.

Sad konačno mogu da pričam o neomiljenom modelu mreže u više nivoa (Layered Network Model). Ovaj mrežni model opisuje sistem mrežne funkcionalnosti koja ima mnoge prednosti nad ostalim modelima. Na primjer, možeš pisati soket-programe koji su u potpunosti isti, i da ne brineš kako se podaci ustvari prenose (serijski, tanki Ethernet, AUI, šta god) jer programi na nižem nivou sve to sređuju za tebe. Stvarni mrežni hardver i topologija su transparentni soket-programeru.

Bez daljeg zadržavanja, predstaviću ti slojeve tog modela u punom svjetlu. Zapamti ovo za ispit iz mreža:

         Aplikacioni (Application)

         Prezentacijski (Presentation)

Page 10: Vodič za mrežno programiranje Korištenje Internet soket-a

         Sesioni (Session)

         Transportni (Transport)

         Mrežni (Network)

         Spoj spone podataka (Data Link)

         Fizički (Physical)

Fizički sloj je hardver (serijski, Ethernet, itd.). Aplikacioni je onoliko daleko od fizičkog što god više možeš da zamisliš – to je mjesto gdje korisnici imaju dodir sa mrežom.

E sad, ovaj model je toliko uopšten da bi mogao da ga koristiš kao vodič za popravku automobila ako bi htio. Slojeviti model koji je više u skladu sa UNIX-om bi mogao da bude sledeći:

         Aplikacioni sloj (telnet, ftp, itd.)

         "Host-to-Host" (server serveru) transportni sloj (TCP, UDP)

         Internet sloj (IP i usmjeravanje)

         Sloj mrežnog pristupa (Ethernet, ATM, ili Šta god)

U ovom trenutku, vjerovatno vidiš kako ovi slojevi odgovaraju enkapsulaciji prvobitnih podataka

Jel' vidiš koliko posla ima oko enkapsuliranja paketa podataka? Gospode! A znaš li da moraš da ukucavaš adresu u zaglavlje koristeći "cat"?! Šalim se, šalim se. Sve što treba da uradiš za stream sokete je da ih pošalješ (send()) vani. Sve što treba da uradiš za datagram sokete je da enkapsuliraš paket metodom koji ti izabereš i da ga pošalješ (sendto()) napolje. Jezgro samo gradi transportni sloj i internet sloj, hardver pravi sloj mrežnog pristupa, takođe sam. Ah, moderna tehnologija.

Tako se završava naš kratki pohod u teoriju mreže. Eh da, zaboravio sam da ti kažem sve što sam htio o usmjeravanju, a to je: ništa. Upravo tako, uopšte neću pričati o usmjeravanju. Usmjeravač "oguli" paket do IP zaglavlja, pogleda u njegovu tabelu usmjeravanja, bla bla bla. Pogledaj IP RFC ako te stvarno zanima. Ako nikad o tome ne naučiš, pa dobro, ostaćeš živ.

3. struct-ure i rukovanje podacima

Page 11: Vodič za mrežno programiranje Korištenje Internet soket-a

Konačno smo ovde. Vrijeme je da pričamo o programiranju. U ovom dijelu, pokriću razne tipove podataka korištene u obraćanju soketima, pošto su neke od njih prava muka za shvatiti.

Prvo nešto lako: soket-deskriptor. Soket-deskriptor je sledećeg tipa:

int

Najobičniji cio broj.

Stvari odavde pa nadalje postaju uvrnute, tako da samo čitaj i trpi me. Znaj ovo: postoje dva uređenja bajtova: najvažniji bajt (ponekad poznat kao "oktet") na prvom mjestu, i pod dva: najmanje bitan bajt na prvom mjestu. Ovaj prethodni je nazvan "Mrežno uređenje bajtova" (Network Byte Order). Neke mašine interno smještaju svoje brojeve u mrežnom uređenju bajtova, neke ne. Kad kažem da nešto mora biti u mrežnom uređenju bajtova, moraš da pozoveš neku funkciju (recimo htons()) da ga prevedeš iz "serverskog uređenja bajtova" (Host Byte Order). Ako ne spomenem "mrežno uređenje bajtova", onda možeš da ga ostaviš u serverskom uređenju bajtova.

(Za radoznale, "mrežno uređenje bajtova" je takođe poznato kao "Big-Endian Byte Order".)

Moja Prva StrukturaTM - struct sockaddr. Ova struktura čuva informacije o adresi soketa za mnoge tipove soketa:

struct sockaddr { unsigned short sa_family; // familija adrese, AF_xxx char sa_data[14]; // 14 bajtova adrese protokola };

sa_family može biti mnoštvo stvari, ali biće AF_INET za sve što mi radimo u ovom dokumentu. sa_data sadrži odredišnu adresu i broj porta za soket. Ovo je prilično nezgrapno pošto niko neće da ručno, mukotrpno pakuje adresu u sa_data.

Da bi radili sa struct sockaddr, programeri su razvili paralelnu strukturu: struct sockaddr_in ("in" kao "internet".)

struct sockaddr_in { short int sin_family; // Familija adrese unsigned short int sin_port; // Broj porta struct in_addr sin_addr; // Internet adresa unsigned char sin_zero[8]; // Da bude iste veličine kao struct sockaddr };

Ova struktura čini lakim da se obraćamo elementima adrese soketa. Primijeti da bi sin_zero (koji je uključen da produži strukturu do dužine strukture struct sockaddr) trebao biti podešen na sve same nule funkcijom memset(). Takođe, a ovo je JAKO bitno, pokazivač na strukturu struct sockaddr_in može biti kastovan u pokazivač na struct sockaddr i obrnuto. Tako da, iako socket() očekuje struct sockaddr *, možeš koristiti struct sockaddr_in i kastovati

Page 12: Vodič za mrežno programiranje Korištenje Internet soket-a

kad zatreba! Takođe, primijeti da sin_family odgovara sa_family u strukturi struct sockaddr i treba da bude podešen na AF_INET. Konačno, sin_port i sin_addr moraju biti u mrežnom uređenju bajtova!

"Ali," protiviš se ti, "kako može cijela struktura, struct in_addr sin_addr, biti u mrežnom uređenju bajtova?" Ovo pitanje zahtijeva pažljiv prilaz strukturi struct in_addr, jednoj od najgorih živih unija:

// Internet adresa (struktura zbog istorijskih razloga) struct in_addr { unsigned long s_addr; // to je 32 bita dužine, ili 4 bajta };

Pa, nekad je to bila unija, ali sad su, izgleda, ti dani prošli. Dobro izbavljenje. Tako ako si predstavio ina da bude tipa struct sockaddr_in, onda ina.sin_addr.s_addr predstavlja četvorobajtnu IP adresu (u mrežnom uređenju bajtova). Primijeti da čak iako tvoj sistem možda koristi od Boga prognanu uniju umjesto strukture za struct in_addr, ipak možeš da se obratiš četvorobajtnoj adresi baš kao što sam i ja iznad (Ovo zahvaljujući #define-ovima)

3.1. Prevedi primitive!

Eto nas dovedenih pravac u sledeće poglavlje. Dosta je bilo priče o "mrežno u serversko uređenje" prevođenjima – vrijeme je za akciju.

E, fino. Postoje dva tipa koja možeš prevesti: short (dva bajta) i long (četiri bajta). Ove funkcije rade i sa unsigned verzijama isto tako dobro. Recimo da hoćeš da prevedeš neki short iz serverskog uređenja bajtova u mrežno uređenje bajtova. Počni sa "h" za "host" (server), nastavi sa "to" (u – prevedi u), i onda "n" za "network" (mreža), i "s" za "short": h-to-n-s, or htons() (čita se: "Host to Network Short").

Skoro da je prejednostavno....

Možeš koristiti bilo koju kombinaciju "n", "h", "s", i "l", isključujući one stvarno glupe. Npr., nema stolh() ("Short to Long Host") funkcije – ne na ovoj žurci, zapravo. Ali postoje:

         htons() - "Host to Network Short"

         htonl() - "Host to Network Long"

         ntohs() - "Network to Host Short"

         ntohl() - "Network to Host Long"

Sad, možda ti se učini da ulaziš polako u ovo. Možeš pomisliti "Šta ako treba da promijenim raspored bajtova u promjenjivoj tipa char?" Onda ćeš možda pomisliti, "Uh, nema veze." Možeš takođe pomisliti da, budući da tvoja mašina "68000" već koristi mrežno uređenje bajtova, pa da ti

Page 13: Vodič za mrežno programiranje Korištenje Internet soket-a

ne moraš pozivati htonl() na svojim IP adresama. Bio bi u pravu, ALI ako pokušaš da preneseš program na mašinu koja ima obrnut raspored, program će pasti. Neka ti programi budu prenosivi! Ovo je svijet UNIX-a! (Koliko god Bil Gejts htio misliti drugačije.) Zapamti: stavi bajtove u mrežno uređenje prije nego što odu na mrežu.

Jedna stavka za kraj: Zašto sin_addr i sin_port moraju biti u mrežnom uređenju bajtova u strukturi struct sockaddr_in, a sin_family ne mora? Odgovor je: sin_addr i sin_port se enkapsuliraju u paket na IP i UDP slojevima, redom. Stoga, moraju biti u tom uređenju. S druge strane, sin_family polje je korišteno samo od strane jezgra da sazna kakav tip adrese struktura sadrži, pa mora biti u serverskom uređenju bajtova. Takođe, pošto sin_family ne biva poslata napolje na mrežu, ona u svakom slušaju može biti u serverskom uređenju bajtova.

3.2. IP Adrese i kako rukovati njima

Na sreću po tebe, postoji mnoštvo funkcija koje rukuju IP adresama. Nema potrebe da ih prevodiš ručno i stavljaš u long pomoću << operatora.

Prvo, recimo da imaš struct sockaddr_in ina, i IP adresu "10.12.110.57" koju hoćeš da smjestiš u datu strukturu. Funkcija koja ti treba, inet_addr(), prevodi brojeve iz zapisa "numbers-and-dots" (brojke i tačke) u oblik neoznačenog cijelog broja (unsigned long). Pridruživanje se može evo ovako obaviti:

ina.sin_addr.s_addr = inet_addr("10.12.110.57");

Primijeti da inet_addr() vraća adresu u mrežnom uređenju bajtova, automatski – ne moraš da pozivaš htonl(). Divno!

Sad, ovaj isječak kôda nije baš jak jer nema provjere za greškama. Vidiš, inet_addr() vraća -1 ako je došlo do greške. Sjećaš se binarnih brojeva? (unsigned)-1 će odgovarati IP adresi 255.255.255.255! To je validna adresa! Promašaj. Sjeti se da odradiš provjeravanje greške kako treba.

Ustvari, postoji drugačiji sistem koji stoji umjesto inet_addr(): nazvan je inet_aton() ("aton" znači "ascii to network"):

#include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> int inet_aton(const char *cp, struct in_addr *inp);

A evo i primjera upotrebe, dok se pakuje struct sockaddr_in (ovaj će primjer imati više smisla kad dođeš do sekcija o bind() i connect().)

struct sockaddr_in my_addr; my_addr.sin_family = AF_INET; // serversko uređenje bajtova my_addr.sin_port = htons(MYPORT); // kratko, mrežno uređenje bajtova inet_aton("10.12.110.57", &(my_addr.sin_addr));

Page 14: Vodič za mrežno programiranje Korištenje Internet soket-a

memset(&(my_addr.sin_zero), '\0', 8); // ostatak strukture popuni nulama

inet_aton()ne liči ni na jednu drugu funkciju vezanu za sokete, jer vraća različito od nule pri uspijehu, a nulu kao grešku. A adresa se prosleđuje u inp (pogledaj deklaraciju iznad ovog primjera.)

Nažalost, ne implementiraju sve platforme inet_aton() pa, iako se preporučuje njegova upotreba prije no inet_addr, inet_addr() se koristi u ovom vodiču.

Dobro, sad znamo prevesti IP adresu u njegovu binarnu reprezentaciju. Kako u suprotnom slučaju? Šta ako imamo struct in_addr a želimo da vidimo oblik te adrese u brojkama i tačkama? U tom slučaju, trebaće nam funkcija inet_ntoa() ("ntoa" znači "network to ascii"):

printf("%s", inet_ntoa(ina.sin_addr));

To će odštampati IP adresu. Primijeti da inet_ntoa() uzima struct in_addr kao argument, ne long. Takođe primijeti da vraća pokazivač na char. Ovaj pokazivač pokazuje na stalno smješteni niz karaktera unutar inet_ntoa() (static char *) tako da svaki put kad pozoveš inet_ntoa(), ovaj će prepisati novu preko stare IP adrese. Na primjer:

char *a1, *a2; . . a1 = inet_ntoa(ina1.sin_addr); // ovo je 192.168.4.14 a2 = inet_ntoa(ina2.sin_addr); // ovo je 10.12.110.57 printf("adresa 1: %s\n",a1); printf("adresa 2: %s\n",a2);

će odštampati:

adresa 1: 10.12.110.57 adresa 2: 10.12.110.57

Ako ti treba da čuvaš adrese, koristi strcpy() da ih iskopiraš u sopstvene nizove.

To je sve o ovoj temi zasad. Kasnije, naučićeš da prevedeš tekst poput "whitehouse.gov" u njegovu odgovarajuću IP adresu (vidi DNS, ispod.)

4. Sistemski pozivi

Page 15: Vodič za mrežno programiranje Korištenje Internet soket-a

Tu smo gdje se saznaju sistemski pozivi koji omogućavaju pristup radu na mreži sa UNIX mašine. Kad pozoveš neku od ovih funkcija, jezgro preuzima kontrolu i obavlja sav ostatak posla za tebe automatski.

Ljudi se najčešće zbune oko toga koji sistemski poziv kad pozvati. Tu man stranice ništa ne pomažu, kao što si vjerovatno već primijetio. Zbog strahote te situacije, pokušao sam da ti predstavim sistemske pozive u tačno onom rasporedu u kom se oni i pozivaju u programima.

To, skupa sa nešto malo izvornog kôda tu i tamo, nešto mlijeka i kolača (koje ćeš, bojim se, morati sam nabaviti), i nešto sirove snage i hrabrosti, i bacaćeš podatke po internetu kao sin Džona Postela (ko god to bio :), prim. prev.).

4.1. socket() - Daj mi fajl-deskriptor!Izgleda da ne mogu više da odlažem – moram da govorim o sistemskom pozivu socket() . Evo:

#include <sys/types.h> #include <sys/socket.h> int socket(int domain, int type, int protocol);

Ali šta predstavljaju ovi argumenti? Prvo, domain treba podesiti na "AF_INET", baš kao u strukturi struct sockaddr_in (iznad.) Dalje, type argument kaže jezgru koji tip soketa je ovo: SOCK_STREAM ili SOCK_DGRAM. Konačno, podesi protocol na "0" da bi socket() izabrao pravi protokol zavisno od type argumenta. (Pazi: postoji mnogo više stvari koje mogu da stoje na mjestu domain argumenta. Postoji mnogo više opcija za type argument. Pogledaj socket() man stranicu. Takođe, postoji "bolji" način da se podesi protocol. Pogledaj getprotobyname() man stranicu.)

socket() jednostavno vraća soket-deskriptor koji kasnije možeš koristiti u sistemskim pozivima, ili -1 ako je došlo do greške. Globalna promjenjiva errno se tada podesi na odgovarajuću vrijednost. (Vidi perror() man stranicu.)

U nekim knjigama ćeš vidjeti kako se pominje mistični "PF_INET". Ovo je jedna neobična zvjerka koja se uopšte rijetko viđa u prirodi, ali ipak ću pokušati da malo razjasnim o čemu se radi. Jednom davno, mislilo se da bi familija adrese (AF u AF_INET) mogla podržavati nekoliko protokola koji su bili predstavljeni svojom familijom protokola (PF u PF_INET). Ali nije bilo tako. Ali dobro. Prava stvar da se uradi je da se koristi AF_INET u strukturi struct sockaddr_in a PF_INET u pozivu funkcije socket(). Ali, AF_INET možeš praktično svuda da koristiš. I pošto tako radi Richard Stevens u svojoj knjizi, tako ću i ja raditi ovdje.

Dobro, dobro, dobro, ali šta ću ja sad sa ovim soketom? Odgovor je da on sam po sebi ništa ne predstavlja, ali moraš da čitaš dalje i naučiš još sistemskih poziva da bi učinio da stvari počnu da funkcionišu.

4.2. bind() - Na kom sam portu?

Page 16: Vodič za mrežno programiranje Korištenje Internet soket-a

Jednom kad dobiješ soket, možda bi trebao da ga pridružiš nekom portu na svojoj lokalnoj mašini. (Ovo obično radiš ako ćeš očekivati (listen()) poziv za uspostavljanje veze na nekom portu. Jezgro koristi broj porta da poveže pristigli paket i odgovarajući proces i njegov soket-deskriptor. Ako ćeš samo koristiti connect(), ovo onda možda nije neophodno. Ipak pročitaj, za svaki slučaj.

Evo sintakse sistemskog poziva bind():

#include <sys/types.h> #include <sys/socket.h> int bind(int sockfd, struct sockaddr *my_addr, int addrlen);

sockfd je soket-deskriptor, broj koji je vracen od funkcije socket(). my_addr je pokazivač na strukturu struct sockaddr koja sadrži adresu tj. port i IP adresu. addrlen se obično podesi na sizeof(struct sockaddr).

E tako. To smo sve u jednom zalogaju. 'de da vidimo primjer:

#include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #define MYPORT 3490 main() { int sockfd; struct sockaddr_in my_addr; sockfd = socket(AF_INET, SOCK_STREAM, 0); // ovde je potrebno provjeriti greške! my_addr.sin_family = AF_INET; // serversko uređenje bajtova my_addr.sin_port = htons(MYPORT); // kratko, mrežno uređenje bajtova my_addr.sin_addr.s_addr = inet_addr("10.12.110.57"); memset(&(my_addr.sin_zero), '\0', 8); // ostatak strukture popuniti nulama // Ti ćeš sam uraditi provjeru grešaka za bind(): bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr));� . . .

Ovde postoji par stvari za primijetiti: my_addr.sin_port je u mrežnom uređenju bajtova. my_addr.sin_addr.s_addr je takođe. Druga stvar koje treba da se paziš je da se sistemska .h zaglavlja koja sam gore uključio razlikuju od sistema do sistema. Jedini način da se uvjeriš je da provjeriš man stranice na svom računaru.

I još nešto, na samom sam vrhu poglavlja trebao spomenuti da svoju IP adresu i/ili port možeš automatski da dobiješ:

my_addr.sin_port = 0; // izaberi slučajnim izborom slobodan port my_addr.sin_addr.s_addr = INADDR_ANY; // izaberi svoju IP adresu

Page 17: Vodič za mrežno programiranje Korištenje Internet soket-a

Vidiš, postavljajući my_addr.sin_port na nulu, ti ustvari kažeš bind()-u da izabere slobodan port. Isto tako, postavljajući my_addr.sin_addr.s_addr na INADDR_ANY, kažeš mu da automatski popuni mjesto za IP adresu adresom računara na kom proces teče.

Ako primjećuješ male stvari, onda si primijetio da nisam stavio INADDR_ANY u mrežno uređenje bajtova! Baš sam nevaljao. Ali, uradio sam to jer sam znao jednu stvar: INADDR_ANY je ustvari uvijek nula! Ako mu okrenemo bajtove, dobićemo istu stvar. Ipak, čistunci bi rekli da može da postoji paralelna dimenzija u kojoj bi INADDR_ANY moglo biti, recimo, 12 i da bi tu moj program pukao. E pa kako hoćete:

my_addr.sin_port = htons(0); // izaberi slobodan port slučajnim izborom my_addr.sin_addr.s_addr = htonl(INADDR_ANY); // izaberi svoju IP adresu

Nećeš vjerovati koliko je program sad prenosiv. Samo sam htio ovo naročito da istaknem da znaš, ali ubuduće se neću mučiti da provlačim INADDR_ANY kroz htonl().

bind() takođe vraća -1 ako dođe do greške i podešava errno na odgovarajuću vrijednost.

Još jedna stvar koje treba da se paziš sa funkcijom bind(): nemoj da ideš ispod donje granice brojeva porta. Svi portovi sa brojem ispod 1024 su REZERVISANI (osim ako si superkorisnik(root))! Može bilo koji port iznad 1024, i to sve do 65535 (osim ako nije već rezervisan od strane nekog drugog procesa.)

Ponekad se desi da pokušaš ponovo pokrenuti isti program i da bind() padne, ostavljajući poruku "Adresa već u upotrebi" ("Address already in use"). Šta to znači? Pa, recimo da tada "dio" soketa ostane poslije gašenja programa u memoriji. Možeš da sačekaš (jedno minut), ili da dodaš kôd programu tako da mu dozvoliš da ponovo koristi port, nešto ovakvo:

int yes=1;//char yes='1'; // za Solaris korisnike

// da sprečiš poruku "Address already in use" if (setsockopt(listener,SOL_SOCKET,SO_REUSEADDR,&yes,sizeof(int)) == -1) { perror("setsockopt"); exit(1); }

Jedna mala potpuno sićušna ekstra-posljednja napomena za funkciju bind(): često je uopšte nećeš morati pozivati. Ako koristiš connect() da se spojiš sa udaljenim računarom i nije te briga s kog porta ideš (kao što je slučaj sa telnet-om kad te zanima samo port računara na koji se spajaš), onda jednostavno koristiš connect(), koji će provjeriti da li je soket slobodan, pa će ga povezati (bind()) na slobodan lokalni port, ako je neophodno.

4.3. connect() - Hej, ti!

Page 18: Vodič za mrežno programiranje Korištenje Internet soket-a

Hajde da se na trenutak pravimo da si ti telnet. Korisnik ti naređuje (Baš kao u filmu TRON) da pribaviš soket-deskriptor. Ti se složiš i pozoveš socket(). Dalje, korisnik ti kaže da se spojiš na "10.12.110.57", port "23" (standardni telnet port.) Hah! Šta ćeš sad?

Imaš sreće, sad čitaš poglavlje o funkciji connect() – kako da se spojiš na udaljeni računar. Zato sad navali na čitanje! Nemaš puno vremena!

Poziv connect() glasi:

#include <sys/types.h> #include <sys/socket.h> int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);

sockfd je soket-deskriptor, kog je vratio poziv socket(), serv_addr je struktura struct sockaddr koja sadrži odedišni port i IP adresu, a addrlen se obično podesi na sizeof(struct sockaddr).

Počinje da ima smisla, zar ne? Evo primjera:

#include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #define DEST_IP "10.12.110.57" #define DEST_PORT 23 main() { int sockfd; struct sockaddr_in dest_addr; // sadržaće odredišni port i IP adresu sockfd = socket(AF_INET, SOCK_STREAM, 0); // Ovde je potrebno provjeriti greške! dest_addr.sin_family = AF_INET; // serversko uređenje bajtova dest_addr.sin_port = htons(DEST_PORT); // kratko, mrežno uređenje bajtova dest_addr.sin_addr.s_addr = inet_addr(DEST_IP); memset(&(dest_addr.sin_zero), '\0', 8); // ostatak strukture popuni nulama // Ne zaboravi da provjeriš greške poslije connect()! connect(sockfd, (struct sockaddr *)&dest_addr, sizeof(struct sockaddr)); . . .

Da ponovim, uvjeri se da si provjerio da li je dobro prošao poziv funkcije connect() – vratiće -1 ako je došlo do greške i errno će sadržati odgovarajuću vrijednost.

Page 19: Vodič za mrežno programiranje Korištenje Internet soket-a

Takođe, jesi li primijetio da uopšte nismo pozivali bind(). U osnovi, nije nas briga s kog idemo porta; briga nas je samo na koji port idemo (udaljeni port). Jezgro će samo izabrati naš lokalni port, a sajt na koji se spajamo će lako saznati koji je to port. Nema da brineš.

4.4. listen() - Molim vas, hoće li me neko nazvati?

OK, vrijeme je za promjenu tempa. Šta ako ti uopšte nećeš da se spajaš na bilo kakav udaljeni računar? Recimo da samo, dakle, hoćeš da sačekaš bilo kakav poziv sa udaljenog računara i obradiš svaki takav poziv na odgovarajući način. Proces je iz dva koraka: prvo koristiš funkciju listen()(slušaj()) a onda accept() (primi()) (vidi ispod.)

Poziv listen() je relativno jednostavan, ali zahtjeva malo objašnjenje:

int listen(int sockfd, int backlog);

sockfd je uobičajeni soket-deskriptor dobijen funkcijom socket(). backlog je broj dozvoljenih poziva koji će moći da čekaju u redu za obradu. Šta to znači? Pa, svi nadolazeći pozivi će sačekati u redu za čekanje, dok ih ne primi tvoj program funkcijom accept() (vidi ispod) i ovaj broj određuje koliko je dozvoljeno da ih bude u redu za čekanje (queue). Većina sistema ovaj broj prećutno ograničava na 20; tebi vjerovatno neće trebati više od 5 ili 10.

Da, naravno, ako dođe do greške, listen() vraća -1 i podešava errno kako treba.

Kao što pretpostavljaš, zovemo bind() prije nego listen(); inače će jezgro samo izabrati port na kom će da čeka pozive. Znači ako ćeš da slušaš pridolazeće pozive, raspored sistemskih poziva će biti ovakav:

socket(); bind(); listen(); /* accept() ide ovde */

Neka to stoji umjesto nekog izvornog kôda, pošto je lako za razumjeti. (Kôd u poglavlju accept(), dole niže, je potpuniji.) Dio o kom se dobro mora razmisliti, jedini takav u cijeloj priči, je dio o funkciji accept().

4.5. accept() – "Hvala vam što ste zvali port 3490."

Spremi se – poziv accept() je pomalo uvrnut! Evo šta će se desiti: Neko ko je jako daleko će pokušati da ti se spoji (connect()) na računar, na port na kom čekaš (listen()). Njegov poziv će se smjestiti u red za čekanje sve dok ga ne primiš (accept()). Pozivaš accept() kojim preuzimaš jedan poziv iz reda za čekanje. Ova funkcija će ti, kao izlaznu vrijednost, vratiti potpuno nov soket-deskriptor koji odgovara ovom jednom pozivu (vezi)! Tako je, odjednom imaš dva soket-deskriptora po cijenu jednog! Onaj početni i dalje očekuje pozive na datom portu, a ovaj novi je konačno spreman da se proslijedi funkcijama send() i recv(). Došli smo dotle!

Page 20: Vodič za mrežno programiranje Korištenje Internet soket-a

Funkcija glasi:

#include <sys/socket.h> int accept(int sockfd, void *addr, int *addrlen);

sockfd je soket-deskriptor soketa koji očekuje pozive na našem portu. Lako. addr je obično pokazivač na lokalnu strukturu struct sockaddr_in. Tu idu informacije o pozivu koji trenutno obrađujemo (i tu saznamo odakle nam dolazi poziv i s kog porta). addrlen je tipa int i treba biti podešen na sizeof(struct sockaddr_in) prije nego se njegova adresa proslijedi funkciji accept(). accept() će najviše toliko bajtova smjestiti u addr. Ako stavi manje nego toliko, to će se odraziti na addrlen.

Znaš šta? accept() vraća -1 i podešava errno ako se pojavi greška. Kladim se da to nisi sam shvatio.

Kao i prije, ovo je komadina za jedan zalogaj, pa navodim primjer izvornog kôda za proučavanje:

#include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #define MYPORT 3490 // port na kom čekamo poziv #define BACKLOG 10 // koliki je maksimum poziva u redu za čekanje main() { int sockfd, new_fd; // čekaj na sock_fd, dolazeći pozivi u new_fd struct sockaddr_in my_addr; // informacije o mojoj adresi struct sockaddr_in their_addr; // informacije o adresi onog koji nas zove int sin_size; sockfd = socket(AF_INET, SOCK_STREAM, 0); // provjera grešaka! my_addr.sin_family = AF_INET; // serversko uređenje bajtova my_addr.sin_port = htons(MYPORT); // kratko, mrežno uređenje bajtova my_addr.sin_addr.s_addr = INADDR_ANY; // automatski popuni mojom IP adresom memset(&(my_addr.sin_zero), '\0', 8); // ostatak strukture popuni nulama // ne zaboravi provjeriti eventualne greške u ovim pozivima: bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)); listen(sockfd, BACKLOG); sin_size = sizeof(struct sockaddr_in); new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size); . . .

Još jednom da napomenem, soket-deskriptor new_fd će biti korišten za sve send() i recv() pozive. Ako ćeš imati samo jednu vezu, možeš zatvoriti (close()) sockfd koji čeka poziv, da bi sprečio nove pridolazeće pozive na istom portu – ako ti tako treba.

Page 21: Vodič za mrežno programiranje Korištenje Internet soket-a

4.6. send() i recv() – Pričaj sa mnom, lutko!

Ove dvije funkcije služe za komunikaciju preko stream-soketa ili povezanog datagram-soketa. Ako želiš koristiti uobičajeni, nepovezani datagram soket, pogledaj poglavlja sendto() i recvfrom(), niže.

send() poziv:

int send(int sockfd, const void *msg, int len, int flags);

sockfd je soket-deskriptor preko kojeg hoćeš da šalješ podatke (bilo da je to onaj vraćen od funkcije socket() ili onaj što si ga dobio sa accept().) msg je pokazivač na podatke koje hoćeš da šalješ, a len je dužina tog teksta u bajtovima. flags podesi na 0. (Vidi send() man strane za više informacija o mogućim opcijama za flags.)

Primjer:

char *msg = "Beej je bi ovde!"; int len, bytes_sent; . . len = strlen(msg); bytes_sent = send(sockfd, msg, len, 0); . . .

send() vraća broj bajtova koje uspije da pošalje – ovo zna biti manje nego broj bajtova koje si mu rekao da pošalje! Vidiš, ponekad mu kažeš da pošalje veliku hrpu podataka i on jednostavno ne uspije. Poslaće koliko god više može, i povjeriće tebi da ostatak pošalješ kasnije. Zapamti, ako vrijednost vraćena funkcijom send() ne odgovara vrijednosti len, na tebi je da pošalješ ostatak teksta. Dobra vijest je da će send(), ako je paket mali (manji od 1K ili nešto slično), vjerovatno uspjeti poslati sve odjednom. Opet, -1 se vraća kao greška, i errno se postavlja na broj greške.

recv() je sličan u mnogim aspektima:

int recv(int sockfd, void *buf, int len, unsigned int flags);

sockfd je soket-deskriptor sa kog čitaš, buf je pokazivač na mjesto gdje će se učitani podaci skladištiti, len je broj za koji se pretpostavlja da neće stići više bajtova od njega (maksimum primljenih bajtova tim pozivom), a flags se opet, obično, postavi na 0. (Pogledaj recv() man stranice za informacije.)

recv() vraća broj bajtova koji su se učitali, ili -1 ako dođe do greške (errno se naravno podešava kako treba.)

Page 22: Vodič za mrežno programiranje Korištenje Internet soket-a

Čekaj! recv() može vratiti 0. Ovo znači samo jedno: Druga strana je zatvorila vezu!

Eto, lako, zar ne? Sad možeš slati i primati podatke kroz stream sokete! Jupi! Postao si mrežni programer za UNIX!

4.7. sendto() i recvfrom() – pričaj sa mnom, DGRAM-stil

"Sve je ovo fino, moj prijatelju", čujem već kako govoriš, "ali gdje se tu uklapaju nepovezani soketi?" Nema frke, čovječe. Imamo pravu stvar za tebe.

Pošto datagram-soketi nisu spojeni sa drugim računarom, već znaš šta treba da se stavi u paket prije nego što ga pošaljemo? Upravo tako! Odredišnu adresu! Da zagrebemo:

int sendto(int sockfd, const void *msg, int len, unsigned int flags, const struct sockaddr *to, int tolen);

Kao što vidiš, ovaj poziv je u osnovi isti kao send() sa dodatkom dvije informacije. to je pokazivač na strukturu struct sockaddr (koju ćeš vjerovatno čuvati u obliku strukture struct sockaddr_in i prevesti je (cast) u zadnjem trenutku) koja sadrži informacije o odredišnoj IP adresi i portu. tolen se uglavnom jednostavno podesi na sizeof(struct sockaddr).

Baš kao send(), i sendto() vraća kao izlaznu vrijednost broj bajtova koje je poslao (koji, opet, može biti manji od broja koji smo zahtijevali!), ili -1 ako doće do greške.

Analogno su slične recv() i recvfrom(). Sintaksa funkcije recvfrom() je:

int recvfrom(int sockfd, void *buf, int len, unsigned int flags, struct sockaddr *from, int *fromlen);

Ponovo, baš kao i kod recv() sa dodatkom dva polja. from je pokazivač na strukturu struct sockaddr koja će biti popunjena IP adresom i portom mašine od koje primamo podatke. fromlen je pokazivač na lokalni int i trebao bi biti isprva podešen na sizeof(struct sockaddr). Kad se funkcija završi, fromlen će sadržati dužinu adrese smještene u from.

recvfrom() vraća broj primljenih bajtova, ili -1 pri grešci (a errno se postavlja na odgovarajuću vrijednost.)

Zapamti, ako se spojiš (connect()) preko datagram soketa, možeš koristiti obične send() i recv() funkcije. soket i dalje ostaje tipa datagram i paketi i dalje koriste UDP, ali soket-interfejs će automatski ubacivati u pakete informacije o odredišnoj adresi.

4.8. close() and shutdown() – Bježi mi s očiju!

Page 23: Vodič za mrežno programiranje Korištenje Internet soket-a

Uf! Slao si i primao informacije čitav dan, i dosta ti je toga. Spreman si da zatvoriš vezu. To je lako. Možeš jednostavno iskoristiti standardnu UNIX-ovu funkciju close() za zatvaranje datoteke:

close(sockfd);

Ovo će sprečiti sva daljnja zapisivanja i čitanja preko tog soketa. Svako ko pokuša sa jedne ili druge strane da piše ili čita – dobijaće poruku o grešci.

Ako poželiš malo više kontrole nad zatvaranjem soketa, možeš koristiti funkciju shutdown(). Ona ti dozvoljava da vezu prekineš u određenom smijeru, ili oba smijera (kao što radi close()). Sintaksa:

int shutdown(int sockfd, int how);

sockfd je soket deskriptor koji hoćeš dsa ugasiš, a how je nešto od sledećeg:

         0 – Sprečavaju se daljnja primanja podataka

         1 -- Sprečavaju se daljnja slanja podataka

         2 -- Sprečavaju se i daljnja slanja i daljnja primanja podataka (to takođe radi close())

shutdown() vraća 0 ako je uspio, i -1 ako je došlo do greške (errno – postavlja se na odgovarajuću vrijednost broja greške.)

Ako se usudiš da koristiš shutdown() na nepovezanom datagram soketu, onda na njemu više nećeš moći koristiti funkcije send() i recv() (sjeti se da ih možeš koristiti samo ako prvo koristiš connect() na tom datagram soketu.)

Važno je primijetiti da shutdown() ustvari ne zatvara soket-deskriptor – samo mijenja njegovu funkciju. Da bi oslobodio soket-deskriptor, moraš koristiti funkciju close().

Ništa drugo.

4.9. getpeername() – Ko si sad pa ti?

Ova funkcija je jako laka.

Toliko je laka da umalo da joj ne dam posebno poglavlje. Ali ipak evo je.

Funkcija getpeername() će ti reći ko je na drugom kraju spojenog stream soketa. Sintaksa:

#include <sys/socket.h> int getpeername(int sockfd, struct sockaddr *addr, int *addrlen);

Page 24: Vodič za mrežno programiranje Korištenje Internet soket-a

sockfd je soket-deskriptor spojenog stream soketa, addr je pokazivač na strukturu struct sockaddr (ili struct sockaddr_in) koja će sadržati informacije o adresi te druge strane, a addrlen je pokazivač na int, koji bi trebao biti na početku postavljen na sizeof(struct sockaddr).

Funkcija vraća -1 pri grešci i postavlja errno na odgovarajuću vrijednost.

Kad dobiješ tu adresu, možeš koristiti inet_ntoa() ili gethostbyaddr() radi više informacija. Ne, ne možeš saznati njegov nalog. (ok, ok, ako taj drugi računar ima "ident daemon", onda je moguće. Ipak, to je van domašaja ovog dokumenta. Pogledaj RFC-1413 ako te zanima više.)

4.10. gethostname() – Ko sam ja?

Ovo je još lakše nego getpeername(). Vraća ime računara na kom teče tvoj program. Ime može biti zatim korišteno u funkciji gethostbyname(), opisanoj ispod, da odrediš IP adresu svoje mašine.

Zar išta može biti zabavnije? Možda bih se mogao i sjetiti par stvari, ali ne bi bile vezane za soket-programiranje. U svakom slučaju, evo:

#include <unistd.h> int gethostname(char *hostname, size_t size);

Parametri su jednostavni: hostname je pokazivač na niz karaktera koji će sadržati ime, a size je veličina niza hostname u bajtovima.

Funkcija vraća 0 ako je bila uspješna, a -1 pri grešci, errno na odgovarajuću vrijednost, bla, bla....

4.11. DNS – Ti kažeš "bijelakuca.gov", ja kažem "198.137.240.92"

U slučaju da ne znaš šta je DNS, to je skraćenica za "Domain Name Service". Ukratko, kažeš mu neku običnu adresu sajta, a on tebi vrati IP adresu (pa je onda možeš koristiti sa funkcijama bind(), connect(), sendto(), ili za šta ti već treba.) Tako, ako neko unese:

$ telnet bijelakuca.gov

telnet zna da treba da se spoji (connect()) sa "198.137.240.92".

Ali kako to radi? Koristićeš funkciju gethostbyname():

#include <netdb.h> struct hostent *gethostbyname(const char *name);

Page 25: Vodič za mrežno programiranje Korištenje Internet soket-a

Kao što vidiš, vraća pokazivač na strukturu struct hostent, koja ima sledeću organizaciju:

struct hostent { char *h_name; char **h_aliases; int h_addrtype; int h_length; char **h_addr_list; }; #define h_addr h_addr_list[0]

Evo objašnjenja polja u strukturi struct hostent:

         h_name – Zvanično ime tog računara.

         h_aliases – Niz alternativnih imena za taj računar.

         h_addrtype – Tip adrese koja se vraća; obično AF_INET.

         h_length – Dužina adrese u bajtovima.

         h_addr_list – Niz mrežnih adresa za taj računar. Sve su u mrežnom uređenju bajtova.

         h_addr – Prva adresa u listi h_addr_list.

gethostbyname() vraća pokazivač na popunjenu strukturu struct hostent, ili NULL ako je došlo do greške. (Ali errno se ne modifikuje; umjesto njega, h_errno se podešava na odgovarajuću vrijednost. Pogledaj herror(), ispod.)

Ali kako se ova funkcija koristi? Ova funkcija je lakša za korištenje nego što se na prvi pogled čini.

Evo programa za primjer

/* ** getip.c – primjer programa za dosezanje imena računara */ #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <netdb.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> int main(int argc, char *argv[]) { struct hostent *h; if (argc != 2) { // provjeri da li je ispravna komandna linija fprintf(stderr,"korištenje: getip adresa\n");

Page 26: Vodič za mrežno programiranje Korištenje Internet soket-a

exit(1); } if ((h=gethostbyname(argv[1])) == NULL) { // daj informacije o računaru herror("gethostbyname"); exit(1); } printf("Host name : %s\n", h->h_name); printf("IP Address : %s\n", inet_ntoa(*((struct in_addr *)h->h_addr))); return 0; }

Sa funkcijom gethostbyname(), ne možeš koristiti perror() da odštampaš poruku o grešci (pošto se ne koristi errno). Umjesto toga, pozovi herror().

Prilično je direktno. Jednostavno proslijediš string koji predstavlja adresu ("bijelakuca.gov") funkciji gethostbyname(), a onda vadiš potrebne informacije iz strukture struct hostent.

5. Pozadina klijent-serveraOvo je svijet klijent-servera, lutko. Skoro sve na mreži se dešava kao komunikacija klijentskog procesa sa serverom i obrnuto. Uzmi telnet, na primjer. Kad se spojiš sa udaljenim serverom, na nekom portu, program na serveru (tzv. telnetd, server) oživi. On upravlja pridolazećim pozivima, daje ti odzivni znak, itd.

Slika 2. Razgovor između klijenta i servera.

Razmjena podataka između klijenta i servera se vidi na Slici 2.

Page 27: Vodič za mrežno programiranje Korištenje Internet soket-a

Primijeti da klijent-server par može da priča na SOCK_STREAM, SOCK_DGRAM, ili bilo kom drugom jeziku (dok god im je oboma isti jezik.) Neki primjeri klijent-server konverzacije su telnet/telnetd, ftp/ftpd, ili bootp/bootpd. Svaki put kad koristiš ftp, udaljeni program, ftpd, takođe radi – i uslužuje te.

Obično, postoji jedan serverski program na serverskom računaru, i taj server rukuje mnogobrojnim klijentima koristeći funkciju fork(). Osnovna procedura je: server čeka poziv, prima ga (accept()), i onda se grana (fork()) da bi novi proces obradio taj poziv. Upravo takvo nešto radi serverski program iz sledećeg poglavlja.

5.1. Primjer jednostavnog stream servera

Sve što ovaj server radi je da šalje tekst "Hello, World!\n" preko stream veze. Sve što je potrebno za testiranje ovog servera je da ga otvoriš u jednom prozoru, a onda se "telnetuješ" na njega iz drugog prozora:

$ telnet remotehostname 3490

gdje je remotehostname ime računara na kom radiš.

Kôd za serverski program: (Primjedba: Obrnuta kosa crta na kraju reda znači da se red nastavlja u sledeći.)

/* ** server.c -- stream socket server */ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/wait.h> #include <signal.h> #define MYPORT 3490 // port na koji će se posjetioci povezivati #define BACKLOG 10 // koliko pridolazećih poziva može da bude u redu za čekanje void sigchld_handler(int s) { while(wait(NULL) > 0); } int main(void) { int sockfd, new_fd; // čekati na sock_fd, vezu primiti na new_fd struct sockaddr_in my_addr; // informacije o mojoj adresi struct sockaddr_in their_addr; // informacije o adresi onog ko zove int sin_size; struct sigaction sa;

Page 28: Vodič za mrežno programiranje Korištenje Internet soket-a

int yes=1; if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { perror("socket"); exit(1); } if (setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&yes,sizeof(int)) == -1) { perror("setsockopt"); exit(1); } my_addr.sin_family = AF_INET; // serversko uređenje bajtova my_addr.sin_port = htons(MYPORT); // kratko, mrežno uređenje bajtova my_addr.sin_addr.s_addr = INADDR_ANY; // automatski popuni mojim IP-om memset(&(my_addr.sin_zero), '\0', 8); // ostatak strukture popuni nulama if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)) == -1) { perror("bind"); exit(1); } if (listen(sockfd, BACKLOG) == -1) { perror("listen"); exit(1); } sa.sa_handler = sigchld_handler; // pokupi mrtve procese sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; if (sigaction(SIGCHLD, &sa, NULL) == -1) { perror("sigaction"); exit(1); } while(1) { // glavna petlja sa funkcijom accept() sin_size = sizeof(struct sockaddr_in); if ((new_fd = accept(sockfd, (struct sockaddr *)&their_addr, &sin_size)) == -1) { perror("accept"); continue; } printf("server: got connection from %s\n", inet_ntoa(their_addr.sin_addr)); if (!fork()) { // ovo je dijete-proces close(sockfd); // djetetu ne treba soket koji očekuje poziv if (send(new_fd, "Hello, world!\n", 14, 0) == -1) perror("send"); close(new_fd); exit(0); } close(new_fd); // roditelju ovo ne treba }

Page 29: Vodič za mrežno programiranje Korištenje Internet soket-a

return 0; }

U slučaju da si radoznao, ovo je kôd jedne velike funkcije main(), čisto radi sintaksne čistoće. Ti slobodno možeš da je razbiješ u manje funkcije.

(Takođe, može biti da ti je čitava priča o funkciji sigaction() pomalo nova – to je ok. Kôd koji tu stoji je odgovoran da počisti sve zombi-procese koji se pojavljuju kad god se neko od djece-procesa završi. Ako ostavljaš mnogo zombija za sobom i ne čistiš ih, administrator će da se ljuti.)

Možeš da komuniciraš sa ovim serverom pomoću klijenta u sledećoj sekciji.

5.2. Jednostavan stream klijent

Ovaj je program lakši od ovog gore servera. Sve što ovaj klijent radi je da se spoji sa računarom zadatim na komandnoj liniji, na portu 3490. Prima tekst koji pošalje server.

Izvorni kôd za klijentski program:

/* ** client.c – Primjer stream soket klijenta */ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <netdb.h> #include <sys/types.h> #include <netinet/in.h> #include <sys/socket.h> #define PORT 3490 // port na koji će se klijent povezati #define MAXDATASIZE 100 // najveći broj bajtova koji se može primiti odjednom int main(int argc, char *argv[]) { int sockfd, numbytes; char buf[MAXDATASIZE]; struct hostent *he; struct sockaddr_in their_addr; // adresa računara na koji se spajamo if (argc != 2) { fprintf(stderr,"usage: client hostname\n"); exit(1); } if ((he=gethostbyname(argv[1])) == NULL) { // daj podatke o serverskom računaru perror("gethostbyname"); exit(1); } if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { perror("socket"); exit(1);

Page 30: Vodič za mrežno programiranje Korištenje Internet soket-a

} their_addr.sin_family = AF_INET; // serversko ureženje bajtova their_addr.sin_port = htons(PORT); // kratko, mrežno uređenje bajtova their_addr.sin_addr = *((struct in_addr *)he->h_addr); memset(&(their_addr.sin_zero), '\0', 8); // ostatak strukture popuni nulama if (connect(sockfd, (struct sockaddr *)&their_addr, sizeof(struct sockaddr)) == -1) { perror("connect"); exit(1); } if ((numbytes=recv(sockfd, buf, MAXDATASIZE-1, 0)) == -1) { perror("recv"); exit(1); } buf[numbytes] = '\0'; printf("Received: %s",buf); close(sockfd); return 0; }

Primijeti da, ako ne pokreneš server prije klijenta, funkcija connect() vraća "Connection refused (Spajanje odbijeno)". Veoma korisno.

5.3. Datagram soketi

Nemam o ovoj temi mnogo da pričam, pa ću samo predstaviti par programa radi primjera: talker.c i listener.c.

listener ("slušalac") čeka paket koji će da stigne na port 4950. talker ("govornik") šalje paket na taj port, na datu mašinu, a taj paket sadrži sve što korisnik stavi na komandnu liniju.

Evo izvornog kôda za listener.c:

/* ** listener.c – primjer datagram-soket servera */ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #define MYPORT 4950 // port na koji će se povezivati korisnici #define MAXBUFLEN 100 int main(void) { int sockfd; struct sockaddr_in my_addr; // informacije o mojoj adresi

Page 31: Vodič za mrežno programiranje Korištenje Internet soket-a

struct sockaddr_in their_addr; // informacije o adresi računara koji nas zove int addr_len, numbytes; char buf[MAXBUFLEN]; if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) { perror("socket"); exit(1); } my_addr.sin_family = AF_INET; // serversko uređenje bajtova my_addr.sin_port = htons(MYPORT); // kratko, mrežno uređenje bajtova my_addr.sin_addr.s_addr = INADDR_ANY; // automatski popuni mojom IP adresom memset(&(my_addr.sin_zero), '\0', 8); // ostatak strukture popuni nulama if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)) == -1) { perror("bind"); exit(1); } addr_len = sizeof(struct sockaddr); if ((numbytes=recvfrom(sockfd,buf, MAXBUFLEN-1, 0, (struct sockaddr *)&their_addr, &addr_len)) == -1) { perror("recvfrom"); exit(1); } printf("dobij paket od %s\n",inet_ntoa(their_addr.sin_addr)); printf("paket je %d bajtova dug\n",numbytes); buf[numbytes] = '\0'; printf("paket sadrži \"%s\"\n",buf); close(sockfd); return 0; }

Primijeti da pri pozivu funkcije socket() konačno koristimo SOCK_DGRAM. Takođe, vidiš li da nema potrebe za funkcijama listen() ili accept(). Ovo je jedna od super stvari kod nepovezanih datagram soketa!

Sad ide izvorni kôd za talker.c:

/* ** talker.c – primjer datagram klijenta */ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <netdb.h>

Page 32: Vodič za mrežno programiranje Korištenje Internet soket-a

#define MYPORT 4950 // port na koji će se korisnici povezivati int main(int argc, char *argv[]) { int sockfd; struct sockaddr_in their_addr; // informacije o adresi računara kojeg zovemo struct hostent *he; int numbytes; if (argc != 3) { fprintf(stderr,"korištenje: talker računar poruka\n"); exit(1); } if ((he=gethostbyname(argv[1])) == NULL) { // daj info za računar koji zovemo perror("gethostbyname"); exit(1); } if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1) { perror("socket"); exit(1); } their_addr.sin_family = AF_INET; // serversko uređenje bajtova their_addr.sin_port = htons(MYPORT); // kratko, mrežno urećenje bajtova their_addr.sin_addr = *((struct in_addr *)he->h_addr); memset(&(their_addr.sin_zero), '\0', 8); // ostatak strukture popuni nulama if ((numbytes=sendto(sockfd, argv[2], strlen(argv[2]), 0, (struct sockaddr *)&their_addr, sizeof(struct sockaddr))) == -1) { perror("sendto"); exit(1); } printf("sent %d bytes to %s\n", numbytes, inet_ntoa(their_addr.sin_addr)); close(sockfd); return 0; }

I to je sve! Pokreni listener na nekoj mašini, onda pokreni talker na drugoj. Pogledaj kako komuniciraju! E zar nije super stvar?!

Moram samo da dodam nešto vezano za ovu temu, za datagram sokete, pošto je ovo poglavlje vezano za njih. Recimo da talker pozove connect() i odredi adresu listener-a. Odatle pa nadalje, talker može samo da šalje i prima podatke sa adrese određene funkcijom connect(). Zbog ovoga, ne moraš da koristiš sendto() i recvfrom(); možeš slobodno koristiti samo send() i recv().

Page 33: Vodič za mrežno programiranje Korištenje Internet soket-a

6. Nešto naprednije tehnikeNisu ovo stvarno napredne tehnike, ali idu malo dalje od dosadašnjeg nivoa učenja. Ustvari, ako si došao dovde, treba da znaš da se možeš smatrati da si završio osnove mrežnog programiranja. Svaka čast!

I evo nas idemo u svijet tajnih mogućnosti UNIX-ovih soketa. Izvoli!

6.1. Blokiranje

Blokiranje. Čuo si za to – ali šta je to? U UNIX-u, "blokirati" znači "spavati". Vjerovatno si primijetio da, kad pokreneš program listener, u prethodnom poglavlju, da on čeka dok ne stigne neki paket. Šta se desilo? Pozvao je funkciju recvfrom(), a podaci nisu dolazili, tako da je recvfrom() "blokirao" (zaspao) čekajući da podaci stignu.

Gomila funkcija blokira. accept() blokira. Sve recv() funkcije blokiraju. Funkcijama se može reći da ne blokiraju u takvim situacijama. Kad tek napraviš soket, i imaš njegov soket-deskriptor, jezgro mu kaže da blokira. Ako hoćeš da to ne radi, onda pozoveš funkciju fcntl():

#include <unistd.h> #include <fcntl.h> . . sockfd = socket(AF_INET, SOCK_STREAM, 0); fcntl(sockfd, F_SETFL, O_NONBLOCK); . .

Kad podesiš soket da ne blokira, možeš bezbijedno napraviti petlju u čijem bi svakom obrtu pokušavao čitati iz tog soketa; pokušaš da čitaš, podataka nema – funkcija vraća -1 i podešava errno na EWOULDBLOCK.

Uopšteno govoreći, ovakvo učitavanje iz bilo kakvog fajl-deskriptora je loša ideja. Ako ostaviš program da ovako očekuje podatke, iscrpšće svu snagu procesora. Elegantniji način provjere da li ima podataka koji čekaju učitavanje je prikazano u sledećoj sekciji koja se tiče funkcije select().

6.2. select() – sinhrono (istovremeno, paralelno (prim. prev.) U/I multipleksiranje

Ova funkcija je malo ;udna, ali je jak korisna. Pazi ovu situaciju: imaš server za koji hoćeš da čeka pridolazeće pozive, i istovremeno čita podatke iz već spojenih.

Nema problema, kažeš, samo funkcija accept() i par funkcija recv(). Stani, štetočino! Šta ako ti poziv accept() blokira? Kako ćeš primati (recv()) podatke dok ovaj blokira? "E pa koristiću

Page 34: Vodič za mrežno programiranje Korištenje Internet soket-a

sokete kojima ću reći da ne blokiraju!" E pa nećeš! Ne želiš da oduzimaš snagu procesora. Šta sad?

select() ti daje moć da posmatraš više soketa odjednom. Reći će ti koji su spremni za čitanje, koji su spremni za pisanje, i kod kojih soketa je došlo do izuzetka, ako te to baš zanima.

Bez daljeg zadržavanja, evo sintakse funkcije 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);

Ova funkcija posmatra skupove fajl-deskritpra; konkretno readfds, writefds, i exceptfds. Ako želiš da znaš da li možeš čitati sa standardnog ulaza i nekog soketa, čiji je soket-deskriptor sockfd, dodaš fajl-deskriptor 0 i soket-deskriptor sockfd skupu readfds. Parametar numfds treba da bude podešen na vrijednost najvišeg fajl-deskriptora plus jedan. U ovom primjeru, to treba da bude sockfd+1, pošto je sockfd sigurno veći od vrijednosti fajl-deskriptora za standardni ulaz (0).

Kad se funkcija select() završi, readfds će se promijeniti da bi prikazao koji od fajl-deskriptora je spreman za čitanje. Možeš ih provjeriti makroom FD_ISSET(), objašnjenom ispod.

Dok ne krenemo dalje, reći đu malo o tome kako rukovati ovim skupovima. Svaki skup je promjenjiva tipa fd_set. Sledeći makroi rade nad promjenjivama ovog tipa:

         FD_ZERO(fd_set *set) – isprazni skup

         FD_SET(int fd, fd_set *set) – dodaje fd skupu

         FD_CLR(int fd, fd_set *set) – briše fd iz skupa

         FD_ISSET(int fd, fd_set *set) – provjerava da li je fd u skupu

I na kraju, šta je ova glupa struktura struct timeval? Pa, desi se da ne želiš yauivijek da čekaš da ti stignu podaci. Možda želiš svakih 96 sekundi da odštampaš na ekran poruku "Još čekam..." jer se još ništa ne dešava. Ova struktura ti omogućava da definišeš vremenski period. Ako dato vrijeme prođe, a funkcija select() još nije našla nikakve spremne fajl-deskriptore, onda će se završiti.

Struktura struct timeval ima sledeća polja:

struct timeval { int tv_sec; // sekunde int tv_usec; // mikrosekunde };

Page 35: Vodič za mrežno programiranje Korištenje Internet soket-a

Podesi tv_sec nba broj sekundi koje će se čekati da prođu, i tv_usec na broj mikrosekundi koje će se čekati. Da, mikrosekundi, ne milisekundi. Ima 1,000 mikrosekundi u milisekundi, a 1,000 milisekundi u mikrosekundi. Tako, ima 1,000,000 mikrosekundi u sekundi. Zašto se zove "usec"? "u" liči na grčko slovo μ (mi) koje koristimo za "mikro". Takođe, kad se funkcija select() završi, možda se struktura struct timeval timeout promijeni da prikaže koliko je bilo još vremena ostalo. Ovo zavisi od UNIX-a koji koristite.

Jao! Imamo časovnik rezolucije tako male da broji mikrosekunde! Ne računaj na njega. Standardni UNIX-ov časovnik broji po oko 100 milisekundi, pa ćeš vjerovatno čekati toliko bez obzira koliko malu vrijednost podesio u promjenjivoj tipa struct timeval.

Još nešto zanimljivo: Ako podesiš sve vrijednosti u strukturi struct timeval na 0, select() će odmah izaći, uspješno provjeravajući fajl-deskriptore u skupovima. Ako postaviš parametar funkcije select() timeout na NULL, onda ona neće mariti za vrijeme, i čekaće dok god nije nijedan fajl-deskriptor spreman. I na kraju, ako te nije briga za neki od skupova, samo proslijedi odgovarajući parametar kao NULL, pri pozivu funkcije select().

U sledećem kôdu se čeka 2.5 sekundi da se nešto pojavi na standardnom ulazu:

/* ** select.c – primjer programa koji sadrži funkciju select() */ #include <stdio.h> #include <sys/time.h> #include <sys/types.h> #include <unistd.h> #define STDIN 0 // fajl-deskriptor za standardni ulaz int main(void) { struct timeval tv; fd_set readfds; tv.tv_sec = 2; tv.tv_usec = 500000; FD_ZERO(&readfds); FD_SET(STDIN, &readfds); // nije te briga za writefds i exceptfds: select(STDIN+1, &readfds, NULL, NULL, &tv); if (FD_ISSET(STDIN, &readfds)) printf("Taster je pritisnut!\n"); else printf("Isteklo vrijeme.\n"); return 0; }

Ako imaš terminal koji učitava liniju po liniju, moraš pritisnuti taster RETURN ili će vrijeme isteći.

E sad, neki mogu da misle da je ovo super način da se čekaju podaci na datagram soketu – i u pravu su: mogao bi biti. Neki UNIX sistemi koriste select() na ovaj način, neki ne. Trebalo bi da pogledaš man strane prije nego išta probaš.

Page 36: Vodič za mrežno programiranje Korištenje Internet soket-a

Neki UNIX sistemi ažuriraju vrijeme u strukturi struct timeval da prikažu koliko je još vremena bilo ostalo do kraja. Ali neki ne ažuriraju. Nemoj da se oslanjaš na pretpostavku ako želiš imati prenosive programe. (Koristi gettimeofday() ako ti treba da znaš koliko je vremena prošlo. Bruka, znam, ail tako je.)

Šta se desi ako se zatvori veza na soketu koji je u skupu za čekanje? Pa u tom slučaju se funkcija select() završava kao da je taj soket spreman za čitanje. I kad onda pokušaš da čitaš iz njega, pomoću funkcije recv(), recv() vraća 0. I tek tada znaš da je klijent zatvorio vezu.

I joč jedan komentar za select(): Ak imaš soket na kome čekaš (listen()), možeš provjeravati da li ima novih poziva na njemu smještajuđi njegov fajl-deskriptor u skup readfds.

I to je, prijatelju moj, kratak pregled svemoćne funkcije select().

Ali, evo i velikog primjera. Pročitaj ovaj primjer, a zatim njegov opis, ispod.

Ovaj program se ponaša kao jednostavan višekorisnički server za razgovor (chat). Pokreni ga u jednom prozoru, onda se spoji na njega pomoću telnet-a ("telnet hostname 9034") iz par drugih prozora. Kad odštampaš neki tekst u jednom primjerku telnet-a, trebalo bi da se pojavi i u ostalim.

/* ** selectserver.c – mali višekorisnički chat-server */ #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 // port na kom čekamo int main(void) { fd_set master; // glavni skup fajl-deskriptora fd_set read_fds; // privremeni skup fajl-deskriptora za select() struct sockaddr_in myaddr; // adresa servera struct sockaddr_in remoteaddr; // adresa klijenta int fdmax; // maksimalni broj fajl-deskriptora int listener; // soket koji čeka pozive int newfd; // soket-deskriptor koji predstavlja novu vezu, upravo primljenu (accept()) char buf[256]; // za skladištenje podataka koje šalje klijent int nbytes; int yes=1; // za setsockopt() SO_REUSEADDR, ispod int addrlen; int i, j; FD_ZERO(&master); // isprazni privremeni i glavni skup fajl-deskriptora FD_ZERO(&read_fds); // get the listener

Page 37: Vodič za mrežno programiranje Korištenje Internet soket-a

if ((listener = socket(AF_INET, SOCK_STREAM, 0)) == -1) { perror("socket"); exit(1); } // onemogući poruku "adresa već u upotrebi" 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); } // dodaj soket-deskritpr na kom se čeka – glavnom skupu FD_SET(listener, &master); // pamti koji je najveći fajl-deskriptor fdmax = listener; // dosad je to ovaj // main loop for(;;) { read_fds = master; // kopiraj ga if (select(fdmax+1, &read_fds, NULL, NULL, NULL) == -1) { perror("select"); exit(1); } // idi kroz sve povezane sokete i vidi da li je ko poslao kakve podatke for(i = 0; i <= fdmax; i++) { if (FD_ISSET(i, &read_fds)) { // evo ih!! if (i == listener) { // obradi nove pozive addrlen = sizeof(remoteaddr); if ((newfd = accept(listener, (struct sockaddr *)&remoteaddr, &addrlen)) == -1) { perror("accept"); } else { FD_SET(newfd, &master); // dodaj ga glavnom skupu if (newfd > fdmax) { // pamti najveći fajl-deskriptor fdmax = newfd; } printf("selectserver: nova veza od %s na soketu",

Page 38: Vodič za mrežno programiranje Korištenje Internet soket-a

"%d\n", inet_ntoa(remoteaddr.sin_addr), newfd); } } else { // obradi podatke od klijenta if ((nbytes = recv(i, buf, sizeof(buf), 0)) <= 0) { // došlo je do greške ili je klijent zatvorio vezu if (nbytes == 0) { // zatvorena veza printf("selectserver: socket %d hung up\n", i); } else { perror("recv"); } close(i); // ćao! FD_CLR(i, &master); // izbriši iz glavnog skupa } else { // dobili smo podatke od klijenta for(j = 0; j <= fdmax; j++) { // šalji svima!... if (FD_ISSET(j, &master)) { // ...osim nama i onome ko je poslao if (j != listener && j != i) { if (send(j, buf, nbytes, 0) == -1) { perror("send"); } } } } } } // kako je ovo RUŽNO!!! } } } return 0; }

Primijeti da imam dva skupa fajl-deskriptora: master i read_fds. Prvi, master, čuva sve soket-deskriptore koji su trenutno spojeni, kao i soket-deskriptor koji čeka nove pozive.

Razlog što imam master skup je što select() ustvari mijenja skup koji mu proslijediš, da prikaže koji su soketi spremni za čitanje. Pošto hoću da pratim sve veze ("konekcije") od jednog poziva funkcije of select() do drugog, moram da ih skladištim negdje gdje će biti bezbijedne. U zadnjem trenutku iskopiram master skup u read_fds, i onda s njim pozovem funkciju select().

Ali zar ovo ne znači da, svaki put kad ostvarim novu vezu, moram da je dodam u skup master? Da! I svaki put kad se neka veza zatvori, moram da je izbrišem iz skupa master? Da, to znači to.

Primijeti da provjeravam da li je soket listener spreman za čitanje. Kad je spreman, to znači da imam novi poziv u listi za čekanje, i onda pozivam funkciju accept() i dodajem tu novu vezu

Page 39: Vodič za mrežno programiranje Korištenje Internet soket-a

skupu master. Slično, kad je veza sa klijentom spremna za čitanje, a recv() vrati 0, to znači da je veza zatvorena, i moram da je uklonim iz skupa master.

Ako recv() tada vrati nešto različito od nule, to znači da se neki podaci primljeni. Tada ih preuzmem, i prođem kroz listu master da svim ostalim pošaljem te podatke.

I to je, prijatelju moj, krajnje-prost pregled svemoćne funkcije select().

6.3. Rukovanje parcijalnim send() funkcijama

Prisjeti se, još iz sekcije o komandi send(), iznad, kad sam rekao da send() može nekad poslati manje bajtova nego što si mu rekao? Znači, ti mu kažeš da pošalje 512 bajtova, a on pošalje 412. Šta se desilo sa ostalih 100 bajtova?

Pa, oni se još uvijek nalaze u tvom malom skladištu podataka (nekom nizu isl.) i čekaju da se pošalju. Zbog nekih stvari van tvoje kontrole, jezgro je odlučilo da ne pošalje sve podatke u jednom zalogaju, i sad je na tebi, prijatelju, da ponovo pošalješ ostatak koji nije već poslat

Mogao bi da napišeš ovakvu funkciju:

#include <sys/types.h> #include <sys/socket.h> int sendall(int s, char *buf, int *len) { int total = 0; // koliko bajtova si poslao int bytesleft = *len; // koliko nam je preostalo da pošaljemo int n; while(total < *len) { n = send(s, buf+total, bytesleft, 0); if (n == -1) { break; } total += n; bytesleft -= n; } *len = total; // broj bajtova koji je zapravi poslat return n==-1?-1:0; // vrati -1 ako nisi uspio, 0 ako jesi }

U ovom primjeru, s je soket na koji hoćeš da pošalješ podatke, buf je skladište sa podacima, a len je pokazivač na cio broj koji predstavlja broj podataka koji se nalaze u tom skladištu.

Funkcija vraća -1 ako je došlo do greške (a errno se samo podešava funkcijom send().) Takođe, broj bajtova koji su poslati se vidi u len. Ovo će biti isti onaj broj koji si zatražio da se toliko podataka pošalje, osim ako je došlo do greške. sendall() će uraditi sve što može, iz sve snage, da pošalje podatke, ali ako dođe do greške, odmah izlazi.

Radi cjelovitosti, evo primjera poziva funkcije:

char buf[10] = "Beej!";

Page 40: Vodič za mrežno programiranje Korištenje Internet soket-a

int len; len = strlen(buf); if (sendall(s, buf, &len) == -1) { perror("sendall"); printf("Poslao sam samo %d bajtova jer je došlo do greške!\n", len); }

Šta se dešava sa druge strane, tamo gdje se podaci primaju? Ako su paketi promjenjive dužine, kako primaoc zna kad se završava a kad započinje neki paket? Da, da, u pravom svijetu je mnogo bola. Trebao bi da enkapsuliraš (prisjeti se toga iz sekcije o enkapsulaciji?) Čitaj dalje ako te zanimaju detalji!

6.4. O enkapsulaciji podataka

Šta to uopšte znaši enkapsulirati podatke? Najjednostavnije rečeno, postaviš zaglavlje sa dužinom paketa ili identifikacijom, ili oboje.

Kako bi trebalo izgledati zaglavlje? Pa, to su samo neki binarni podaci koji predstavljaju što god treba da predstavljaju da bi imao završen projekat.

Uh! To je prilično neodređeno.

Dobro. Na primjer, recimo da imaš višekorisnički program za razgovaranje (chat) koje koristi SOCK_STREAM. Kad korisnik nešto otkuca, dva špdatka treba da budu poslata serveru: šta je otkucano, i ko je otkucao.

Zasad dobro? "U čemu je problem?", pitaš.

Problem je što poruke mogu biti različite dužine. Osoba pod imanom "Toma" može reći, "Zdravo", a neka druga osoba "Pera" može reći, "Hej ljudi, šta ima?"

Eh, i sad ti šalješ klijentima sve podatke kako dolaze sa raznih strana. To što šalješ izgleda ovako:

t o ma Z d r a v o P e r a H e j , l j u d i , š t a i m a ?

I tako dalje. Kako klijent da zna kad koji paket počinje i završava se? Mogao bi, ako bi htio, učiniti sve poruke iste dužine i slati ih funkcijom sendall(),iznad. Ali to je suludo! Nećemo valjda slati (send()) 1024 bajta samo zato što je "Toma" rekao "Zdravo".

Tako mi enkapsuliramo podatke u majušno zaglavlje i zapakuhjemo strukturu. I server i klijent zanju kako otpakovati i kako zapakovati podatke. Nemoj sad da gledaš, upravo definišemo sopstveni protokol kako klijent i server komuniciraju!

U ovom slučaju, pretpostavimo da je ime korisnika fiksne dužine od, recimo, 8 karaktera i završava se znakom '\0'. A onda pretpostavimo da je poruka koju korisnik pošalje promjenjive dužine, najviše 128 karaktera. Pogledajmo primjer strukture koja bi predstavljala paket:

Page 41: Vodič za mrežno programiranje Korištenje Internet soket-a

1.      len (1 bajt, neoznačeni) – ukupna dužina paketa, brojeći i ime korisnika i tekst poruke.

2.      name (8 bajtova) – ime korisnika, koje se završava znakom '\0'.

3.      chatdata (n-bajtova) – tekst poruke, ne duži od 128 bajtova. Dužina paketa se broji kao broj bajtova ovde plus osam.

Zašto sam izabrao ograničenja od 8 i 128 bajtova? Izvukao sam ih iz rukava, uz pretpostavku da su sasvim dovoljna. Možda će nekome biti preuska, pa može imati polja od 30 i 1000 znakova. Tvoj izbor.

Koristećo gornju definiciju paketa, prvi paket bi se sastojao od sledećih podataka (heksadecimalno i tekstualno):

0B 54 6F 6D 61 00 00 00 00 5A 64 72 61 76 6F (dužina) T o m a \0 Z d r a v o

a drugi bi bio sličan:

17 50 65 72 61 48 65 6A 20 6C 6A 75 64 69 2C 20 5B 74 61 20 69 6D 61 3F (dužina) P e r a H e j l j u d i , š t a i m a ?

(Podatak o dužini je u mrežnom uređenju podataka, naravno. U ovom slučaju, u pitanju je samo jedan bajt pa nema veze, ali generalno govoreći svi cijeli brojevi u paketima treba da budu u mrežnom uređenju podataka.)

Kad šalješ podatke, treba da budeš siguran te ih šalješ funkcijom sendall(), iznad, pa da znaš da su svi podaci otišli, pa čak i ako to zahtijeva nekoliko poziva funkcije send().

Isto tako, kad primaš podatke, treba da uradiš nešto više posla. Da budeš siguran, treba da pretpostaviš da mogu stići djelimični podaci. Treba pozivati recv() sve dok ne stignu svi podaci.

Ali kako? Pa znamo koliko ukupno bajtova treba da primimo da bi paket bio cjelovit, jer je taj broj nalijepljen na prednji di paketa. Takođe znamo da je maksimaldna dužina paketa 1+8+128, ili 137 bajtova (jer smo tako sami definisali paket.)

Možeš da napraviš niz dovoljno veliki da primi dva paketa. To će ti biti radni niz i tu ćeš rekonstruisati pakete kako budu pristizali.

Svaki put kad primiš (recv()) podatke, stavljećeš ih u radni niz i provjeravati da li je stigao cio paket. Dakle, broj bajtova u nizu je veći ili jednak dužini napisanoj u zaglavlju (+1, jer dužina ne broji i onaj bajt u kom se sama dužina skladišti.) Ako je dužina niza manja od 1, paket nije cjelovit, očito. Za takvu situaciju moraš obraditi poseban slučaj, pošto se ne možeš osloniti na samo jedan bajt.

Page 42: Vodič za mrežno programiranje Korištenje Internet soket-a

Kad je paket gotov, možeš s njim da radiš šta god hoćeš. Koristi ga, i izbaci ga iz radnog niza.

Uf! Miješa li ti se u glavi? E pa evo još jedan od dva udarca: Može se desiti da pročitaš preko kraja prvog paketa i komad sledećeg paketa u samo jednom pozivu funkcije recv(). To znači, imaš radni paket sa jednim cijelim paketom, i komadom sledećeg paketa! Do vraga. (Ali zato i jesi napravio paket dovoljno velik da sadrži dva paketa – za slučaj da se ovo desi!)

Pošto znaš dužinu prvog paketa – iz zaglavlja, a i pratio si koliko bajtova imaš u nizu, možeš izračunati koliko bajtova u nizu pripada drugom (nekompletnom) paketu. Kad si obradio prvi paket, možeš da ga ukloniš iz radnog niza i drugi pmjeriš na njegov početak. Tada možeš krenuti sa novim pozivom funkcije recv().

(Neki od čitalaca će primijetiti da pomjeranje komada drugog paketa oduzima vrijeme, i da bi bilo bolje napraviti neku cikličnu skladišnu strukturu. Diskusija o cikličnim strukturama je van dosega ovog dokumenta, nažalost. Ako si radoznao, čitaj o tome iz knjiga.)

Nisam ni rekao da je lako. Pa dobro, rekao sam. I jeste lako; treba ti samo dosta vježbe i uskoro će ti samo doći. Kunem se :)!

7. Više podataka o ovoj temiDošao si dovde i sad moliš za još! Gdje možeš još da odeš da naučiš još više o ovoj temi?

7.1. man stranice

Za početak, probaj ove man stranice:

• htonl()• htons()• ntohl()• ntohs()• inet_aton()• inet_addr()• inet_ntoa()• socket()• socket options• bind()• connect()• listen()• accept()• send()• recv()• sendto()• recvfrom()

Page 43: Vodič za mrežno programiranje Korištenje Internet soket-a

• close()• shutdown()• getpeername()• getsockname()• gethostbyname()• gethostbyaddr()• getprotobyname()• fcntl()• select()• perror()• gettimeofday()

7.2. Knjige

Ako ti trebaju neke prave papirne knjige da ih držiš u ruci i učiš iz njih, probaj neke od narednih. Pogledaj istaknut logo sajta Amazon.com. Šta znači ovaj besramni komercijalizam? E pa dobijam pare (Amazon.com ustvari smješta kredit) za prodavanje njihovih knjiga kroz ovaj vodič. Tako da, ako ćete već da naručujete neke od ovih knjiga, zašto mi ne biste poslali specijalnu zahvalnost počinjući žurku sa nekog od donjih linkova.

Na kraju krajeva, i vama će služiti više nego dobro sve te knjige.

Unix Network Programming, volumes 1-2 by W. Richard Stevens. Published by Prentice Hall. ISBNs for volumes 1-2: 013490012X, 0130810819.

Internetworking with TCP/IP, volumes I-III by Douglas E. Comer and David L. Stevens. Published by Prentice Hall. ISBNs for volumes I, II, and III: 0130183806, 0139738436, 0138487146.

TCP/IP Illustrated, volumes 1-3 by W. Richard Stevens and Gary R. Wright. Published by Addison Wesley. ISBNs for volumes 1, 2, and 3: 0201633469, 020163354X, 0201634953.

TCP/IP Network Administration by Craig Hunt. Published by O'Reilly & Associates, Inc. ISBN 1565923227.

Advanced Programming in the UNIX Environment by W. Richard Stevens. Published by Addison Wesley. ISBN 0201563177.

Using C on the UNIX System by David A. Curry. Published by O'Reilly & Associates, Inc. ISBN 0937175234. Out of print.

Page 44: Vodič za mrežno programiranje Korištenje Internet soket-a

7.3. Priručnici na mreži

Na mreži:

BSD Sockets: A Quick And Dirty Primer (ima i ostale informacije uopšteno o UNIX programiranju!)

The Unix Socket FAQ

Client-Server Computing

Intro to TCP/IP

Internet Protocol Frequently Asked Questions

The Winsock FAQ

7.4. RFC-i

RFC-i – the real dirt:

RFC-768 – The User Datagram Protocol (UDP)

RFC-791 – The Internet Protocol (IP)

RFC-793 – The Transmission Control Protocol (TCP)

RFC-854 – The Telnet Protocol

RFC-951 – The Bootstrap Protocol (BOOTP)

RFC-1350 – The Trivial File Transfer Protocol (TFTP)

8. Često postavljena pitanjaQ: Gdje da nađem sve te .h datoteke koje mi trebaju?Q: Šta da radim kad mi bind() odgovori "Adresa već u upotrebi"? Q: Kako da dobijem listu otvoreih soketa na sistemu?Q: Kako da pregledam tabelu usmjeravanja?

Page 45: Vodič za mrežno programiranje Korištenje Internet soket-a

Q: Kako da pišem klijent-server programe kad imam samo jedan računar? Zar mi ne treba mreža za mrežne programe?Q: Kako da saznam da li je s druge strane zatvorena veza?Q: Kako sam da napravim "ping"? Šta je ICMP? Gdje da nađem još informacija o "sirovim soketima" i SOCK_RAW ? Q: Kako da pravim ove programe u Windows-u?Q: Kako da pravim ove programe za Solaris/SunOS? Stalno dobijam greške pri povezivanju izvrpne datoteke!Q: Zašto mi select() stalno pada na signalu? Q: Kako da ostvarim tajmaut pri pozivu funkcije recv() ? Q: Kako da šifriram ili kompresujem podatke prije nego ih pošaljem kroz soket?Q: Šta je taj "PF_INET " koji stalno viđam? Jel' ima veze sa "AF_INET"?Q: Kako da napravim server koji prima komande ljuske od klijenta, i izvršava ih?Q: Šaljem gomilu podataka, ali kad ih primam pomoću recv() , primim stalno samo po 536 bajtova ili 1460 bajtova. Ali ako pokrenem program lokalno, primim sve podatke odjednom. Šta se dešava?Q: Imam Windows, pa nemam fork() , i nemam nikakav struct sigaction . Šta da radim? Q: Kako da pošaljem podatke sigurnom vezom, pomođu TCP/IP koristeći enkripciju?Q: Koristim firewall – kako da pustim ljude izvan njega da znaju moju IP adresu pa da se spoje na moju mašinu?

Q: Gdje da nađem sve te .h datoteke koje mi trebaju?

A: Ako ih već nemaš instalirane, vjerovatno ti i ne trebaju. Provjeri man stranice u vezi svoje platforme. Ako radiš u Windows-u, treba ti samo #include <winsock.h>.

Q: Šta da radim kad mi bind() odgovori "Adresa već u upotrebi"?

A: Treba ti setsockopt() sa uključenom opcijom SO_REUSEADDR da ga primijeniš na soketu koji čeka pozive. Pogledaj poglavlje bind() i poglavlje select() ako ti trebaju primjeri.

Q: Kako da dobijem listu otvorenih soketa na sistemu?

A: Koristi netstat. Pogledaj man stranice za detalje, ali možeš dosta stvari shvatiti i samo kucanjem:

$ netstat

Jedino je problem odrediti koji soket je povezan sa kojim programom. :-)

Q: Kako da pregledam tabelu usmjeravanja?

A: Koristi komandu route (/sbin/route na većini Linux sistema) ili netstat -r.

Q: Kako da pišem klijent-server programe kad imam samo jedan računar? Zar mi ne treba mreža za mrežne programe?

Page 46: Vodič za mrežno programiranje Korištenje Internet soket-a

A: Srećom po tebe, svi računari imaju virtuelni povratni mrežni "uređaj" koji čuči u jezgru i pravi se da je mrežna kartica. (Ovo je interfejs koji je predstavljen kao "lo" u tabeli usmjeravanja.)

Zamislimo da smo prijavljeni na računar pod nazivom "goat". Pokreni klijent u jednom prozoru i server u drugom. Ili možeš i da pokreneš server u pozadini ("server &") i onda u istom prozoru i klijent. Rezultat je da možeš da se spojiš na goat ili localhost (pošto je "localhost" vjerovatno definisan u datoteci /etc/hosts) i imaćeš razgovor klijenta i servera bez mreže!

Ukratko, nema potrebe vršiti ikakve izmjene u izvornom kôdu da bi proradio na sistemu bez mreže! Abrakadabra!

Q: Kako da saznam da li je s druge strane zatvorena veza?

A: Možeš da znaš po tome što recv() vraća nulu (0).

Q: Kako sam da napravim "ping"? Šta je ICMP? Gdje da nađem još informacija o "sirovim soketima" i SOCK_RAW?

A: Sva tvoja pitanja u vezi "sirovih soketa" imaju odgovor u knjigama Ričarda Stivensa koje se tiču UNIX mrežnog programiranja. Pogledaj odjeljak knjige u okviru ovog vodiča.

Q: Kako da pravim ove programe u Windows-u?

A: Prvo, izbriši Windows i instaliraj Linux ili BSD. };-). Ne, šalim se, samo pogledaj primjedbu za Windows programere u uvodu ove knjige.

Q: Kako da pravim ove programe za Solaris/SunOS? Stalno dobijam greške pri povezivanju izvršne datoteke!

A: Greške pri povezivanju se dešavaju jer se Sun sistemima mora eksplicitno ukazati na to da treba da prevedu sa bibliotekama soket-funkcija. Pogledaj primjedbu za Solaris/SunOS programere.

Q: Zašto mi select() stalno pada pri signalu?

A: Signali uvijek žele da zaustave blokirajuće sistemske pozive, koji bi zatim vratili -1 i podesili errno na EINTR. Kad podesiš rukovalac signala pomoću sigaction(), možeš postaviti opciju SA_RESTART, koji će uzrokovati da se sistemski poziv ponovo pokrene nakon što se prekine.

Naravno, ovo ne uspijeva uvijek.

Moje omiljeno rješenje je goto naredba. Znaš da ovo jako nervira tvoje profesore, zato baš i hoćemo tako!

select_restart:

Page 47: Vodič za mrežno programiranje Korištenje Internet soket-a

if ((err = select(fdmax+1, &readfds, NULL, NULL, NULL)) == -1) { if (errno == EINTR) { // neki signal nas je prekinuo, zato hajde ispočetka goto select_restart; } // ovde obradi pravu grešku: perror("select"); }

Naravno, goto nije neophodno u ovom slučaju; možeš da koristiš bilo koju drugu kontrolnu strukturu. Ali ja mislim da je sa goto stvar jasnija.

Q: Kako da ostvarim tajmaut pri pozivu funkcije recv()?

A: Koristi select()! Pomoću njega možeš odrediti tajmaut za soket-deskriptor od kog očekuješ informacije. Mogao bi sve da odradiš u jednoj jedinoj funkciji, kao što je ova:

#include <unistd.h>#include <sys/time.h>#include <sys/types.h>#include <sys/socket.h> int recvtimeout(int s, char *buf, int len, int timeout){ fd_set fds; int n; struct timeval tv;  // podesi skup fajl-deskriptora FD_ZERO(&fds); FD_SET(s, &fds);  // Podesi struct timeval za tajmout tv.tv_sec = timeout; tv.tv_usec = 0;  // Čekaj dok ne primiš informacije ili se tajmaut ne potroši n = select(s+1, &fds, NULL, NULL, &tv); if (n == 0) return -2; // timeout! if (n == -1) return -1; // error  // ovde moraju biti podaci, tako da ćemo uraditi obični recv() return recv(s, buf, len, 0);} // Primjer poziva funkcije recvtimeout(): . . n = recvtimeout(s, buf, sizeof(buf), 10); // tajmaut od 10 sekundi  if (n == -1) { // greška se desila perror("recvtimeout"); }

Page 48: Vodič za mrežno programiranje Korištenje Internet soket-a

else if (n == -2) { // potrošio se tajmaut } else { // neki podaci su u buf } . .

Primijeti da recvtimeout() vraća -2 u slučaju da se potrošio tajmout. Yašto ne vrati 0? Pa, ako možeš da se sjetiš, recv() vraća nulu ako je došlo do zatvaranja veze sa druge strane. Na taj način, već je potrošena nula, a -1 znači "greška", pa sam izabrao -2 kao indikator tajmauta.

Q: Kako da šifriram ili kompresujem podatke prije nego ih pošaljem kroz soket?

A: Jedan jednostavan način je da koristiš SSL (secure sockets layer(sloj sigurnosnih soketa)), ali to je van domašaja ove knjige.

Ali ako pretpostavimo da hoćeš da ubaciš sopstveni kompresor ili sistem šifrovanja, onda samo zamisli podatke kako prelaze preko niza stepenika između dva kraja. Svaki stepenik mijenja podatke na neki način.

1.      server čita podatke iz datoteke (ili odnekle drugo)

2.      server šifrira podatke (ovo ti radiš)

3.      server šalje šifrirane podatke

A sad s druge strane:

4.      klijent prima šifrirane podatke

5.      klijent dešifruje podatke (ovo takođe ti radiš)

6.      klijent piše podatke u datoteku (ili šta god)

Takođe možeš da uradiš kompresiju odnosno dekompresiju na mjestu gdje je u gornjem primjeru urađeno šifriranje odnosno dešifrovanje. A može i oboje! Samo zapamti da kompresuješ prije nego što šifriraš. :)

Dok god klijent vraća unatrag opercije koje je uradio server, podaci će biti bezbijedni, bez obzira koliko tih operacija ima..

Na taj način, možeš jednostavno da koristiš moj kôd da bi slao i primao podatke, a u njemu nađi pogodno mjesto da ubaciš obradu datih podataka.

Q: Šta je taj "PF_INET" koji stalno viđam? Jel' ima veze sa "AF_INET"?

Page 49: Vodič za mrežno programiranje Korištenje Internet soket-a

A: Da, da, ima. Pogledaj poglavlje o funkciji socket() ako te interesuju detaljni podaci.

Q: Kako da napravim server koji prima komande ljuske od klijenta, i izvršava ih?

A: Radi jednostavnosti, recimo da se klijent spaja (connect()), šalje podatke (send()), i zatvara (close()) vezu (dakle, nema naknadnih sistemskih poziva bez ponovnog klijentovog povezivanja.)

Procedura po kojoj ide klijent je sledeća:

1.      connect() na server

2.      send("/sbin/ls > /tmp/client.out")

3.      close()

U međuvremenu, server rukuje podacima izvršavajući ih:

1.      accept() vezu od klijenta

2.      recv(str)

3.      close()

4.      system(str) da bi pokrenuo datu komandu

Pazi! Ako server izvršava sve što klijent kaže, to je kao da si dao klijentu ljusku na svom sistemu, i sve svoje dozvole!. Eto, u gornjem primjeru, šta ako klijent pošalje "rm -rf ~"? Briše se sve što je pod tvojim nalogom, eto šta!

Zato budi pametan, i ne dozvoli da klijent može raditi išta sem nekoliko potpuno bezbijednih operacija, kao što je odrađeno u foobar programu:

if (!strcmp(str, "foobar")) { sprintf(sysstr, "%s > /tmp/server.out", str); system(sysstr); }

Ali još si ranjiv, nažžalost: šta ako klijent pošalje "foobar; rm -rf ~"? Jedina sigurna stvar je da napišeš malu funkciju koja će postaviti karakter "\" ispred svih ne-alfanumeričkih karaktera (uključujući razmake, ako je potrebno) argumenta komande.

Kao što vidiš, sigurnost je prilično velika stvar kda server počne da ima ulogu izvršavanja klijentovih komandi.

Page 50: Vodič za mrežno programiranje Korištenje Internet soket-a

Q: Šaljem gomilu podataka, ali kad ih primam pomoću recv(), primim stalno samo po 536 bajtova ili 1460 bajtova. Ali ako pokrenem program lokalno, primim sve podatke odjednom. Šta se dešava?

A: Dostižeš MTU – najveći broj podataka kojim fizički uređaj može da rukuje. Lokalno, tvoj virtuelni povratni mrežni uređaj može da održi 8k ili i više pa nema problema. Ali na mreži, koja može da rukuje sa samo 1500 bajtova skupa sa zaglavljem, dostižeš to ograničenje. Preko modema, sa MTU-om od 576 bajtova (opet, uključujući zaglavlje), dostižeš još niže ograničenje.

Treba uvijek da budeš siguran da si poslao sve podatke, pod jedan. (Vidi sendall() radi detalja.) Kad si se to uvjerio, onda treba u nekoj petlji da pozivaš recv() dok ne pročitaš sve podatke.

Pročitaj poglavlje O enkapsulaciji podataka radi detalja o primanju čitavih paketa podataka koristeći višestruke pozive komande recv().

Q: Imam Windows, pa nemam fork(), i nemam nikakav struct sigaction. Šta da radim?

A: Ako ih ima igdje, onda su u POSIX bibliotekama koje si možda dobio uz svoj prevodilac. Pošto nemam Windows, stvarno ne mogu da ti odgovorim, ali mislim da se sjećam da Microsoft ima sloj kompatibilnosti sa POSIX-om i tu bi fork() trebao biti. (a možda čak i sigaction.)

Pretraži pomoćne datoteke (help) koje dolaze uz VC++ za riječima "fork" ili "POSIX" pa vidi ako ima išta.

Ako nema ništa od toga, odustani od fork() i sigaction, i počni da koristiš Win32 ekvivalent: CreateProcess(). Ne znam kako se koristi CreateProcess() – uzima mali milion argumenata, ali vjerovatno ga pokriva dokumentacija koja dolazi uz VC++.

Q: Kako da pošaljem podatke sigurnom vezom, pomođu TCP/IP koristeći enkripciju?

A: Pregledaj OpenSSL projekat.

Q: Koristim firewall – kako da pustim ljude izvan njega da znaju moju IP adresu pa da se spoje na moju mašinu?

A: Nažalost, uloga firewall-a je da spreči ljude van njega da se spajaju na računar unutar njega, tako da ako dozvoliš ljudima da ipak uđu – praktično si napravio rupu u bezbijednosti.

Ali, nije sve izgubljeno. Prva stvar, još uvijek možeš da se spajaš (connect()) kroz firewall ako se sve našminka kako treba. Jednostavno, tvoj program unutar firewall-a treba da inicira vezu, i sve će biti u redu.

Ako nisi zadovoljan, pitaj administratora sistema da napravi rupu u firewall-i da bi se ljudi mogli spajati s tobom. Firewall može da ti prosleđuje podatke kroz svoje NAT aplikacije, ili kroz proxy ili nešto slično.

Page 51: Vodič za mrežno programiranje Korištenje Internet soket-a

Budi pažljiv da rupa u firewall-u nije ništa naivno. Moraš uvijek da paziš da loši ljudi ne dospiju unutar tvoje mreže; ako si početnik, napraviti program bezbijednim je teže nego što možeš i zamisliti.

Nemoj da se tvoj administrator ljuti na mene.

9. Objava i poziv u pomoćEto , to je bilo to. Nadam se da je bar nešto od svih ovih informacija bilo precizno i prenosivo, i iskreno se nadam da nema nekih grešaka da se vide iz aviona. Dobro, naravno da ih uvijek ima.

Tako, neka ovo bude upozorenje! Žao mi je ako su ti neke nepreciznosti ovdje prouzrokovale bilo kakvu muku, ali jednostavno ne mogu biti odgovoran za takve stvari. Vidiš, ne stojim niti iza jedne jedine riječi ovog dokumenta, formalno govoreći.Cijela priča i sve rečeno bi moglo biti potpuno i sve pogrešno!

Ali najvjerovatnije nije. Ipak, proveo sam mnoge sate baveći se ovim, i sproveo u djelo nekoliko TCP/IP mreža, napisao neke programe za igranje višekorisničkih igara preko mreže i tako dalje. Ali nisam Bog za sokete. Ja sam samo običan neki tip.

Usput, ako bilo ko ima neke konstruktivne (ili destruktivne) kritike u vezi ovog dokumenta, neka mi pošalje poruku na <[email protected]> i pokušaću da uradim najbolje što mogu.

Ako se pitate zašto radim sve ovo, pa, radim to za pare. Ha! Ne, stvarno, uradio sam to jer su mi mnogi ljudi postavljali pitanja vezana za sokete i kad im kažem da sam razmišljao o pisanju stranice sa svim informacijama o soketima, oni kažu "super!" Pored toga, mislim da će propasti svo ovo teško naučeno znanje ako ga ne podijelim sa drugima. Ispostavlja se da je web savršena stvar. Preporučujem svima da rade slične stvari kad god mogu.

Dosta priče – hajde na pisanje programa!

Page 52: Vodič za mrežno programiranje Korištenje Internet soket-a