Radomir Mladenovic, student Elektronskog fakulteta u Nisu
Boban Nikolic, Ei Sigraf
OBJEKTNO ORIJENTISANI PRISTUP TCP/IP SOCKET-IMA
Svedoci smo da poslednjih nekoliko godina dolazi do sirenja i medjusobnog povezivanja
racunarskih mreza, i pojave velikog broja servisa i aplikacija ciji je cilj da sto bolje iskoriste
resurse umrezenih racunara i unaprede i popularisu ovakav nacin komunikacije, rada i
informisanja., ili, jednostavno zabave. Ovakav, sve brzi razvoj hardverskih resursa zahteva i
pracenje odgovarajucom softverskom podrskom, sto uslovljava i unapredjenje razvojnih paketa i
biblioteka funkcija koji se bave mrezama. CSock biblioteka sadrzi skup klasa za C++ i znatno
olaksava razvoj programa koji imaju potrebu za razmenom podataka na mrezi, sa ili bez
konekcije. CSock biblioteka je razvijena za TCP/IP protokol i u Windows verziji postoji kao
dinamicka biblioteka (DLL).
Osnovni entitet pri TCP konekciji je soket. Sama njegova implementacija zavisi od operativnog
sistema i krece se od identifikatora do fajl deskriptora. Soket tako mozemo da zamislimo i kao
objekat, tj instancu klase u kojoj su sadrzane sve funkcije za rad sa soketom. Ova klasa mogla bi
da izgleda na sledeci nacin:
class CSocket
{
protected:
SOCKET sock;
int protokol;
int status; // identifikuje stanje socketa
virtual void OnRead( UINT ); // soket spreman za citanje
virtual void OnWrite( UINT ); // soket spreman za pisanje
virtual void OnOOB( UINT ); // out-of-band data za citanje
virtual void OnAccept( UINT ); // spreman za accept nove konekcije
virtual void OnConnect( UINT ); // konekcija zavrsena
virtual void OnClose( UINT ); // soket zatvoren
. . .
public:
CSocket();
~CSocket();
// verzije za inicijalizaciju socketa
virtual BOOL Init( const char *local_ini );
virtual BOOL Init( sockaddr *adr );
virtual void close();
virtual long GetStatus();
virtual int send( void *buff, int len );
virtual int recv( void *buff, int len );
virtual int io(long cmd, u_long FAR *argp);
virtual int AsyncSelect( HWND hWnd, UINT msg, long event );
virtual int Socket( int af = PF_INET, int type = SOCK_STREAM, int prot = 0 );
virtual int bind( sockaddr *name, int NameLen );
virtual int bind( int Port, int fam = AF_INET );
virtual SOCKET accept();
virtual int listen( int que );
virtual SOCKET operator= ( SOCKET s );
. . .
};
Ovakva klasa sadrzi clan tipa SOCKET koji ima vrednost dobijenu od sistema pri otvaranju
soketa. Clan protokol ukazuje na tip protokola za koji je soket otvoren (TCP ili UDP), dok status
predstavlja trenutno stanje soketa ili samog objekta. Virtuelne funkcije OnRead, OnWrite,
OnClose, OnAccept i druge iz ove grupe funkcija, mogu se iskoristiti u izvedenim klasama i
predefinisati tako da na odredjeni nacin reaguju na pojavu podataka za citanje, zatvaranje soketa
odnosno pucanje veze, pojavu urgentnih informacija na soketu ili na zahtev za konekcijom.
Ostvarivanje konekcije moguce je otvaranjem soketa funkcijom Socket i postavljanjem soketa
(bind) na odredjeni port. Ako program treba da prihvati konekciju, koristi se listen za osluskivanje
porta, OnAccept za detekciju zahteva, i najzad, accept za prihvatanje veze.
Ipak, u vecini slucajeva, PC se koristi kao klijent i povezuje se za racunar koji ima ulogu servera
za datu aplikaciju. U takvim slucajevima, najveci komfor se postize inicijalizacijom preko Init
funkcija, koje prihvataju ili sockaddr strukturu, ili ime INI fajla u kojem se nalaze stavke sa IP
adresom ili imenom hosta i porta na kome on ocekuje konekciju.
Pisanje na soket, odnosno slanje podataka, obavlja se funkcijom send kojoj se daje pointer na
podatke koji se salju i broj bajtova za slanje. Citanje sa soketa vrsi se funkcijom recv koja uzima
pointer na bafer u koji treba smestiti podatke i broj bajtova koji treba procitati. Obe ove funkcije,
sem poziva sistemskih funkcija, proveravaju i detektuju eventualne greske i vracaju status.
Klasa kao sto je CSocket sadrzi u sebi sve sto je potrebno za mreznu komunikaciju. Medjutim, u
slozenim klijent-server aplikacijama dolazi do pojave problema koji bitno usloznjavaju
komunikaciju. Razmena podataka izmedju klijenta i servera vrsi se po principu transakcija,
odnosno zahteva, i ako je potrebno, odgovora na zahtev, pri cemu svaka vrsta zahteva ima
jednistven identifikator, koji se naziva kod transakcije.
Prvi problem koji se javlja pri radu sa velikim kolicinama podataka koje se salju i citaju sa soketa,
je taj da IP deli jedan veliki paket na vise manjih. Tako, ne mozemo da se oslonimo na to da
cemo u jednom citanju pokupiti sve podatke koje nam je poslao server. Jedan poziv sistemske
funkcije za citanje podataka sa soketa pokupice prvi paket, cija velicina zavisi od slobodne
memorije bafera, koji inace kontrolise sistem. Velicina ovih najvecih paketa je 1460 bajtova
(odnosno 1006 kod SLIP-a , IP na serijskim linijama). Zato se, ako ne proveravamo duzinu pri
citanju, moze pokazati kao misterija cinjenica da je server poslao sve podatke, a da ih aplikacija
nije primila. Drugi problem vezan sa velikim kolicinama podataka je kada je za prihvat podataka
potrebno vise memorije nego sto je predvidjeno aplikacijom, pa je potrebno da se razrese i
ovakve situacije, a da se podaci ipak procitaju da ne bi ostali u soketu.
Druga vrsta problema javlja se kod aplikacija koje treba da, asinhrono, znaci u bilo kom
trenutku, prihvate i obrade neki zahtev ili obavestenje od strane servera. Moze se desiti da
aplikacija posalje zahtev serveru i ceka odgovor, a da u tom trenutku stigne transakcija od
servera koja nije ocekivana, i koja moze da bude protumacena kao odgovor na zahtev zatrazen
od strane klijenta.
Zbog ovih i slicnih problema koji se javljaju pri komunikaciji, razvijeno je par klasa za upravljanje
memorijom i dogovoren protokol za klijent-server aplikacije koji je ugradjen u klasu CCSocket.
Klasa CCSocket nasledjuje klasu CSocket, pri cemu najveci broj njenih osobina zadrzava, a
standardizuje nacin razmene transakcija na mrezi izmedju server i klijent aplikacije. Osnovne
funkcije koje dodaje ova nova klasa, a koje se ticu komunikacije su:
class CCSocket : public CSocket
{
. . .
public:
virtual void Send( UINT kod, const void *data = NULL, long len = 0 );
virtual CData& Recv( UINT kod, int block = 1 );
virtual CData& Reply( UINT kod, const void *data = NULL,
long len = 0, int block = 1 );
. . .
};
Podaci se sada salju funkcijom Send koja preuzima kod transakcije, pointer na podatke i
kolicinu podataka koja se salje kroz soket. Recv kao jedini obavezan parametar zahteva kod
transakcije, i opciono, da li je poziv blokiran, odnosno da li se obavezno ocekuje transakcija sa
datim kodom ili se samo proverava postojanje takve transakcije u baferu. Funkcija Reply
predstavlja kombinaciju Send i Recv.
Da bi se razresile konfliktne situacije, kao gore navedene, klasa CCSocket tj. objekat njenog tipa,
ispred svih paketa koji se razmenjuju kroz soket dodaje zaglavlje oblika:
struct t_head
{
char id[2];
char kod[3];
unsigned char arg; // tip onoga sto sledi u sledeca 4 bajta hedera
union {
long len; // duzina data dela koji sledi iza hedera
. . .
};
};
Zaglavlje zadrzi identifikator onoga ko salje, i ne mora da ima prakticni znacaj. Za njim slede kod
transakcije, i identifikator tipa, koji govori o tome sta je sadrzano u nastavku zaglavlja, a u vecini
slucajeva se radi o kolicini podataka koja se salje iza zaglavlja. Zaglavlje moze sadrzati i
informacije drugog tipa, ne samo o duzini transakcije, na sta ukazije t_head.arg, i u kom slucaju
iza zaglavlja nema podataka.
Funkcija Recv obavlja sva citanja sa soketa, pri cemu se prihvata prvo zaglavlje a zatim podaci
(ako ih ima), pri cemu se razresava problem pojave transakcije koja se ne ocekuje. U tom
slucaju pristigla transakcija se procita i cuva, a transakcija sa zeljenim kodom se ceka dalje.
Kada naidje zaglavlje sa zeljenim kodom, rezervise se onoliko memorije koliko je potrebno za
prihvat podataka. Podaci procitani sa soketa vracaju se aplikaciji kao objekti tipa CData.
Klasa CData namenjena je za prihvatanje podataka sa soketa. Cilj je bio da aplikacija bude
oslobodjena rezervisanja i oslobadjanja memorije za prihvat transakcije. Objekat tipa CData,
izmedju ostalog, sadrzi pointer na bafer sa podacima i velicinu tog bafera. Kada se kreira, ovakav
objekat ne raspolaze rezervisanom memorijom. Memoriju rezervise funkcija Recv i dodeljuje je
objektu CData. Kada se prihvati sav sadrzaj transakcije, Recv kao rezultat vraca ovaj objekat.
Prihvat podataka u aplikaciji i koriscenje podataka se vrsi sa:
CData data;
data = soc.Recv( 100 );
char *str = (char *) data.GetData();
. . .
Podaci koje drzi objekat data, tj. na koje ukazuje pointer str, mogu se koristiti sve dok zivi dati
objekat ili dok se ovom objektu na dodele novi podaci (npr. ponovnim citanjem sa soketa). U
tim slucajevima oslobadja se zauzeta memorija.
Za aplikacije u C++-u, najelegantniji nacin za dodefinisanje ponasanja objekata za sokete je
nasledjivanje jedne od klasa iz biblioteke i prilagodjenje funkcija specificnostima aplikacije ili
protokola. Na ovaj nacin omoguceno je stvaranje soketa koji mogu da obave deo transakcija
zatrazenih od strane servera bez obavestavanja aplikacije, i slicno.
Za ne C++ prevodioce, pristup klasama iz CSock biblioteke moguc je kroz eksportovane
funkcije DLL-a, kao sto su:
int CSInit( const char *local_ini );
void CSClose( int ids );
int CSsend( int sid, char *buff, int len );
int CSrecv( int sid, char *buff, int len );
int CSSend( int sid, int kod, , char *buff, int len );
int CSRecv( int sid, int kod ); // vraca ID bafera u koji je prihvacena transakcija
int CSGetLength( int did ); // vraca velicinu bafera
int CSGetData( int did, char *buff ); // kopiranje bafera
. . .
U ovim funkcijama soketu se pristupa preko celobrojnog identifikatora, kako bi se ova
biblioteka, bez problema, mogla koristiti i u jezicima kao sto je Visual Basic. Isto tako,
identifikator se dobija i za CData objekte. Ovakvim funkcijama omoguceno je iskoriscenje
mogucnosti klasa iz biblioteke ali je njihovo koriscenje ipak znatno neudobnije.
Prednost razvoja programa uz koriscenje CSock biblioteke, sem konfora koji pruza u radu sa
mrezom, je i to sto ne postoji direktna zavisnost izmedju samog protokola na mrezi i aplikacije.
DLL predstavlja, na neki nacin, interfejs za izlazak programa na mrezu, tako da je u slucaju
potrebe za nekim drugim mreznim protokolom dovoljna zamena DLL-a bez intervencije na kodu
programa. Iako se moze uciniti da je ovakvo usloznjavanje nepotrebno i da je zbog objektno
orjentisane arhitekture rad sporiji nego sto bi, u teoriji, mogao da bude, u praksi je taj gubitak,
kada se uzme u obzir brzina mreze, sasvim zanemarljiv. Sa druge strane, nasledjivanje klasa
predstavlja dobru osnovu za razradu postojeceg protokola namenjenog klijent-server okruzenju,
ili za izgradjivanje specificnih protokola od strane programera koji razvija aplikaciju.