Sunteți pe pagina 1din 11

Laborator 7: Eficienta si scalabilitate. Java NIO ? Responsabili: Catalin Gosman catagosman@gmail.com ? Data publicarii: 30-03-2013 ?

Data ultimei modificari: 30-03-2013 Obiective Scopul acestui laborator este prezentarea unor mecanisme avansate de I/O, cu imp licatii puternice in ceea ce priveste optimizarea aplicatiilor. Subiecte atinse: ? Influenta mecanismelor I/O asupra eficientei si scalabilitatii unei aplicatii ? Java NIO Introducere Permanent, cautam sa optimizam aplicatiile pe care le scriem. De cele mai multe ori, ne concentram pe realizarea unui design conceptual evoluat, ce defineste di ferite entitati si modurile in care ele comunica. Dar, aceasta izolare in sfera conceptuala se dovedeste, uneori, o capcana. Abstractizarile reprezinta mecanism e utile, care inlesnesc realizarea design-ului si reutilizarea, dar capata valen te negative in momentul in care se distanteaza prea mult de realitatile pe care le reproduc. Un astfel de exemplu il constituie reprezentarea mecanismelor I/O, in cadrul bib liotecii Java. Abordarea clasica din acest limbaj este stream-oriented: FileInpu tStream, BufferedReader sunt cateva exemple. Daca aceasta perspectiva, ce ia for ma fluxurilor de octeti, este potrivita in cazul socket-ilor sau al pipe-urilor, in privinta fisierelor lucrurile stau altfel. Fara a intra foarte mult in detal iile de implementare ale sistemelor de fisiere, amintim ca operatiile I/O cu dis cul se fac la nivel de bloc (sector, pagina etc.). Aceasta abordare se opune per ceptiei clasice Java, care opereaza la nivel de octet. O metoda de lucru la nive l de bloc ar spori viteza, deoarece s-ar mula, natural, pe mecanismele din spate . Iata, deci, un caz in care o serie de abstractiuni favorizeaza unificarea conc eptuala si independenta de platforma, cu pretul ignorarii unor mecanisme eficien te, puse la dispozitie de sistemul de operare. In afara de aceastea, o buna vreme, bibliotecii Java i-a lipsit posibilitatea lu crului cu stream-uri in mod neblocant si a multiplexarii (posibilitatea monitori zarii concomitente a mai multor surse de date). De exemplu, pentru scrierea unui server, singura posibilitate era alocarea unui thread separat pentru fiecare so cket catre clienti. Aceasta solutie este nescalabila, deoarece, la un numar mare de conexiuni, cea mai mare parte a timpului s-ar petrece facand switching intre thread-uri, si nu executand cod efectiv. Merita precizat urmatorul fapt: in ciuda dezavantajelor explicate, utilizarea bi bliotecii Java se poate dovedi in continuare foarte utila, dar in alta maniera:

in locul rezervarii unui fir pentru fiecare conexiune in parte, se poate intrebu inta un pool, cu un numar maxim de fire, stabilit independent, care pot servi ce rerile diferitilor clienti, in timp ce firul initial se ocupa doar de delegarea acestor sarcini celorlalte fire. In aceasta situatie, trebuie avute in vedere, d esigur, eventuale probleme de sincronizare. O posibilitate de a le evita o repre zinta dezactivarea activitatii pe o anumita conexiune, cat timp prelucrarea unei sarcini asociate conexiunii respective este in desfasurare, pe un anumit fir, s i reactivarea acesteia la terminarea procesarii. Acest lucru ar preveni delegare a concomitenta a datelor sosite pe aceeasi conexiune catre alt fir. Java NIO java.nio (new I/O) este pachetul adaugat bibliotecii Java inca din versiunea 1.4 , cu scopul de a rezolva problemele mentionate mai sus. Cele mai importante conc epte sunt: ? buffer: introduce lucrul cu buffer-e, specifice operatiilor I/O la nivel de fisi er. Reprezinta, de fapt, containere de date ? channel: modeleaza comunicarea cu un serviciu de I/O ? selector: ofera posibilitatea urmaririi simultane a mai multor canale neblocante (de exemlu, socketi). Imbunatatirea adusa consta in exploatarea unor mecanisme expuse de majoritatea s istemelor de operare moderne, intr-un mod portabil. Buffers Buffer-ele reprezinta containere pentru vectori de octeti. Clasa cel mai des int rebuintata este ByteBuffer. Avantajul consta in incapsularea unor proprietati im portante: ? capacity: dimensiunea vectorului ambalat, care este data la crearea buffer-ului ? limit: indicele din vector care marcheaza sfarsitul zonei utile (care contine da te propriu-zise). Este cel mult capacity ? position: indicele din vector unde se gaseste urmatorul octet de interes (care v a fi citit sau scris). Iata un exemplu de creare a unor buffer-e: ByteBuffer buf1 = ByteBuffer.allocate(16); // aloca 16 oc teti ByteBuffer buf2 = ByteBuffer.allocateDirect(16); // special, ex plicat mai jos ByteBuffer buf3 = ByteBuffer.wrap(new byte[] {(byte)1, (byte)2}); // ambaleaza u n vector de byte Metoda allocateDirect merita o atentie sporita. Aceasta aloca zona cu pricina in afara heap-ului masinii virtuale Java (JVM), folosind apeluri native. Acest luc ru poate conduce la o eficienta sporita, sistemul de operare putand sa-l folosea

sca direct, fara copieri. Buffer-ul astfel obtinut poarta denumirea de direct bu ffer. Alte functii utile la lucrul cu bufferele sunt: buf.clear(); // reseteaza buffer-ul, in general inain te de populare acestuia: position = 0, limit = capacity buf.put((byte)1); // adauga octetul 1 la buffer, la indice le precizat de position, avansand pozitia cu 1 buf.putInt(65); // adauga cei 4 octeti ai int-ului la bu ffer, avansand pozitia cu 4 buf.put(new byte[] {(byte)1, (byte)2}); // adauga continutul vectorului de byte, avanseaza pozitia cu nr de elemente, 2 buf.flip(); // pregateste citirea datelor din buffer : limit = position, position = 0 byte b = buf.get(); // obtine octetul de la indicele positio n, avansand pozitia cu 1 int n = buf.remaining(); // intoarce numarul de octeti intre posi tion si limit Ca o regula generala, apelati: ? clear, inainte de popularea unui buffer ? flip, inainte de utilizarea datelor din el In general, buffer-ele constituie parametri ai metodelor de citire/scriere ale c analelor (channels). Ele nu sunt thread-safe. Channels Canalele reprezinta mecanisme de acces la serviciile I/O. Acestea sunt de doua t ipuri: ? stream I/O: SocketChannel, ServerSocketChannel ? file I/O: FileChannel Canalele stream-based pot fi neblocante. In aceste conditii, pot fi utilizate in conjunctie cu selectorii pentru multiplexare I/O. File Channels Se pot obtine doar prin intermediul unor fisiere deja deschise. Clasa de referin ta este FileChannel, care expune operatiile de citire si de scriere. Exemplu: RandomAccessFile raf = new RandomAccessFile("out.txt", "rw"); // open for read a nd write FileChannel fc = raf.getChannel(); // get associated file channel fc.write(buf); // buf este un Byt eBuffer; position este actualizat in consecinta fc.read(buf2); fc.close(); Atentie! In general, pentru un apel read, se incearca citirea unui numar de octe

ti egal cu buf.remaining() (se va incerca umplerea completa a buffer-ului). Nu a vem insa nicio garantie ca vor fi cititi exact cati octeti ne asteptam. De aceea , trebuie sa verificam rezultatul intors de read, respectiv numarul de octeti ef ectiv cititi. Astfel, putem folosi o bucla, in care sa efectuam citiri succesive . La intalnirea sfarsitului de fisier se va intoarce -1. Maparea fisierelor in memorie Acest mecanism permite asocierea unui fisier cu o zona de memorie si accesarea c ontinutului acestuia prin operatii cu memoria, renuntandu-se la apelurile read/w rite. Acest lucru exploateaza implementarile moderne ale sistemelor de fisiere, care mapeaza intern zonele de date ale fisierelor peste pagini de memorie virtua la. Metoda map, disponibila pentru un file channel, intoarce un obiect MappedByteBuf fer, care este un buffer direct (definit mai sus). Scrierea si citirea in acest buffer se reflecta direct asupra fisierului. Exemplu: MappedByteBuffer buf = fc.map(FileChannel.MapMode.READ_WRITE, 0, fc.size()); // mapeaza intregul fisier, cu acces read/write socket.read(buf); // citeste de pe socket si scrie DIRECT in fisier!!! Socket Channels Fiecare socket channel are asociat un socket Java clasic, din pachetul java.net. Configurarea modului neblocant: socket.configureBlocking(false); Un ServerSocketChannel este folosit pentru acceptarea de conexiuni noi, de obice i la server. Pe el nu circula date propriu-zise. Exemplu: ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.socket().bind(new InetSocketAddress("x.x.x.x", port)); // ap elul bind se face asupra socket-ului clasic asociat SocketChannel socketChannel = serverSocketChannel.accept(); ceptarea unei conexiuni si obtinerea socket chanel-ului asoicat // ac

Un SocketChannel obisnuit este folosit pentru comunicarea datelor utile: SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("x.x.x.x" , port)); // clientul incearca conectarea la server-ul precizat socketChanner.read(buf); // buf este un ByteBuffer socketChannel.write(buf2); Atentie! Observatia asupra numarului de octeti cititi de read este si mai pregna nta in cazul socket-ilor neblocanti. Un apel read neblocant va intoarce 0 daca n u exista date in acel moment, in buffer-ul din sistem (cuvantul nu se refera la un ByteBuffer, ci la entitati low-level din sistem). In general, citirile se vor face in bucle ce vor continua cat timp read intoarce o valoare strict pozitiva. In momentul in care unul din participantii la discutie inchide socket-ul, readul celuilalt va intoarce -1 (care semnaleaza EOF). In acest caz, socket-ul trebu ie inchis. Atentie! De asemenea, scrierile pe socket se vor face tot in bucla, cat timp val oarea intoarsa de write este strict pozitiva.

Selectors Selector-ii reprezinta modalitatea de realizare a multiplexarii I/O. Putem avea, de exemplu, mai multi socketi, catre diferiti clienti, si ii putem monitoriza, in paralel, pe toti. Practic, delegam sistemului de operare o sarcina la care se pricepe. In procesul de multiplexare sunt implicate 3 tipuri de entitati: ? selector: monitorizeaza aparitia de evenimente, de obicei pe mai multe canale ? selectable channel: un canal ce poate fi selectat: socket sau pipe, nu fisiere ? selection key: reprezinta asocierea dintre un selectable channel si un selector Un selectable channel se poate inscrie la un selector apeland metoda register, c areia i se precizeaza tipurile de evenimente ce se doresc semnalate din partea s electorului pentru canalul respectiv (interest ops). In momentul apelului se cre eaza o cheie de selectie (SelectionKey) unica pentru canalul respectiv. Tipurile de evenimente sunt (pentru precizare mai multor tipuri se foloseste OR pe biti) : ? OP_READ: au sosit date noi pe canal ? OP_WRITE: buffer-ul de iesire este gol, si se pot scrie noi date ? OP_ACCEPT: valabil doar pentru un server socket channel, semnaleaza conectarea u nui nou client ? OP_CONNECT: cererea de conectare s-a finalizat Exemplu: Selector selector = Selector.open(); // initializare selector socketChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE); // inregistrare cu interes de citire si scriere while (true) { selector.select(); // metoda blocanta, se asteapta aparitia de evenimente ... } Se observa ca metoda select suspenda thread-ul curent in asteptarea de eveniment e. La aparitia acestora, se parcurge lista cheilor de selectie si se executa act iunile corespunzatoare. Un exemplu il constituie clasa Server, din scheletul de laborator. Evenimentele de interes pentru un canal se pot schimba prin metoda in terestOps a cheii de selectie (observati cele 2 variante ale metodei, cu si fara parametru, avand rolurile de setter, respectiv getter): key.interestOps(key.interestOps() | SelectionKey.OP_WRITE); // adauga interesul de scriere la cele existente

Modificarile facute asupra unei chei nu sunt vizibile cat timp selectorul se afl a in metoda select. De aceea, mai ales cand modificarea cheii se face de pe alt thread fata de cel pe care selectorul face select, se recomanda apelul selector. wakeup(), care trezeste selectorul pentru a-l face sa tina cont de noile chei, o data cu noul apel select (la reluarea buclei while). Se poate asocia un context cu o conexiune, apeland varianta register cu 3 parame tri. Acest ultim parametru reprezinta un atasament, ce poate fi ulterior obtinut apeland metoda attachment a cheii de selectie. De asemenea, cheile ofera 2 meto de, pentru obtinerea canalului, respectiv a selectorului asociat: Object att = key.attachment(); Channel channel = key.channel(); Selector selector = key.selector(); Inchiderea unui socket conduce la deinregistrarea lui de la toti selectorii la c are a fost inregistrat. Atentie! Inregistrearea intereselor de citire si de scriere au semnificatie dife rita: ? citire: ne intereseaza cand au aparut noi date pe canal ? scriere: ne intereseaza cand se pot trimite noi date, nu neaparat cand s-au trim is cele deja planificate. Cu aceasta observatie, inregistrarea interesului de scriere se face doar cand do rim sa transmitem noi informatii. Altfel, daca in urma semnalarii unui eveniment de scriere, nu se scriu efectiv date (printr-un apel write), selectorul va anun ta in continuu ca se poate scrie. Exercitii ? Utilizati scheletul de laborator. ? Rulati exercitiile pe Eclipse-ul aflat pe masinile din laborator. ? Majoritatea exercitiilor au un comentariu TODO asociat in cod. Exemplu: pentru exercitiul 2, subpunctul 4, cautati // TODO 2.4. Exercitiile trebuie rezolvate i n ordine. 1(3p). Buffers & Channels 1. Cercetati clasa BufferTest, in care se citesc linii de la tastatura si se afise aza inapoi la consola. Rulati dupa fiecare subpunct (inclusiv la inceput). 2. Populati buffer-ul buf cu octetii ce compun sirul line, folosind metoda put a cl asei ByteBuffer: ? Folositi metoda getBytes a clasei String. ? Adaugati la buffer terminatorul de linie, aflat in constanta NEW_LINE.

? Nu uitati sa flancati secventa de apeluri put cu apelurile clear si flip. ? Afisati buffer-ul cu System.out.println dupa fiecare dintre apelurile de mai sus (4 in total) si cercetati parametrii position, limit, capacity. 3. Trimiteti buffer-ul pe canalul outChannel, care este legat la System.out, apelan d write. 4. Inlocuiti secventa de citire de la consola si populare a buffer-ului cu un apel read pe canalul inChannel, legat la System.in. Unde trebuie plasate acum apeluri le clear si flip? 5. Realizati modificarile necesare astfel incat scrierea sa se faca intr-un fisier : ? Initializati variabilele cu tipul RandomAccessFile si FileChannel. ? Folositi obiectul FileChannel in locul outChannel. 6. Realizati modificarile necesare pentru ca scrierea in fisierul de mai sus sa se faca prin maparea acestuia in memorie: ? Initializati obiectul MappedByteBuffer. ? Cititi din inChannel direct in el. ? Mai este necesar apelul write?

2(4p). Sockets - server single-threaded 1. Cercetati clasa Server. Observati constantele membru. 2. In metoda main initializati selectorul cu un apel open. 3. Initializati server socket-ul, astfel incat server-ul sa poata primi conexiuni pe portul dat: ? alocati-l printr-un apel open ? stabiliti modul neblocant: metoda configureBlocking. ?

asociati-l cu dubletul (IP, PORT), apeland bind pe socket-ul intors de metoda so cket a canalului. Folositi clasa InetSocketAddress ca parametru pentru bind. ? inregistrati-l la selector cu interesul OP_ACCEPT: metoda register 4. Observati bucla while din main: ? mai intai, se asteapta producerea unui eveniment prin apelul blocant select. ? la aparitia unor evenimente, acestea sunt parcurse si scoase pe rand din lista (vezi apelul it.remove()). Inlaturarea este un pas important, ce nu trebuie sari t! ? in functie de evenimentul aparut (accept/read/write), se apeleaza functia speci fica de tratare. Voi le veti implementa in cele ce urmeaza. 5. Rulati aplicatia. Observati deschidarea portului utilizand comanzile: ? Windows: netstat -a ? Linux: netstat -la | grep 30000 6. Implementati metoda accept: ? observati parametrul cheie ? puteti obtine canalul si selectorul asociate folosind metodele channel, respect iv selector. ? initializati obiectele server socket (se obtine din cheie) si socket, corespunza tor conexiunii cu noul client. Acesta din urma se poate obtine apeland accept pe server socket. ? configurati noul socket in mod neblocant ? inregistrati-l la selector (se obtine din cheie), cu interes OP_READ. ? folositi buf ca atasament. Amintim ca atasamentele sunt obiecte specifice unei conexiuni, in care se poate retine informatie de context; in cazul nostru este v orba de un buffer. Folositi varianta register cu 3 parametri. 7. In acest moment, server-ul ar trebui sa poata accepta conexiuni noi: ?

testati acest lucru cu comanda: telnet 127.0.0.1 30000 ? server-ul ar trebui sa afiseze adresa clientului conectat 8. Implementati metoda read: ? observati cum se obtin canalul si atasamentul asociate cheii: metodele channel si attachment. Atasamentul este al treilea parametru primit de register, la subp unctul 2.6. ? cititi, intr-un ciclu, de pe socket in buffer, cat timp valoarea intoarsa de rea d este >0. Nu uitati de clear inainte de citire! ? afisati la consola buffer-ul primit, folosind o modalitate de la exercitiul 1 (v ezi comentariile din cod). Nu uitati de flip inainte de scriere! 9. Rulati aplicatia cu telnet: ? scrieti in fereastra si ar trebui sa primiti caracterele la server ? opriti programul telnet. Cum se comporta server-ul? Ati verificat conditia de EO F? (read intoarce -1). La EOF, socket-ul trebuie inchis (va fi automat deinregis trat de la selector). 10. Modificati metoda read, astfel incat sa tot scrie in buffer-ul asociat unui sock et (eventual in urma mai multor apeluri), iar cand acesta se umple complet, sa f ie trimis in intregime inapoi pe socket: ? inlaturati apelul clear. La urmatorul apel read al clasei Server vrem sa adaugam unde am ramas. ? verificati valoarea returnata de buf.hasRemaining(). Daca buffer-ul este plin, apelati flip si schimbati interesul cheii la scriere (metoda key.interestOps()). De-acum ne intereseaza cand putem scrie pe socket. Nu vom mai fi notificati asu pra evenimentelor de citire. 11. Implementati metoda write, incat sa trimita, treptat, tot buffer-ul (eventual i n urma mai multor apeluri), iar cand acesta se goleste, sa se reinstaureze inter esul pentru citire: ? folositi o bucla si aici, pentru scriere, cat timp valoarea intoarsa de write e ste >0. ? verificati, la fel ca la subpunctul 2.10, daca mai sunt octeti in buffer. Daca n u, apelati clear si restabiliti interesul la citire.

12. Rulati aplicatia, eventual cu mai multi clienti telnet. La fiecare 4 (BUF_SIZE) caractere scrise pe telnet, ar trebui sa vina inapoi intregul calup de dimensiu ne 4. 13. Cum se comporta programul daca pentru o conexiune proaspat stabilita se activea za, de la inceput, interesul OP_WRITE? 3(3p). Sockets - server multi-threaded 1. Observati membrul pool al clasei Server. Reprezinta un pool de thread-uri caror a le putem delega prelucrarile informatiilor de pe socketi, astfel incat threadul principal sa se ocupe doar de monitorizarea activitatii. 2. Realizati modificarile necesare astfel incat prelucrarile de citire sa fie dele gate pool-ului: ? ambalati continutul metodei read a clasei in metoda run a unui obiect Runnable. ? delegati prelucrarile pool-ului, apeland metoda pool.execute(Runnable). 3. Rulati aplicatia: ? de ce, la fiecare caracter trimis pe telnet, se afiseaza de mai multe ori sirul READ? ? motivul este ca thread-ul worker nu apuca sa citeasca de pe socket, in timp ce thread-ul principal, care apeleaza select, ajunge imediat acolo, constata, din n ou, prezenta evenimentului de citire, si deleaga alt thread, inainte ca primul s a inceapa citirea. 4. Rezolvati problema de la subpunctul de mai sus: ? in metoda read, inaintea invocarii pool-ului, dezactivati interesul pentru citi re. ? la sfarsitul metodei run a obiectului Runnable, reactivati interesul pentru cit ire, si apelati key.selector().wakeup() pentru a trezi selectorul, acesta reintr and in select cu interesul actualizat.

4(2p). Sockets - client (bonus) 1. Creati o clasa, Client, ce se va conecta la server-ul instantiat la problema 2. Utilizati, ca punct de plecare, clasa Server.(Puteti utiliza codul de la aceast a clasa)

2. In metoda main, initiati conexiunea cu server-ul: ? obtineti un nou socket channel, apeland open ? configurati-l in mod neblocant ? apelati connect, cu parametru InetSocketAddress, ce indica adresa server-ului ? inregistrati-l la selector, cu interes OP_CONNECT 3. Realizati modificarile necesare pentru finalizarea procesului de conectare la s erver-ul: ? in bucla while adaugati un test key.isConnectable(), si apelati, in caz afirmat iv, metoda connect(key), pe care o veti implementa (asemanator cu accept, read, write, de la problema 2). ? implementati metoda connect, astfel incat sa apeleze finishConnect pe canalul o btinut din cheie. Acest apel este necesar pentru finalizarea procesului de stabi lire a legaturii. 4. Rulati server-ul si clientul. Primul ar trebui sa afiseze adresa clientului.

S-ar putea să vă placă și