Uploaded by duma_david2001

Lucrarea 2 - SDC

advertisement
LUCRAREA 2
Secțiunea A. Comunicația client-server prin TCP
1. Scopul lucrării
Lucrarea urmărește studiul comunicației între programe la distanță prin intermediul
soclurilor TCP. Exemplificarea se face pe o aplicație client-server care retransmite în
ecou un mesaj.
2. Noțiuni teoretice
O conexiune TCP reprezintă un canal de comunicație abstract bidirecțional, ale cărei
capete sunt identificate prin câte o adresă IP și un număr de port. Pentru TCP Java
pune la dispoziție două clase: Socket și ServerSocket. Înainte de a putea transmite
date, este nevoie de o fază de stabilire a conexiunii, în care clientul trimite o cerere de
conectare la server. Aici o instanță a lui ServerSocket ascultă pentru o cerere de
conectare, după care creează un nou Socket pentru tratarea cererii și transferul
efectiv de date.
Serverul TCP
Rolul serverului este de a stabili un terminal de comunicație și a aștepta o cerere de
conectare de la clienți. Etapele parcurse de server sunt:
1. Creează o instanță ServerSocket și ascultă o cerere de conectare la portul
specificat.
2. În mod repetat
o preia o nouă cerere de conectare cu metoda accept() a lui
SeverSocket, după care construiește o instanță Socket pentru noua
conexiune
o comunică cu clientul prin clasele InputStream și OutputStream ale
clientului
o închide soclul pentru client la terminarea transmiterii de date cu metoda
close() a lui Socket
Clientul TCP
Clientul este cel care inițiază comunicația cu serverul, etapele parcurse fiind:
1. Creează o instanță a clasei Socket, care stabilește o conexiune TCP la portul
specificat al serverului.
2. Comunică cu serverul folosind clasele InputStream și OutputStream ale
soclului.
3. Închide conexiunea cu metoda close() a lui Socket.
Soclurile TCP folosesc abstracția stream (flux), care reprezintă un șir ordonat de octeți.
Când se scrie un astfel de șir în fluxul de ieșire (OutputStream) al unui soclu, atunci
în cele din urmă el se poate citi din fluxul de intrare (InputStream) al soclului de la
capătul celălalt al conexiunii.
3. Aplicația propusă
Aplicația client comunică cu un server de ecou folosind protocolul TCP. Mesajul care
trebuie retransmis în ecou se dă ca argument al programului client. Serverul rulează
în buclă infinită, acceptând în mod repetat o conexiune, recepționând și retransmițând
fluxul de octeți recepționați, până când conexiunea se închide de către client.
Pașii programului server:
1. Se fac inițializările și se analizează argumentele programului.
2. Se construiește un soclu server legat de portul specificat:
servSock = new ServerSocket(servPort)
3. Se construiește un buffer de recepție al mesajelor.
4. În buclă infinită
o se acceptă cererea de conectare și se creează un nou soclu conectat
deja la soclul la distanță al clientului:
clntSock = servSock.accept()
o se tipărește adresa și numărul de port al clientului, care se obțin cu
metoda getRemoteSocketAddress() a lui Socket
o se creează instanțele lui InputStream și OutputStream ale soclului
pentru fluxul de intrare/ieșire
o se citesc octeții din fluxul de intrare și se scriu imediat în fluxul de ieșire
până când nu mai sunt date disponibile și clientul a închis soclul său
(indicat prin valoarea -1 returnată de read())
o se închide soclul folosit pentru client
Pașii programului client:
1. Se analizează argumentele programului (serverul, mesajul textual, portul
serverului).
2. Se convertește mesajul String în șir de octeți cu metoda getBytes().
3. Se creează un soclu TCP și se conectează la server la portul specificat:
socket = new Socket(server, servPort)
4. Se creează instanțele de tip OutputStream și InputStream ale soclului
pentru transmiterea și recepționarea datelor.
5. Se transmite șirul de octeți la server cu metoda write() a lui OutputStream.
6. Se citesc în buclă datele recepționate în ecou (metoda read() se blochează
până când există niște bucăți de date disponibile) până la atingerea numărului
de octeți care au fost trimiși.
7. Se tipăresc datele recepționate convertite în șir de caractere.
8. Se închide soclul clientului.
4. Partea practică
1. Scrieți, compilați și rulați pe calculatorul gazdă local programele client și server
specificate la punctul 3. Demonstrați funcționalitatea lor prin transmiterea
diferitelor mesaje în ecou.
2. Modificați serverul astfel încât să citească și să scrie un singur octet, după
care închide soclul. Studiați ce se întâmplă cu clientul care trimite serverului
un mesaj format din mai mulți octeți.
Secțiunea B. Implementarea soclurilor TCP
1. Scopul lucrării
Lucrarea urmărește înțelegerea modului de implementare a soclurilor TCP în nivelul
de dedesubt furnizat de platforma pe care rulează aplicația. Se studiază activitățile
care au loc la acest nivel la stabilirea, respectiv închiderea unei conexiuni TCP.
2. Noțiuni teoretice
Figura 1 reprezintă o schemă simplificată a structurilor de date asociate cu un soclu
TCP. Soclul se referă aici la abstracția de dedesubt corespunzătoare unei instanțe
Socket, care este furnizată de sistemul de operare sau de mașina virtuală Java
(JVM). Structura de soclu conține printre altele:
-
Adresele IP și numerele de port locale și la distanță asociate cu soclul.
-
Cozile de tip FIFO (primul intrat primul ieșit) ale datelor care așteaptă să fie
transmise (SendQ), respectiv ale datelor recepționate care așteaptă să fie
livrate aplicației (RecvQ).
-
Informația de stare a protocolului TCP cu confirmare (handshake) referitoare la
deschiderea și închiderea soclului.
Fig. 1. Structurile de date asociate soclului TCP/IP [1]
Următoarele metode ale soclului creează accesul la buffer-ele aferente cozilor de
intrare/ieșire:
in = socket.getInputStream()
out = socket.getOutputStream()
TCP oferă un serviciu fiabil orientat de flux de octeți, la care datele depuse în coada
de ieșire trebuie să fie păstrate până când sunt recepționate cu succes la celălalt capăt
al conexiunii. Scrierea datelor în fluxul de ieșire nu înseamnă că acestea au și fost
transmise, doar că au fost copiate în buffer. Spre deosebire de soclul de tip datagramă
(DatagramSocket), aici limitele mesajului nu sunt păstrate. (La datagramă, datele nu
sunt puse în buffer, metoda send() predă direct pachetul subsistemului de
comunicație, iar dacă acesta nu poate fi transmis, va fi pierdut.)
Atunci când o nouă instanță Socket este creată, ea poate fi folosită imediat pentru
trimiterea și primirea datelor. La returnarea instanței de către constructor, soclul este
deja conectat la partenerul aflat la distanță, iar protocolul de mesaje de deschidere a
conexiunii fusese înfăptuit.
3. Stabilirea conexiunii
Pe partea de client relația dintre invocarea constructorului Socket și evenimentele
asociate cu stabilirea conexiunii sunt arătate în figura 2. (Adresa IP a clientului este
A.B.C.D., iar a serverului W.X.Y.Z., portul local fiind P, iar cel a serverului Q.) Când
clientul apelează constructorul, cu
socket = new Socket(server, servPort)
implementarea de dedesubt creează o instanță soclu în starea închis (Closed).
Implementarea copiază adresele și numerele de port locale și la distanță în structura
de soclu de dedesubt, și inițiază protocolul cu confirmare pentru stabilirea conexiunii
TCP.
Fig. 2. Stabilirea conexiunii pe partea de client [1]
Stabilirea conexiunii se face cu un protocol triplu cu confirmare (3-way handshake),
care implică trei mesaje: cererea de conectare de la client, confirmarea de la server
și o nouă confirmare de la client la server. După primul mesaj, starea clientului devine
în curs de conectare (Connecting). Clientul consideră conexiunea stabilită
(Established) imediat ce primește confirmarea de la server, iar dacă aceasta nu
sosește, face încercări repetate prin retransmiterea cererii. Dacă nici după un timp (de
ordinul minutelor) nu vine confirmarea de la server, se produce o excepție de tip timeout. În cazul în care serverul nu acceptă conexiunea, va trimite un mesaj de rejectare.
Secvența de evenimente la server începe după crearea unei instanțe a soclului server:
servSock = new ServerSocket(servPort)
Implementarea de dedesubt creează o structură de soclu (fig. 3.), în care completează
portul local (Q) și o adresă de substituție (*) pentru IP-ul local. Soclul trece într-o stare
de ascultare (Listening) în care este gata să accepte cereri adresate la acest port.
Fig. 3. Crearea soclului la server [1]
Serverul apelează metoda accept() a soclului server
clntSock = servSock.accept()
care se blochează (Listening) până când nu vine cererea de conectare de la un client
TCP (fig. 4.). Când o astfel de cerere sosește (primul handshake), se creează o nouă
structură de soclu pentru conectare (Connecting) și se trimite mesajul de confirmare
la client (al doilea handshake). Structura noului soclu se completează pe baza
informațiilor din mesajul de cerere cu adresele IP locale și la distanță, respectiv cu
numărul portului la distanță. Starea noului soclu este în curs de conectare (Connecting)
și așteaptă sosirea celui de-al treilea mesaj handshake, care desăvârșește protocolul.
Când acesta sosește, conexiunea se consideră a fi stabilită (Established), ceea ce se
înregistrează în starea structurii. Dacă totuși mesajul nu sosește, structura se șterge.
Fig. 4. Prelucrarea cererii la server [1]
Când noul soclu pentru deservirea clientului a fost creat, acesta se adaugă la lista
soclurilor asociate cu ServerSocket care încă nu sunt conectate. Odată cu stabilirea
conexiunii (prin terminarea protocolului cu trei mesaje) se returnează o instanță
Socket pentru noul soclu destinat schimbului de date și acest soclu se mută pe lista
structurilor care reprezintă conexiuni stabilite. ServerSocket este deblocat și ascultă
o eventuală nouă cerere de conectare de la un alt client. (Este de menționat, că după
primirea confirmării de la server clientul poate deja trimite date, înainte ca pe partea
de server noua instanță să fie returnată!)
4. Închiderea conexiunii
Secvența evenimentelor pe partea care închide prima conexiunea este arătată în
figura 5. Desfacerea completă a conexiunii se face după două protocoale cu
confirmare, una inițiată de partea care apelează prima dată close(), cealaltă inițiată
de partea care apelează mai târziu metoda de închidere.
Fig. 5. Partea care închide prima [1]
Ordinea acțiunilor este următoarea:
-
După invocarea lui close() implementarea de dedesubt transmite toate
datele rămase în SendQ și trimite primul mesaj handshake. (Ca urmare a
acestui mesaj, pe partea receptoare o metodă read() va returna –1). Starea
structurii trece la cea în curs de închidere (Closing) și în aplicație metoda
close() revine imediat.
-
TCP așteaptă confirmarea de la partea receptoare, după care conexiunea
devine pe jumătate închisă (Half-Closed), până când un protocol cu
confirmare nu are loc pe cealaltă direcție.
-
Când sosește primul mesaj al protocolului de închidere inițiat de partea
cealaltă, această parte trimite confirmarea acestuia și trece într-o stare de
așteptare-temporizare (Time-Wait) în care mai rămâne structura de dedesubt
pentru un timp (de ordinul minutelor, în practică dublul timpului cât un pachet
poate rămâne în rețea). Această stare e necesară pentru ca pachete rătăcite
în rețea să nu fie luate în considerare dacă imediat se stabilește o nouă
conexiune între aceste părți.
Evoluția stării structurii de dedesubt a soclului pe partea care închide a doua este
arătată în figura 6. Ordinea evenimentelor este următoarea:
-
Când sosește primul handshake de închidere, partea care încă nu a închis
trimite imediat o confirmare și trece structura în starea de așteptare pentru
închidere (Close-Wait). În această stare așteaptă ca aplicația de deasupra să
apeleze close() pentru închiderea soclului.
-
La apelarea lui close() se inițiază către partea cealaltă protocolul final de
închidere și după primirea confirmării se șterge structura. În aplicație close()
revine imediat fără să aștepte terminarea protocolului.
Fig. 6. Partea care închide a doua [1]
Din cauza faptului că invocarea lui close() revine imediat, este posibil ca după
apel să mai rămână date în SendQ, care pot să se piardă dacă gazda de la distanță
cade. Ca urmare este indicat ca partea care închide să facă acest lucru doar după
ce s-a asigurat la nivelul aplicației că toate datele sale au fost recepționate. Cu
metoda setSoLinger() se poate însă fixa un interval de timp până când
close() să se blocheze sau să se blocheze până la finalizarea protocolului de
închidere. În acest caz datele din SendQ sunt trimise și se așteaptă până la
primirea confirmării la nivelul TCP.
5. Partea practică
1. Vizualizați și studiați starea momentană a structurilor de date asociate
conexiunilor active pe calculatorul pe care lucrați. Se va folosi programul
netstat, disponibil pe platforma Windows.
2. Încercați să captați cu netstat stările soclurilor în cursul execuției unei
aplicații client-server orientat pe conexiune TCP.
3. Este posibil ca diferite socluri pe aceeași gazdă să aibă aceeași adresă locală
și același număr de port. De exemplu la server o nouă instanță Socket
acceptată printr-un soclu server va avea același port ca și ServerSocket.
Studiați la care soclu trebuie livrat un pachet de intrare pentru care există mai
multe socluri cu același număr de port.
BIBLIOGRAFIE
1. K. L. Calvert, M. J. Donahoo, TCP/IP Sockets in Java, Morgan Kaufmann, 2008.
2. Tutorial Java: www.learn-java-tutorial.com.
Download