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.