Download presentation
Presentation is loading. Please wait.
1
glava 12. Non-Blocking I/O
Mrežno računarstvo glava 12. Non-Blocking I/O
2
Uvodna priča U poređenju sa CPU ili čak diskovima, mreže su spore.
Svi se oni vremenom ubrzavaju, ali verovatno je da CPU i diskovi ostaju nekoliko redova veličine brži od mreža Poslednje što želimo pod tim okolnostima je da brzi CPU čeka relativno sporu mrežu
3
Tradicionalno Java rešenje problema je kombinacija baferovanja i multithreading-a.
Veći broj niti može generisati podatke za nekoliko različitih konekcija odjednom i smestiti ih u bafere dok mreža ne postane zaista spremna da ih pošalje Ovakav pristup funkcioniše za prilično jednostavne servere i klijente koji nemaju potrebe za ekstremnim performansama Pa ipak vreme potrebno za kreiranje niti i njihovo upravljanje nije zanemarljivo. Npr. svaka nit zahteva oko 1M dodatnog RAM-a
4
Na velikim serverima, koji mogu procesirati hiljade zahteva u sekundi, bolje je ne pridruživati po nit svakoj konekciji, čak i kada su te niti ”reusable” (poglavlje 5). Upravljanje nitima jako degradira performanse sistema Brže je ako jedna nit može preuzeti odgovornost za veći broj konekcija, odabere jednu koja je spremna da primi podatke, napuni je što je moguće brže sa što je moguće više podataka koje konekcija može da obradi i pređe na sledeću spremnu konekciju
5
Da bi ovaj pristup dobro radio, mora biti podržan od strane operativnog sistema
Srećom, skoro svi moderni sistemi koji se koriste na serverima podržavaju takav neblokirajući I/O Međutim, neki klijentski sistemi od interesa, poput PDA, mobilnih telefona i sličnih možda nemaju odgovarajuću podršku Ipak, ceo novi I/O API je i dizajniran jedino za servere
6
klijent i čak peer-to-peer sistemi retko treba da procesiraju toliko mnogo simultanih konekcija da multithreading stream based I/O postane usko grlo Postoje neki izuzeci (web spider poput Google-a), ali za većinu klijenata novi I/O API je nepotreban i nije vredan dodatne složenosti koju iziskuje
7
Primer klijenta iako novi I/O API-ji nisu posebno dizajnirani za klijente, rade i za njih prvi primer je sa klijentom, jer je jednostavniji: mnogi klijenti mogu se implementirati korišćenjem jedne konekcije, pa je moguće uvesti kanale i bafere pre priče o selektorima i neblokirajućem I/O
8
Primer: protokol generisanja karaktera
jednostavan klijent za protokol generisanja karaktera definisan u RFC 864 ovaj protokol je dizajniran za testiranje klijenata server osluškuje konekcije na portu 19 kada se klijent konektuje, server šalje sekvencu karaktera dok klijent ne prekine konekciju. Svaki ulaz klijenta se ignoriše
9
RFC ne određuje koja sekvenca karaktera se šalje, ali preporučuje da server koristi prepoznatljivu šemu Jedna uobičajena šema je rotirajuća 72 karaktera carriage return/line feed (od 95 štampajućih ASCII karaktera) ovaj protokol je izabran za primere u ovom poglavlju, jer su i protokol slanja podataka i algoritam za njihovo generisanje dovoljno prosti da ne bace u zasenak I/O. Međutim, moguće je odaslati mnogo podataka preko relativno malog broja konekcija i brzo zasititi mrežnu konekciju, tako da je ovo dobar kandidat za novi I/O API.
10
Kreiranje kanala Kada se klijent implementira pomoću new I/O API-ja, najpre se poziva statički metod SocketChannel.open() da kreira java.nio.channels.SocketChannel objekat Argument ovog metoda je java.net.SocketAddress objekat koji ukazuje na host i port na koji se konektujemo
11
npr. sledeći fragment se konektuje na rama.poly.edu na port 19
SocketAddress rama = new InetSocketAddress("rama.poly.edu",19); SocketChannel client = SocketChannel.open(rama); Kanal se otvara u blokirajućem modu. Naredna linija koda neće se izvršiti dok se ne uspostavi konekcija. Ako se konekcija ne može uspostaviti, izbacuje se IOException.
12
Kreiranje bafera Da je u pitanju tradicionalni klijent, sada bismo tražili input i output stream-ove soketa. Ali nije. Kada imamo kanal, pišemo direktno u sam taj kanal. Umesto pisanja byte nizova, pišemo ByteBuffer objekte Jasno je da su linije duge 74 ASCII karaktera (72 štampajuća karaktera za kojima sledi par carriage return/linefeed), tako da kreiramo ByteBuffer koji ima kapacitet 74 bajta korišćenjem statičkog allocate() metoda: ByteBuffer buffer = ByteBuffer.allocate(74);
13
read() Ovaj ByteBuffer objekat prosleđujemo metodu read() kanala. Kanal puni ovaj bafer podacima koje čita iz soketa. Vraća broj bajtova uspešno pročitanih i smeštenih u bafer. int bytesRead = client.read(buffer); Po default-u, ovo će pročitati bar jedan bajt ili vratiti -1 da ukaže da nema više podataka, upravo kao što radi i InputStream često će pročitati više bajtova, ako je više bajtova dostupno za čitanje metod takođe može izbaciti IOException ako nešto pođe po zlu prilikom čitanja
14
System.out izlazni kanal
Postoje načini da se iz ByteBuffer ekstrahuje byte niz koji se potom može pisati na tradicionalni OutputStream poput System.out Ipak, informativnije je držati se čistog, channel-based rešenja takvo rešenje zahteva wrapping OutputStream-a System.out u kanal korišćenjem klase Channels, odnosno njenog metoda newChannel(): WritableByteChannel output = Channels.newChannel(System.out);
15
Pisanje u kanal: flip(), write()
Zatim se pročitani podaci mogu pisati u ovaj output kanal povezan sa System.out Međutim, pre toga, neophodno je preokrenuti (eng. flip) bafer tako da izlazni kanal počne od početka podataka radije nego od njihovog kraja buffer.flip(); output.write(buffer);
16
clear() Izlaznom kanalu ne moramo reći koliko bajtova da piše. Baferi prate koliko bajtova sadrže. Ipak, uopšteno, ne garantuje se da će izlazni kanal ispisati sve bajtove iz bafera U ovom posebnom slučaju, kanal je blokirajući i ili će ispisati sve ili izbaciti IOException Ne treba kreirati novi bafer za svako čitanje i pisanje. To bi ubilo performanse. Umesto toga, koristiti ponovi isti bafer. Treba očistiti bafer pre ponovnog čitanja u njega: buffer.clear();
17
clear() je malo drugačiji od flip()
flip ostavlja podatke u baferu netaknutim, ali ih priprema za pisanje, radije nego za čitanje. clear resetuje bafer u pređašnje stanje (to je pojednostavljeno. stari podaci su još uvek prisutni, nisu prepisani, ali biće prepisani novim pročitanim podacima primer: kako je dizajniran kao beskonačan protokol, program se mora završiti sa Ctrl-C!!!
18
ovo je samo alternativa programa koji se lako mogao napisati i korišćenjem stream-ova
nove mogućnosti nastaju kada želimo da klijent radi još nešto osim kopiranja svog ulaza na izlaz. konekcija se može pokrenuti bilo u blokirajućem bilo u neblokirajućem modu u kome se read() završava neposredno čak i ako nema raspoloživih podataka. To dopušta programu da radi nešto drugo pre no što pokuša da čita. Ne mora da čeka na sporu mrežnu konekciju
19
client.configureBlocking(false);
Neblokirajući mod za promenu blokirajućeg moda, true (blokirajući), odnosno false (neblokirajući) se prosledi metodu configureBlocking() učinimo konekciju neblokirajućom: client.configureBlocking(false); U neblokirajućem modu, read() može vratiti 0 jer nije pročitao ništa. Petlja izgleda malo drugačije Primer (pod komentarom)
20
Primer servera Kanali i baferi su zapravo namenjeni serverskim sistemima koji treba da procesiraju mnogo simultanih konekcija efikasno. Za servere su, pored kanala i bafera, korišćenih za klijente, neophodni i selektori koji omogućavaju serveru da nađe sve konekcije koje su spremne da prihvate ulaz ili pošalju izlaz
21
Kreiranje serverskog kanala
Pišemo jednostavni server za protokol generisanja karaktera. Kada se piše server koji koristi new I/0, počinje se pozivom statičkog metoda ServerSocketChannel.open() da bi se kreirao ServerSocketChannel objekat ServerSocketChannel serverChannel = ServerSocketChannel.open();
22
Inicijalno, ovaj kanal ne sluša ni na jednom portu
Inicijalno, ovaj kanal ne sluša ni na jednom portu. Da bi se vezao za port, dohvati se njegov ServerSocket peer objekat metodom socket(), a zatim koristi bind() metod na tom peer-u. Npr. sledeći fragment koda vezuje kanal za serverski soket na portu 19: ServerSocket ss = serverChannel.socket(); ss.bind(new InetSocketAddress(19)); Kao i kod regularnih soketa, vezivanje na port 19 zahteva root privilegije na Unix-u. Za portove počev od 1024 nisu potrebne root privilegije
23
Prihvatanje konekcije
Serverski socket kanal sada osluškuje dolazeće konekcije na portu 19. Da bi prihvatio neku, poziva accept() metod, koji vraća SocketChannel objekat: SocketChannel clientChannel = serverChannel.accept(); Na serverskoj strani, definitivno želimo da učinimo klijentski kanal neblokirajućim kako bismo omogućili da server procesira veći broj simultanih konekcija: clientChannel.configureBlocking(false);
24
Neblokirajući serverski kanal
Možemo takođe učiniti ServerSocketChannel neblokirajućim. Podrazumevano, accept() metod blokira dok se ne pojavi dolazeća konekcija, kao i accept() metod klase ServerSocket. Da bi se ovo promenilo, prosto se pozove configureBlocking(false) pre poziva accept(): serverChannel.configureBlocking(false); neblokirajući accept() vraća null skoro neposredno ako nema dolazećih konekcija. Osigurajte se da proveravate to, jer ćete dobiti NullPointerException kada pokušate da koristite taj soket.
25
Selector Sada imamo dva otvorena kanala: serverski kanal i klijentski kanal. Oba treba da se procesiraju. Oba se mogu izvršavati neograničeno. U tradicionalnom pristupu, svakoj konekciji pridružuje se nit, i broj niti rapidno raste kako se klijenti konektuju. Nasuprot tome, u novom I/O API-ju, kreira se Selector koji omogućuje da program iterira preko svih konekcija koje su spremne za procesiranje
26
za konstruisanje novog Selector-a, poziva se statički Selector
za konstruisanje novog Selector-a, poziva se statički Selector.open() metod: Selector selector = Selector.open(); Zatim je potrebno registrovati svaki kanal kod selektora koji ga prati, korišćenjem register() metoda kanala Prilikom registracije, zadaje se operacija za koju smo zainteresovani korišćenjem imenovane konstante iz klase SelectionKey. Za serverski soket, jedina operacija od interesa je OP_ACCEPT, tj. da li je serverski soket spreman da prihvati novu konekciju serverChannel.register(selector, SelectionKey.OP_ACCEPT);
27
Za klijentske kanale, želimo da znamo nešto drugo, tj
Za klijentske kanale, želimo da znamo nešto drugo, tj. da li su oni spremni da se na njih upišu podaci. Za to se koristi OP_WRITE: SelectionKey key2 = clientChannel.register(selector, SelectionKey.OP_WRITE); Oba register() metoda vraćaju SelectionKey objekat. Međutim, ovaj objekat ćemo koristiti samo za klijentske kanale, jer ih može biti više od jednog. Svaki SelectionKey objekat ima prikačen proizvoljan Object tip. Uobičajeno, on se koristi za čuvanje objekta koji ukazuje na tekuće stanje konekcije. U ovom slučaju, možemo čuvati bafer koji kanal ispisuje u mrežu. Nakon što se bafer u potpunosti isprazni, ponovo ga napunimo. Jedan niz se napuni podacima koji će biti kopirani u svaki bafer. Radije nego da pišemo na kraj bafera, a zatim premotamo na početak bafera pa pišemo ponovo, lakše je početi od dve uzastopne kopije podataka, tako da je svaka linija dostupna kao neprekidna sekvenca u nizu:
28
byte[] rotation = new byte[95*2];
for( byte i = ’ ’; i <= ’~’; i++ ) { rotation[i-’ ’] = i; rotation[i+95-’ ’] = i; } Pošto će ovaj niz biti samo čitan nakon što je inicijalizovan, možemo ga koristiti za veći broj kanala. Međutim, svaki kanal će dobiti svoj sopstveni bafer ispunjen sadržajem ovog niza. Ispunićemo bafer sa prva 72 bajta niza rotation, a potom dodati carriage return/linefeed par za kraj linije. Zatim okrenemo (flip) bafer tako da je spreman za pražnjenje, i prikačimo ga ključu kanala:
29
ByteBuffer buffer = ByteBuffer.allocate(74);
buffer.put(rotation, 0, 72); buffer.put((byte) ’\r’); buffer.put((byte) ’\n’); buffer.flip(); key2.attach(buffer);
30
Za proveru da li je išta spremno, poziva se metod select() selektora
Za proveru da li je išta spremno, poziva se metod select() selektora. Za server koji se dugo izvršava, to normalno ide u beskonačnu petlju: while(true){ selector.select(); // process selected keys… }
31
Pod pretpostavkom da selektor nađe spreman kanal, njegov selectedKeys() metod vraća java.util.Set koji sadrži po jedan SelectionKey objekat za svaki spreman kanal. Inače, on vraća prazan skup U oba slučaja, možemo proći kroz skup pomoću java.util.Iterator-a:
32
Set readyKeys = selector.selectedKeys();
Iterator iterator = readyKeys.iterator(); while(iterator.hasNext()){ SelectionKey key = (SelectionKey)(iterator.next()); // Remove key from set so we don’t // process it twice iterator.remove(); // operate on the channel... }
33
Uklanjanje ključa iz skupa
Uklanjanje ključa iz skupa kaže selektoru da smo radili sa njim, i da Selektor ne treba da nam ga vraća svaki put kada pozovemo select(). Selector će vratiti kanal nazad u skup kada se select() pozove ponovo ako je kanal ponovo spreman. Zato je vrlo važno ukloniti ključ iz skupa spremnih.
34
Ako je spreman serverski kanal, program prihvata novi socket kanal i dodaje ga u selektor.
Ako je spreman socket kanal, program piše što je moguće više iz bafera u njega Ako nema spremnih kanala, selektor čeka na neki da postane spreman. Jedna nit, glavna nit, procesira veći broj simultanih konekcija
35
U ovom slučaju, lako je reći da li je klijentski ili serverski kanal selektovan, pošto je serverski kanal jedino spreman za prihvatanje, a klijentski kanal jedino za pisanje. Obe ove stvari jesu I/O operacije, i obe mogu izbaciti IOException iz mnoštva razloga, pa ćemo morati da imamo try-catch blok:
36
try{ if(key.isAcceptable()){ ServerSocketChannel server = (ServerSocketChannel)key.channel(); SocketChannel connection = server.accept(); connection.configureBlocking(false); connection.register(selector,SelectionKey.OP_WRITE); // set up the buffer for the client... } else if(key.isWritable()){ SocketChannel client = (SocketChannel)key.channel(); // write data to client...
37
Pisanje u kanal Pisanje podataka u kanal je jednostavno. Dohvati se prilog ključa, kastuje u ByteBuffer i pozove hasRemaining() kako bi se proverilo da li je ostalo podataka u baferu koji nisu ispisani. Ako ima, pišemo ih. Inače, ponovo napunimo bafer sledećom linijom podataka iz niza rotation i pišemo to.
38
ByteBuffer buffer = (ByteBuffer) key.attachment();
if(!buffer.hasRemaining()){ // Refill the buffer with the next line // Figure out where the last line started buffer.rewind(); int first = buffer.get(); // Increment to the next character int position = first - ’ ’ + 1; buffer.put(rotation, position, 72); buffer.put((byte) ’\r’); buffer.put((byte) ’\n’); buffer.flip(); } client.write(buffer);
39
Algoritam koji određuje gde da dohvati sledeću liniju zasniva se na činjenici da su karakteri smešteni u nizu rotation u ASCII poretku. buffer.get() nakon buffer.rewind() čita prvi bajt podataka iz bafera. Od ovog broja oduzima se blanko (32) jer je to prvi karakter u nizu rotation. Ovo nam kaže od kog indeksa u nizu počinje bafer. Dodamo 1 da nadjemo početak naredne linije i ponovo napunimo bafer
40
u ”chargen” protokolu server nikada ne zatvara konekciju
u ”chargen” protokolu server nikada ne zatvara konekciju. On čeka da klijent prekine soket. Kada se to desi, biva izbačen izuzetak. Poništava se ključ i zatvara odgovarajući kanal: catch(IOException){ key.cancel(); try{ // You can still get the channel from // the key after cancelling the key key.channel().close(); }catch(IOException ex) {} } Primer: ChargenServer
41
Ovaj primer koristi samo jednu nit.
Postoje situacije kada možda želimo da koristimo veći broj niti, posebno ako različite operacije imaju različite prioritete. Npr. možda želimo da prihvatamo nove konekcije u jednoj niti visokog prioriteta, a servisiramo postojeće konekcije u niti nižeg prioriteta. Međutim, više ne moramo imati 1:1 odnos između niti i konekcija, što dramatično poboljšava skalabilnost servera pisanih u Javi.
42
Takođe, može biti od značaja korišćenje većeg broja niti za postizanje maksimalnih performansi.
Veći broj niti omogućava serveru da iskoristi veći broj procesora. Čak i sa jednim procesorom, često je dobra ideja razdvojiti prihvatajuću od procesirajućih niti Thread pools (glava 5) su relevantni čak i sa novim I/O modelom. Nit koja prihvata konekcije može dodavati prihvaćene konekcije u red. Međutim, problemi sinhronizacije su prilično mučni.
43
BAFERI U glavi 4 preporučeno je da se stream-ovi uvek baferišu.
Skoro ništa nema većeg uticaja na performanse mrežnih programa od dovoljno velikog bafera U novom I/O modelu, međutim, više nam nije dat izbor. Sav I/O je baferisan Zapravo, baferi su osnovni delovi API-ja Umesto pisanja podataka u izlazne tokove i čitanja iz ulaznih, čitamo i pišemo podatke u bafere. Baferi mogu biti nizovi bajtova, kao u baferisanim tokovima, ali native implementacije ih mogu povezati direktno sa hardverom ili memorijom, ili koristiti druge, veoma efikasne, implementacije
44
Iz programerske perspektive, ključna razlika između stream-ova i kanala je u tome što su stream-ovi byte-based, dok su kanali block-based. Stream je dizajniran da obezbedi bajt po bajt. Mogu biti prosleđeni nizovi bajtova, zbog performansi. Međutim, osnovna namera je prosleđivanje podataka bajt po bajt. Suprotno, kanal prosleđuje blokove podataka u baferima. Pre no što bajtovi mogu da se pročitaju iz ili upišu u kanal, moraju biti smešteni u bafer, i podaci se čitaju i pišu bafer po bafer.
45
Ako pokušamo da pišemo u kanal koji je read-only ili da čitamo iz write-only kanala, biće izbačen UnsupportedOperationException. Ipak, češće nego što ne mogu, mrežni programi mogu čitati i pisati u isti kanal o baferu se može razmišljati kao o listi elemenata fiksirane veličine određenog, obično primitivnog, tipa, poput niza. Međutim, nije nužno da je to u stvarnosti baš niz. Nekad jeste, nekad nije.
46
Postoje određene potklase klase Buffer za sve Java primitivne tipove, osim boolean: ByteBuffer, CharBuffer, ShortBuffer, IntBuffer, LongBuffer, FloatBuffer i DoubleBuffer U svakoj od ovih klasa metodi prihvataju i vraćaju vrednosti odgovarajućeg tipa Zajednička klasa Buffer obezbeđuje samo metode koji ne moraju znati tip podataka koje bafer sadrži. Mrežni programi skoro isključivo koriste ByteBuffer.
47
Osim svoje liste podataka, svaki bafer prati 4 ključne informacije
Svi baferi imaju iste metode za postavljanje i očitavanje ovih vrednosti, bez obzira na tip bafera: position – sledeća lokacija u baferu sa koje će biti čitano ili na koju će biti pisano. Kreće od 0. (metodi position()...) capacity – maksimalan broj elemenata koje bafer može sadržati. Ovo se postavlja prilikom kreiranja bafera i kasnije se ne može promeniti (metod capacity()...)
48
limit – poslednja pozicija u baferu koja može sadržati podatke
limit – poslednja pozicija u baferu koja može sadržati podatke. Nije moguće čitati ili pisati iza ove pozicije bez promene limita, čak i ako bafer ima još kapaciteta (metodi limit()...) mark – klijentom određen indeks u baferu. Postavlja se na tekuću poziciju pozivom mark() metoda. Tekuća pozicija se postavlja na označenu pozivom reset()
49
Za razliku od čitanja iz InputStream, čitanje iz bafera zapravo ne menja podatke u baferu ni na koji način. Moguće je postaviti poziciju bilo ispred ili iza, tako da je moguće početi čitanje od određenog mesta u baferu. Slično, program može podesiti limit kako bi kontrolisao kraj podataka koji će biti pročitani. Samo je kapacitet fiksiran.
50
clear() Superklasa Buffer takođe obezbeđuje i nekoliko metoda koji operišu ovim zajedničkim svojsvima Metod clear() ”prazni” bafer postavljanjem pozicije na 0, a limit-a na capacity. Ovo omogućuje da bafer bude u potpunosti ponovo napunjen Međutim, clear() metod ne uklanja stare podatke iz bafera. Oni su još uvek prisutni i mogu biti pročitani korišćenjem apsolutnih get() metoda ili ponovnom promenom limita i pozicije
51
rewind(), flip() rewind() metod postavlja poziciju na nulu, ali ne menja limit. Ovo omogućuje da bafer bude pročitan iznova flip() metod postavlja limit na tekuću poziciju i poziciju na 0. Poziva se kada želimo da ispraznimo bafer koji smo upravo napunili
52
Konačno, postoje 2 metoda koja vraćaju informaciju o baferu, a ne menjaju ga.
remaining() metod vraća broj elemenata u baferu između tekuće pozicije i limita hasRemaining() metod vraća true ako je broj preostalih elemenata veći od 0
53
Kreiranje bafera Prazni baferi se kreiraju metodima allocate().
Baferi koji su napunjeni podacima kreiraju se wrap() metodima. allocate metodi su često korisni za ulaz, dok su wrap metodi obično korišćeni za izlaz
54
Allocation osnovni allocate() metod prosto vraća novi prazan bafer zadatog fiksnog kapaciteta sledeće linije kreiraju byte i int bafere, svaki veličine 100: ByteBuffer buffer1 = ByteBuffer.allocate(100); IntBuffer buffer2 = IntBuffer.allocate(100); Kursor je pozicioniran na početak bafera, tj. position je 0.
55
Bafer kreiran allocate() metodom će biti implementiran povrh Java niza, kome se može pristupiti pomoću array() i arrayOffset() metoda Npr. može se pročitati veliki komad podataka u bafer koristeći kanal i potom dohvatiti niz iz bafera da bi se prosledio drugim metodima: byte[] data1 = buffer1.array(); int[] data2 = buffer2.array(); metod array() otkriva privatne podatke bafera, pa ga koristiti oprezno. Promene na nizu odražavaju se na bafer i obrnuto. Uobičajeno je napuniti bafer podacima, dohvatiti niz i zatim raditi nad nizom. Nema problema sve dok ne pišemo u bafer nakon što smo počeli raditi sa nizom
56
Direct allocation ByteBuffer klasa (ali ne i ostale bafer-klase) ima dodatni allocateDirect() metod koji možda ne kreira niz za bafer. VM može da implementira direktno alociran ByteBuffer korišćenjem direktnog pristupa memoriji bafera na Ethernet karti, kernel memoriji ili nečemu drugom. Ne zahteva se, ali je dopušteno, i može poboljšati performanse I/O operacija. ByteBuffer buffer1 = ByteBuffer.allocateDirect(100); Poziv array() i arrayOffset() na direktnom baferu izbacuje UnsupportedOperationException. Direktni baferi mogu biti brži na nekim VM, posebno ako je bafer veliki (MB ili veći). Međutim, direktni baferi su skuplji za kreiranje od indirektnih, pa ih treba alocirati kada bafer treba da postoji neko vreme. Detalji su krajnje VM-zavisni. (ne koristiti ako baš ne postoji potreba)
57
wrapping ako već imamo niz podataka koje želimo da ispišemo, izgradimo bafer oko njega, pre nego da alociramo novi bafer i kopiramo komponente u bafer jednu po jednu npr. byte[] data = ”Some data”.getBytes(”UTF-8”); ByteBuffer buffer1 = ByteBuffer.wrap(data); char[] text = ”Some text”.toCharArray(); CharBuffer buffer2 = CharBuffer.wrap(text); Ovde bafer sadrži referencu na niz. Baferi kreirani wrap-iranjem nisu nikada direktni. Ponovo, promene na nizu odražavaju se na bafer, i obrnuto, tako da ne wrap-ujte niz dok niste završili sa njim
58
punjenje i pražnjenje baferi su dizajnirani za sekvencijalni pristup
pored liste podataka, svaki bafer ima kursor koji ukazuje na tekuću poziciju. Kursor je int koji se broji od 0. Kursor se inkrementira za jedan kada se element pročita iz ili upiše u bafer. Takođe, može se pozicionirati i ručno.
59
pretpostavimo da želimo da obrnemo karaktere u stringu
pretpostavimo da želimo da obrnemo karaktere u stringu. Posoji tuce različitih načina da se to uradi, uključujući string bafere, char[] nizove, povezane liste itd. Međutim, ako radimo koristeći CharBuffer, mogli bismo da počnemo sa punjenjem bafera podacima iz stringa: String s = ”Some text”; CharBuffer buffer = CharBuffer.wrap(s);
60
Bafer možemo napuniti samo do njegovog limita
Bafer možemo napuniti samo do njegovog limita. Ako probamo više, put() metod izbacuje BufferOverflowException. Slično, ako probamo get() iza limita, izbacuje se BufferUnderflowException. Pre nego pročitamo podatke nazad, moramo da obrnemo bafer: buffer.flip(); Ovo repozicionira kursor na početak bafera.
61
Možemo isprazniti bafer u novi string:
String result = ””; while(buffer.hasRemaining()){ result += buffer.get(); } Bafer klase takođe imaju apsolutne metode koji pune i prazne na određenim pozicijama unutar bafera bez ažuriranja kursora. Npr. ByteBuffer ima sledeća 2: public abstract byte get(int index); public abstract ByteBuffer put(int index, byte b);
62
oba ova metoda izbacuju IndexOutOfBoundsException ako probamo pristup poziciji iza limita bafera
Npr. korišćenjem apsolutnih metoda, mogli bismo ovako da obrnemo string: String s = ”Some text”; CharBuffer buffer = CharBuffer.allocate(s.length()); for(int i=0; i < s.length(); i++) { buffer.put(s.length()-i-1, s.charAt(i)); }
63
Bulk metodi Čak i sa baferima, često je brže raditi sa blokovima podataka nego puniti i prazniti element po element Razne bafer klase imaju tzv. ”bulk” metode koji pune i prazne niz elemenata njihovog tipa public ByteBuffer get(byte[] dst, int offset, int length); public ByteBuffer get(byte[] dst); public ByteBuffer put(byte[] array, int offset, int length); public ByteBuffer put(byte[] array);
64
ovi put() metodi ubacuju podatke iz zadatog niza ili podniza, počev od tekuće pozicije.
get() metodi čitaju podatke u argument niz ili podniz počev od tekuće pozicije i put() i get() inkrementiraju poziciju za dužinu niza ili podniza put() metodi izbacuju BufferOverflowException ako bafer nema dovoljno mesta za niz ili podniz. get() metodi izbacuju BufferUnderflowException ako bafer nema dovoljno podataka preostalih za popunjavanje niza ili podniza. Ovo su runtime izuzeci
65
Data conversion Svi podaci u Javi na kraju se razrešavaju u bajtove. Svaki primitivni tip – int, double, float, itd. može se zapisati bajtovima. Bilo koja sekvenca bajtova prave dužine može se interpretirati kao primitivni podatak. Npr. proizvoljna sekvenca od 4 bajta odgovara int-u ili float-u (u stvari, oboma, u zavisnosti od toga kako hoćemo da je pročitamo) ByteBuffer klasa (i samo ta klasa) obezbeđuje relativne i apsolutne put() metode koji pune bafer bajtovima koji odgovaraju argumentu primitivnog tipa (osim boolean) i relativne i apsolutne get() metode koji čitaju odgovarajući broj bajtova da oforme primitivni podatak. spisak metoda (page 21 of 49) i (page 22 of 49) getChar(), putChar(), putShort(), getShort()...
66
ovi metodi u svetu novog I/O rade posao koji u tradicionalnom I/O rade DataOutputStream i DataInputStream Ovi metodi imaju dodatnu mogućnost koju DataOutputStream i DataInputStream nemaju. Može se birati da li da se sekvenca bajtova interpretira kao big-endian ili little-endian int, float, double, itd. Podrazumevano, sve vrednosti se čitaju i pišu kao big-endian, tj. bajt najveće težine prvi. Dva order() metoda očitavaju i podešavaju poredak bajtova u baferu, koristeći imenovane konstante klase ByteOrder.
67
Primer: Intgen Npr. može se promeniti bafer u little-endian interpretaciju: if(buffer.order().equals(ByteOrder.BIG_ENDIAN)) { buffer.order(ByteOrder.LITTLE_ENDIAN); } Pretpostavimo da umesto chargen protokola želimo da testiramo mrežu generisanjem binarnih podataka. Ovaj test može osvetliti probleme koji nisu očigledni u ASCII chargen protokolu, poput starog gateway-a konfigurisanog da odbaci bit najveće težine svakog bajta, odbaci svaki 230 bajt ili pređe u mod dijagnostifikovanja neočekivanom sekvencom kontrolnih karaktera. Ovo nisu teoretski problemi.
68
Mreža se može testirati na ovakve probleme slanjem svakog mogućeg int-a.
oko 4.2 biliona iteracija – testiranje svake moguće 4-bajtne sekvence na primajućem kraju, može se lako testirati da li su primljeni podaci očekivani jednostavnim numeričkim poređenjem. ako se pronađe bilo koji problem, lako je reći tačno gde su se desili.
69
Drugim rečima, ovaj protokol (Intgen) ponaša se ovako:
klijent se konektuje na server server odmah počinje slanje 4-bajtnih, big-endian integer-a, počev od 0 i inkrementirajući za 1 svaki put. Server će eventualno zaokrenuti u negativne brojeve server se izvršava neograničeno. Klijent zatvara konekciju kada mu je dovoljno
70
Server smešta tekući int u 4-bajtni ByteBuffer
po jedan bafer se prikači uz svaki kanal kada kanal postane dostupan za pisanje, bafer se prazni na kanal zatim se bafer premota (rewind) i sadržaj bafera pročita sa getInt(). Program zatim čisti bafer (clear), inkrementira prethodna vrednost za 1 i puni bafer novom vrednošću koristeći putInt(). Konačno, bafer se okreće (flip) tako da bude spreman da se isprazni sledeći put kada kanal postane spreman za pisanje. Primer: IntgenServer
71
View Buffers Ako znamo da ByteBuffer pročitan iz SocketChannel-a ne sadrži ništa osim elemenata jednog primitivnog tipa, može biti vredno kreiranje view bafera. to je novi Buffer objekat odgovarajućeg tipa poput DoubleBuffer, IntBuffer, itd. koji izvlači podatke iz ByteBuffer-a počev od tekuće pozicije. Promene na view baferu odražavaju se na bafer, i obrnuto. Međutim, svaki bafer ima svoj nezavisni limit, kapacitet, oznaku i poziciju.
72
View baferi se kreiraju jednim od sledećih 6 metoda u ByteBuffer:
public abstract ShortBuffer asShortBuffer() public abstract CharBuffer asCharBuffer() public abstract IntBuffer asIntBuffer() public abstract LongBuffer asLongBuffer() public abstract FloatBuffer asFloatBuffer() public abstract DoubleBuffer asDoubleBuffer()
73
primer: IntgenClient Razmotrimo klijent za Intgen protokol
protokol samo čita int-ove, pa je od pomoći koristiti IntBuffer umesto ByteBuffer. Za promenu, klijent je sinhron i blokirajući, ali i dalje koristi kanale i bafere Napomena: iako možemo puniti i prazniti bafere koristeći isključivo metode klase IntBuffer, podaci moraju biti čitani i pisani u kanal koristeći originalni ByteBuffer čiji je IntBuffer pogled. SocketChannel klasa ima samo metode za čitanje i pisanje ByteBuffer-a. Ne može čitati i pisati bilo koju drugu vrstu bafera. Ovo takođe znači da moramo da očistimo ByteBuffer u svakom prolazu kroz petlju ili će se bafer napuniti i program stati. Pozicije i limiti dva bafera su nezavisni i moraju se razmatrati odvojeno. Konačno, ako radimo u neblokirajućem modu, potrebno je da pazimo da su svi podaci odgovarajućeg ByteBuffer-a ispražnjeni pre čitanja ili pisanja iz view buffer-a. Neblokirajući mod ne obezbeđuje garanciju da će bafer i dalje biti poravnat po int/double/char itd. granicama nakon pražnjenja. Sasvim je moguće da neblokirajući kanal piše pola bajta u int ili double. Kada se koristi neblokirajući I/O, proveriti ovaj problem, pre stavljanja još podataka u view buffer.
74
Compacting Buffer Većina writable buffer-a podržava compact() metod
public abstract ByteBuffer compact() IntBuffer ShortBuffer, FloatBuffer, Char..., Double... Compacting šiftuje bilo koje preostale podatke u baferu na početak bafera, oslobađajući još prostora za elemente. Svi podaci na tim pozicijama biće prepisani. Pozicija bafera se postavlja na kraj podataka, tako da je spremno za upis još podataka
75
Compacting je posebno korisna operacija kada kopiramo – čitamo iz jednog kanala i pišemo podatke u drugi koristeći neblokirajući I/O. Možemo pročitati neke podatke u bafer, ispisati bafer ponovo, a zatim izvršiti compact podataka tako da svi podaci koji nisu ispisani budu na početku bafera, a pozicija je na kraju podataka ostalih u baferu, spremna da se primi još podataka. ovo omogućava da čitanja i pisanja budu isprepletana manje više slučajno sa samo jednim baferom. Nekoliko čitanja se može desiti uzastopno, za kojima sledi nekoliko uzastopnih pisanja. Ako je mreža spremna za neposredni ispis, ali ne i učitavanje (ili obrnuto), program može koristiti prednost toga.
76
Primer: echo server Ova tehnika se može iskoristiti za implementiranje echo servera echo protokol prosto odgovara klijentu podacima koje je klijent poslao kao i chargen, koristan je za testiranje mreže takođe, slično chargen-u, echo računa da klijent zatvara konekciju za razliku od chargen-a, echo server mora i da čita i da piše u konekciju
77
veličina bafera je jako bitna
veliki bafer može sakriti dosta bug-ova ako je bafer dovoljno velik da sadrži kompletne test-slučajeve bez obrtanja i pražnjenja, vrlo lako je moguće ne primetiti da bafer nije obrnut ili compacted u pravo vreme jer test-slučajevi zapravo nikada nemaju potrebu za tim. probati sa manjom veličinom bafera (znatno manjom od očekivanog ulaza) To značajno degradira performanse.
78
Duplicating buffers Često je poželjno napraviti kopiju bafera radi isporuke iste informacije u 2 ili više kanala metodi duplicate() u svakoj od 6 bafer klasa to rade duplikati bafera dele iste podatke, uključujući isti niz, ako bafer nije direktan. Promene podataka u jednom baferu odražavaju se na ostale bafere. Zato se ovaj metod treba uglavnom koristiti kada se podaci samo čitaju iz bafera. Inače, može biti čupavo odrediti gde su podaci modifikovani. originalni i duplirani baferi imaju nezavisne markere, limite i pozicije čak i kada dele iste podatke. Jedan bafer može žuriti ili kasniti za drugim.
79
Dupliranje je korisno kada se žele poslati isti podaci preko većeg broja kanala (paralelno). Mogu se napraviti duplikati glavnog bafera za svaki kanal i omogućiti da svaki kanal radi svojom brzinom. primer: NonBlockingSingleFileHTTPServer fajl koji treba servirati čuva se u konstanti, read-only baferu. Svaki put kada se klijent konektuje, program pravi duplikat ovog bafera samo za taj kanal i čuva ga kao attachment kanala. Bez duplikata, jedan klijent mora da čeka dok drugi završi da bi se originalni bafer preokrenuo. Duplikati omogućuju simultanu reupotrebu bafera.
80
Slicing buffers neznatna varijanta dupliranja
takođe kreira novi bafer koji deli iste podatke sa starim baferom ipak, inicijalna pozicija parčeta je tekuća pozicija originalnog bafera parče je poput podsekvence originalnog bafera koja samo sadrži elemente od tekuće pozicije do limita premotavanje dela ga samo pomera na poziciju originalnog bafera kada je parče kreirano. Parče ne može videti ništa od originalnog bafera pre te pozicije korisno je kada imamo dug bafer podataka koji se lako deli u veći broj delova, poput protokol header-a za kojim slede podaci
81
Marking and reseting baferi mogu biti označeni i resetovani kada želimo da ponovo pročitamo neke podatke ovo je moguće za sve bafere mark() i reset() metodi OBJECT METHODS equals(), hashCode(), toString() Comparable<>, compareTo() ne: Serializable, Cloneable page 40 of 49
82
CHANNELS Kanali pomeraju blokove podataka u i iz bafera u ili iz raznih I/O izvora poput fajlova, soketa, datagrama itd. Za svrhe mrežnog programiranja, bitne su 3 klase: SocketChannel, ServerSocketChannel i DatagramChannel Za TCP konekcije, o kojima smo do sada pričali, potrebne su prve 2
83
SocketChannel čita i piše u TCP sokete
podaci moraju biti smešteni u ByteBuffer objekte Svaki SocketChannel je pridružen sa peer Socket objektom koji može biti korišćen za naprednu konfiguraciju, ali to se može ignorisati za primene gde su podrazumevane opcije zadovoljavajuće
84
connecting novi SocketChannel objekat se kreira pozivom statičkog open() metoda ima 2 varijante: prva pravi konekciju i blokirajuća je (metod se ne završava dok se ne napravi konekcija ili izbaci izuzetak). Prima 1 argument tipa SocketAddress SocketAddress address = new InetSocketAddress(” 80); SocketChannel channel = SocketChannel.open(address); Druga verzija nema argumente, ne pravi konekciju odmah
85
reading da bi se čitalo iz SocketChannel prvo se kreira ByteBuffer u koji kanal može da smešta podatke on se zatim prosleđuje read() metodu kanal puni bafer sa što je moguće više podataka i vraća broj bajtova koje je tamo smestio. Ako naiđe na kraj stream-a, vraća -1. Ako je kanal blokirajući, čita bar 1 bajt ili vraća -1 ili izbacuje izuzetak Ako je kanal neblokirajući, može vratiti i 0
86
Pošto se podaci smeštaju u bafer na poziciju kursora, koji se automatski ažurira kako se podaci čitaju, možemo nastaviti prosleđivanje istog bafera read() metodu, sve dok se bafer ne napuni sledeća petlja čita dok se bafer ne napuni ili se ne naiđe na kraj stream-a while(buffer.hasRemaining() && channel.read(buffer) != -1) ;
87
writing socket kanali imaju i read i write metode
uopšteno, oni su full duplex u cilju pisanja, prosto se napuni ByteBuffer, okrene (flip) i prosledi jednom od write() metoda koji ga prazni kopirajući podatke iz njega na izlaz kao i kod čitanja (a za razliku od OutputStream), ovaj metod ne garantuje da će ispisati kompletan sadržaj bafera ako je kanal neblokirajući. Međutim, cursor-based priroda bafera omogućuje da lako pozovemo ovaj metod iznova i iznova dok se bafer potpuno ne isprazni i podaci u potpunosti ne ispišu: while(buffer.hasRemaining() && channel.write(buffer) != -1) ;
88
closing treba zatvoriti kanal kada smo završili sa njim kako bi se oslobodio port i ostali korišćeni resursi close() zatvaranje već zatvorenog kanala nema efekta pokušaj pisanja ili čitanja iz zatvorenog kanala izbacuje izuzetak isOpen() – vraća false ako je zatvoren, true inače
89
ServerSocketChannel ima jednu svrhu: prihvatanje dolazećih konekcija
ne može se pisati ili čitati, ili konektovati na ServerSocketChannel jedina operacija koju on podržava je prihvatanje nove dolazeće konekcije sama klasa definiše 4 metoda, od kojih je najbitniji accept()
90
kreiranje server socket kanala
ServerSocketChannel.open() metod kreira novi ServerSocketChannel objekat Ovaj metod ne otvara zaista novi server socket. On samo kreira objekat. Pre nego što on može da se koristi, treba pozvati socket() metod da bi se dobio odgovarajući peer ServerSocket. Na ovom mestu, moguće je konfigurisati server opcije, kao što su veličina primajućeg bafera ili socket timeout. Zatim se ovaj ServerSocket konektuje na SocketAddress za port za koji želimo da ga vežemo.
91
Npr. ovaj fragment koda otvara ServerSocketChannel na portu 80:
try{ ServerSocketChannel server = ServerSocketChannel.open(); ServerSocket socket = serverChannel.socket(); SocketAddress address = new InetSocketAddress(80); socket.bind(address); }catch(IOException ex){ System.err.println(”Could not bind to port 80 because “ + ex.getMessage(); }
92
accepting connections
nakon otvaranja i vezivanja ServerSocketChannel objekta, metod accept() može osluškivati dolazeće konekcije accept može operisati u blokirajućem ili neblokirajućem modu u blokirajućem modu, metod čeka dolazeću konekciju, zatim je prihvata i vraća SocketChannel objekat konektovan na udaljenog klijenta. Nit ne može raditi ništa dok se konekcija ne napravi. Ova strategija je prikladna za jednostavne servere koji mogu neposredno odgovoriti na svaki zahtev. Blokirajući mod je podrazumevani.
93
U neblokirajućem modu, metod accept() vraća null ako nema dolazećih konekcija. Ovaj mod je prikladniji za servere koji za svaku konekciju imaju dosta posla da odrade i zato žele da obrade veći broj zahteva paralelno neblokirajući mod se obično koristi zajedno sa Selector-om. Da bi se ServerSocketChannel učinio neblokirajućim, prosledi se false configureBlocking() metodu accept() metod je deklarisan da izbacuje IOException ako nešto nije kako treba
94
Channels klasa za wrap-iranje kanala oko tradicionalnih I/O stream-ova, čitača i pisača. I obrnuto. Korisna je ako želimo da koristimo novi I/O model u jednom delu programa zbog performansi, ali i dalje radimo sa API-jima koji očekuju stream-ove. Ima metode koji konvertuju stream-ove u kanale i metode koji konvertuju kanale u stream-ove, čitače i pisače ...
95
Readiness Selection readiness selection – mogućnost izbora soketa koji neće blokirati kada se iz njega čita ili u njega piše ovo je od primarnog interesa za servere, mada i klijenti koji izvršavaju veći broj simultanih konekcija sa nekoliko otvorenih prozora – npr. web spider ili browser – mogu koristiti prednosti toga. neophodno je da se razni kanali registruju kod Selector objekta. Svakom kanalu se pridružuje SelectionKey. Program zatim pita Selection objekat za skup ključeva kanala koji su spremni da izvrše operaciju koju želimo da izvršimo bez blokiranja
96
Selector klasa novi selektor se kreira pozivom metoda Selector.open()
sledeći korak je dodavanje kanala selektoru. metod register() deklarisan u SelectableChannel klasi svi mrežni kanali jesu selectable kanal se registruje kod selektora tako što se selektor prosledi register() metodu kanala ... register() metodi prototipovi
97
register() Prvi argument je selektor
drugi je imenovana konstanta iz klase SelectionKey koja identifikuje operaciju za koju se kanal registruje 4 imenovane bit konstante: SelectionKey.OP_ACCEPT SelectionKey.OP_CONNECT SelectionKey.OP_READ SelectionKey.OP_WRITE ovo su bit-flag int konstante (1,2,4,etc.) Ako kanal treba da se registruje za veći broj operacija kod istog selector-a (npr. za čitanje i pisanje soketa), konstante se kombinuju bitskim ILI operatorom (|) prilikom registrovanja: channel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
98
Nakon registrovanja kanala kod selector-a, možemo tražiti od selector-a u bilo kom trenutku da otkrije koji kanali su spremni za procesiranje. Kanali mogu biti spremni za neke operacije, a za neke ne Postoje 3 metoda za selektovanje spremnih kanala: selectNow(), select(), select(long timeout) prvi neblokirajući, druga dva blokirajuća
99
Kada znamo da su kanali spremni za procesiranje, dohvatamo spremne kanale koristeći selectedKeys()
Povratna vrednost je tipa java.util.Set Svaki objekat u skupu je SelectionKey objekat Možemo iterirati kroz skup na uobičajeni način. Takođe, želimo da uklonimo ključ iz iteratora da kažemo Selector-u da smo ga obradili. Konačno, kada želimo da isključimo server, ili nam više nije potreban Selector, zatvaramo ga close() Ovaj korak oslobađa sve resurse pridružene selector-u. Još važnije, otkazuje sve ključeve registrovane kod selector-a i prekida niti blokirane nekim od selektorovih select() metoda
100
SelectionKey klasa objekti ove klase služe kao pokazivači na kanale
takođe mogu sadržati attachment u kome se obično čuva stanje konekcije na tom kanalu SelectionKey objekti se vraćaju register() metodom prilikom registrovanja kanala kod Selector-a. Obično ne moramo sačuvati tu referencu. selectedKeys() metod vraća iste objekte ponovo unutar Set-a. Jedan kanal može se registrovati kod većeg broja selector-a.
101
Kada se dohvata SelectionKey iz skupa selektovanih ključeva, često želimo da testiramo šta je taj ključ spreman da radi: isAcceptable() isConnectable() isReadable() isWritable() Ovo testiranje nije uvek neophodno. U nekim slučajevima Selector testira samo jednu mogućnost i vraća samo ključeve spremne da rade tu jednu stvar. Ali, ako Selector testira veći broj stanja, želimo da testiramo koje je od njih spremno za naš kanal, pre nego počnemo da radimo sa njim. Moguće je i da je kanal spreman za više od jedne stvari.
102
Nakon što znamo da je kanal pridružen ključu spreman, možemo dohvatiti kanal metodom channel()
Ako smo smestili objekat koji čuva informaciju o stanju u SelectionKey, možemo ga dohvatiti metodom attachment() Konačno, kada završimo sa konekcijom, ”deregistrujemo” njen SelectionKey objekat tako da Selector ne troši resurse istražujući njegov readiness. cancel() Ovaj korak je neophodan jedino ako nismo zatvorili kanal. Zatvaranje kanala automatski deregistruje sve ključeve tog kanala u svim selector-ima. Slično, zatvaranje selector-a poništava sve ključeve u tom selector-u.
Similar presentations
© 2025 SlidePlayer.com. Inc.
All rights reserved.