NIO
K.1. Nowe wejście-wyjście (NIO): przegląd
Niewątpliwie podstawowym motywem wprowadzenia nowych środków wejścia-wyjścia,
zawartych w pakicie java.nio i jego podpakietach było zapewnienie zwiększenia
efektywności działania programów w wysokim stopniu obciążonych operacjami
wejścia-wyjścia o dużej częstotliwości.
Tradycyjne wejście-wyjście w Javie jest blokujące, tzn. wątek, który
podejmuje próbę odczytania danych ze strumienia (metoda read()) jest blokowany,
jeśli danych w strumieniu (jeszcze) nie ma. Zatem obsługa strumieni połączeń
sieciowych (przez serwer) musiała być wykonywana poprzez wiele wątków, każdy
z których obsługiwał strumień związany z jednym gniazdem (socket) i ew. był
blokowany na tym strumieniu, czekający na dane. W takiej sytuacji przy dużej
liczbie połączeń działa równolegle dużo wątków, a ponieważ liczba połaczeń
zmienia się dynamicznie, to z dużą częstotliwością wiele wątków powstaje
i umiera (przechodzi do stanu Dead). W sumie prowadzi to do problemów efektywnościowych,
a także jest trudne w programowaniu, gdyż:
- tworzenie i przełączanie wątków (realizowane przez JVM) jest czasowo kosztowne,
- szybkie narastanie niepotrzebnych obiektów (zakończonych wątków)
powoduje zmniejszenie efektywności dzialania programu ze względu na zwiększającą
się zajętośc pamięci, bowiem standardowy odśmiecacz JVM nie jest odpowiednio
przystosowany do takiej sytuacji,
- synchronizowanie dużej liczby wątków na wspóldzielonych zasobach
może być trudne w programowaniu, zawodne i bardzo czasochłonna z punktu widzenia efektywności
dzialania programu.
Podstawowym elementem nowego wejścia-wyjścia, odpowiadającym na te problemy, jest koncepcja kanału.
Kanał reprezentuje otwarte połączenie do obiektu, który wykonuje
jedną lub wiele różnych operacji wejścia-wyjścia. Takim obiektem może być
urządzenie sprzętowe, plik, gniazdo sieciowe, a nawet komponent programu.
Gniazdo sieciowe (socket) jest punktem docelowym dwustronnej komunikacji
dwóch programów działających równolegle w sieci
Kanały podłączone do gniazd
sieciowych (np. klasa SocketChannel) umożliwiają nieblokujące (asynchroniczne) wejście-wyjście.
Przy czytaniu z takiego kanału za pomoca metody read(...), wątek nie jest
blokowany, gdy brak danych. Metoda read(...) natychmiast zwraca wynik - liczbę
przeczytanych bajtów (jeśli nie ma jeszcze danych - to 0).
Umożliwia to odpytywanie (polling) - sprawdzanie co jakiś czas czy dane nadeszły. Wątek nie jest blokowany i może wykonywać inne zadania.
Schemat nieblokującego wejścia
SocketChannel socketChannel = SocketChannel.open(); // otwarcie kanału
socketChanel.connect(...) // podłączenie do gniazda
socketChannel.configureBlocking (false); // tryb nieblokujący
...
while (true) {
...
if (socketChannel.read (buffer) != 0) { // czy są jakieś dane?
processInput (buffer); // tak - przetwórz je
}
else { // nie - wykonuj inne czynności
...
}
}
Uwaga: co to jest buffer? O tym za chwilę.
Jasne jest, że stanowi to alternatywę dla wielu wątków, z których każdy blokowany
jest na czytaniu danych z kanału. Teraz jeden wątek może obsługiwać wiele
kanałów bez blokowania przy braku danych.
Nazywa się to multipleksowaniem kanałów wejścia/wyjścia.
Ale samo nieblokowane wejście-wyjście nie wystarcza jeszcze, by prawidłowo
rozwiązać problem obsługi wielu połączeń. "Ręczne" odpytywanie wielu kanałów
ma wady:
- trzeba je oprogramować (i można przy tym popełnić błędy),
- polega na wysyłaniu instrukcji we/wy za pośrednictwem JVM (read(...)),
co jest czasowo kosztowne i w rezultacie - przy dużej liczbie połączeń -
może niedopuszczalnie zmniejszać czas reakcji na każdym z połaczeń.
Dlatego w java.nio wprowadzono mechanizm selektorów,
który łączy ze sobą zalety nieblokującego odpytywania oraz
natychmiastowej reakcji na dane po blokowaniu wątku na odczycie.
Dzięki nieblokującemu we/wy, a szczególnie selektorom:
- jeden wątek może łatwo monitorować dużą liczbę gniazd sieciowych,
- wątek ten może być zablokowany dopóki nie pojawią się dane z któregokolwiek gniazda, a po ich pojawieniu się natychmiast wznowiony,
- może przy tym dowiedzieć się, który ze strumieni
danych jest gotowy do przetwarzania i natychmiast podjąc to
przetwarzanie.
Niewątpliwie przy nieblokującym wejściu-wyjściu można by to oprogramować
w Javie i bez selektorów, ale użycie selektorów ma dwie podstawowe zalety:
- zwalnia nas od obowiązku pisania trudnego kodu,
- jest efektywne, bowiem multipleksowanie i selekcja kanałów są zrealizowane
w dużej mierze przez odwolania do rodzimych funkcji platformy systemowej
i - wobec tego - omija JVM, pozostawiając ciężką pracę do wykonania procedurom
systemowym, które robią to najsprawniej.
"Nowe wejście-wyjście" ma rownież szereg nie związanych z programowaniem
sieciowym, mających ogólniejsze znaczenie, własciwości.
Obecnie w Javie kanały mogą być połączone z gniazdami, plikami oraz dowolnymi
strumieniami klas InputStream, OutputStream (te ostatnie kanały nazwiemy
kanałami strumieniowymi).
Wszystkie takie kanały mają jedną wspólną właściwość: kanały wejściowe wprowadzają dane do buforow bajtowych, a kanały wyjściowe wyprowadzają dane z tych buforów.
Bufor bajtowy przypomina nieco tablicę bajtów: jest to skończona sekwencja
bajtów, na której można wykonywać pewne operacje (m.in. pobieranie i dodawanie);
od zwykłej tablicy różni się przede wszystkim tym, że operacje na buforze
są dostosowane do potrzeb wejścia-wyjścia
Zatem zapoznanie się z koncepcją
buforów - również jednego z elementów "nowego wejścia-wyjścia" - jest nieodzowne
do korzystania z kanałów.
Bufory mogą być też stosowane samodzielnie i dostarczają pewnych nowych,
ciekawych możliwości, które mogą być wykorzystywane niezależnie od kanałowego
wejścia-wyjścia.
Zanim przejdziemy do bardziej konkretnego omawiania niektórych nowych możliwości,
dostarczanych przez kanały i bufory, warto rzucić okiem na syntezę tych
możliwości (tablica).
KANAŁY
|
|
Kanały gniazdowe
|
Kanały plikowe
|
Kanały strumieniowe
|
uzyskanie kanału
|
metoda open() z klas SocketChannel,
ServerSocketChannel,
DatagramChannel
+ connect
|
Metoda
getChannel()
z klas:
FileInputStream
FileOutputStream
RandomAccessFile
|
Metody statyczne klasy
Channels:
newChannel(InputStream)
newChannel(OutputStream)
|
|
Możliwości dostarczane przez kanały
|
nieblokujące wejście-wyjście
i możliwość stosowania selektorów
|
tak
|
nie
|
nie
|
atomistyczne czytanie rozprowadzające po wielu buforach (scattering read), atomistyczne pisanie gromadzące z wielu buforów (gathering write)
|
tak
|
tak
|
nie
|
mapowanie plików na pamięć
|
nie
|
tak
|
nie
|
bezpośrednie transfery kanałowe (np. jedno wywołanie kopiuje cały plik do innego w sposób bardzo efektywny)
|
tak, jeśli jednym z kanałów jest kanał plikowy
|
tak
|
tak, jeśli jednym z kanałów jest kanał plikowy
|
blokowanie (lock) dostępu do całego pliku lub jego części
|
nie
|
tak
|
nie
|
czytanie, pisanie do/z buforów
(zob. możliwości buforów)
|
tak
|
tak
|
tak
|
Możliwości, dostarczane przez bufory
|
różne widoki buforów bajtowych (jako ciągu bajtów lub elementów wybranego typu pierwotnego) |
przestawianie bajtów (możliwość wyboru i/lub
łatwej zmiany konwencj uporządkowania bajtów danych binarnych - czy bardziej
znaczące częsci danej mają w pamięci mniejsze czy większe adresy)
|
kodowanie - dekodowanie danych w buforach
znakowych przy uwzględnieniu wybranej strony kodowej (klasy Charset, CharsetDecoder
i CharsetEncoder, operujące na buforach)
|
bufory bezpośednie (bufory takie "opakowują"
pamięć alokowaną poza JVM przez natywne środki platformy systemowej i umożliwiają
m.in. wysoką efektywność operacji kanałowych, a także dają programiście możliwość
dostępu z poziomu programu Javy do dowolnego obszaru pamięci, alokowanego
w systemie - np. bezpośredniej pamięci graficznej)
|
Zwróćmy na koniec jeszcze raz uwagę, że "nowe wejście-wyjście" (NIO)
nie zastępuje "starego" (klas strumieniowych). Generalnie klasy strumieniowe
operują na wyższym poziomie abstrakcji, kanały zaś przeznaczone są przede
wszystkim do zwiększenia efektywności operacji wejścia-wyjścia i ukierunkowane
są raczej na niskopoziomowe operowanie na sekwencjach bajtów.
K.2. NIO: bufory
Bufor jest ciągłą, skończoną sekwencją
elementów jednego z typów pierwotnych: byte, short, char,
long, float, double
Odpowiednio do tego mamy różne klasy opisujące bufory np.
ByteBuffer - bufor bajtów (elementy typu byte),
IntBuffer - bufor zawierający liczby calkowite typu int,
DoubleBuffer - bufor liczb typu double.
Oprócz tych klas, odpowiadających typom pierwotnym, istnieje rownież klasa
MappedBuffer, za pomocą której możemy mieć dostęp do pliku mapowanego na
pamięć.
Ogólne charakterystyki i funkcjonalność wszystkich klas buforowych określa klasa abstrakcyjna Buffer.
Rysunek przedstawia hierarchię klas buforowych.
Źródło: Ron Hitchens, Java NIO, O'Reilly 2002
UWAGA: Kanały operują wyłącznie na buforach bajtowych.
Opakowanie danych tablicy przez bufor oznacza, że dane są te same
- zarówno w buforze jak i tablicy, a operacje na buforze, zmieniające dane
bufora zmieniają równocześnie dane w tablicy i vice versa
Bufory mogą być
tworzone poprzez:
- alokowanie prywatnej pamięci bufora - metody allocate i allocateDirect (ta ostatnia dla buforów bezposrednich),
- opakowanie istniejącej tablicy elementów wybranego typu pierwotnego - metoda wrap,
- mapowanie plików lub ich części (metoda map(...) z klasy FileChanel)
Dane do buforów mogą być zapisywane za pomoca metod put(..) z klas
buforowych oraz na skutek czytania z kanału (read(...) z klas kanałowych).
Metody kanałowe zapisują do bufora całe sekwencje bajtów.
Dane z buforów mogą być pobierane za pomocą metod get(...) z klas buforowych oraz zapisywane do kanału (write()...) z klas kanałowych).
Metody put(...) i get(...) dzielą się na relatywne (zapisujące lub odczytujące bufor poczynając od jego bieżącej pozycji) i absolutne (zapisujące/odczytujące element na podanej pozycji w buforze).
Operacje relatywne mogą dotyczyć pojedynczego elementu lub ich sekwencji
(zapisywanych do bufora z tablicy lub innego bufora, odczytywanych z bufora
do tablicy).
Metody get(...) z klas buforowych
(typ oznacza byte, char, short, int, long, float, doble - w zależności od typu bufora).
|
typ
|
get()
Relatywna metoda get - zwraca bieżący element |
typBuffer |
get(typ[] dst)
Relatywna
metoda get - zapisuje tablicę dst elementami bufora, poczynając od jego
bieżacej pozycji. |
typBuffer |
get(typ[] dst,
int offset,
int length)
j.w. ale diotyczy części tablicy |
typ |
get(int index)
Absolutna metoda get - zwraca element na pozycji index. |
Metody put z klas buforowych
(typ oznacza byte, char, short, int, long, float, doble - w zależności od typu bufora).
|
typBuffer |
put(typ b)
Relatywna metoda put - zapisuje element b na bieżącej pozycji |
typBuffer |
put(typ[] src)
Relatywna metoda put - zapisuje do bufora tablicę elementów |
typBuffer |
put(typ[] src,
int offset,
int length)
j.w. - część tablicy |
typBuffer |
put(typBuffer src)
Relatywna metoda put - zapisuje zawartosc bufora src do bufora |
typBuffer |
put(int index, typ b)
Absolutna metoda put, zapisuje b na pozycji index |
Uwaga: jeżeli bufor jest tylko do odczytu (read-only), to użycie metody put(...) spowodują powstanie wyjątku ReadOnlyBufferException. Bufor możemy uczynić "tylko do odczytu" za pomocą metod asReadOnly() z klas buforowych.
|
Każdy z buforów ma następujące (
ważne, a czasem nieintuicyjne) charakterystyki:
- pojemność (capacity) - liczbę elementów bufora (niezmienną!),
- limit (limit) - indeks pierwszego elementu, który nie może być czytany z bufora lub zapisany do bufora,
- pozycję (position) - indeks kolejnego elementu, który będzie odczytany lub zapisany (inaczej bieżącą pozycję bufora).
Informacje o tych charakterystykach można uzyskac za pomocą metod
- int capacity()
- int limit()
- int position()
Pojemność bufora nie może być zmieniona, ale pozycję i limit możemy ustawiać za pomocą metod
- Buffer position(int pos)
- Buffer limit(int lim)
Metoda
remaining() zwraca liczbę elementów znajdujących się pomiędzy pozycją a limitem.
Metoda
hasRemaining() zwraca true, jeśli liczba ta jest większa od 0.
Każdy relatywny zapis do bufora zapisuje element bądź elementy do
bufora poczynając od jego bieżącej pozycji i zmienia pozycję, tak by wskazywała
na następny element (miejsce), który może być zapisany.
Każde relatywne czytanie z bufora czyta element bądź elementy, poczynajac
od jego bieżącej pozycji i zmienia pozycję tak, by wskazywala na następny
jeszcze nie odczytany element.
Uwaga:
Operacje absolutne nie zmieniają pozycji bufora.
Czytanie z bufora bądź zapis do bufora nie zmieniają limitu bufora.
Próba odczytu danych poza limitem spowoduje powstanie wyjątku BufferUnderflowException
(brak danych w buforze), natomiast próba zapisu danych poza limit spoowoduje
wyjątek BufferOverflowException (przepełnienie bufora).
Po zapisaniu danych do bufora, po to by można było je odczytać należy bufor przestawić.
Przestawienie bufora polega na wywolaniu metody flip(), która ustawia limit na bieżącą pozycję, po czym ustala pozycję na zero.
Jeżeli po odczytaniu danych z bufora chcemy jeszcze raz przeczytać je od początku, powinniśmy bufor przewinąć. Służy temu metoda rewind(), która ustala pozcycję na zero, pozostawiając limit bez zmian
Zachowanie buforów i zmiany ich charakterystyk ilustruje poniższy program.
import java.nio.*;
class Buffers {
static void say(String s) { System.out.println(s); }
static void showParms(String msg, Buffer b) {
say("Charakterystyki bufora - " + msg);
say("capacity :" + b.capacity());
say("limit :" + b.limit());
say("position :" + b.position());
say("remaining :" + b.remaining());
}
public static void main(String args[]) {
// alokacja bufora 10 bajtowego (inicjalnie warości elementów = 0)
ByteBuffer b = ByteBuffer.allocate(10);
showParms("Po utworzeniu", b);
// Zapis dwóch bajtów do bufora
b.put((byte) 7).put((byte) 9);
showParms("Po dodaniu dwóch elementów", b);
// Przestawienie bufora
b.flip();
showParms("Po przestawieniu", b);
// Teraz możemy czytać wpisane dane
say("Czytamy pierwszy element: " + b.get());
showParms("Po pobraniu pierwszego elementu", b);
say("Czytamy drugi element: " + b.get());
showParms("Po pobraniu drugiego elementu", b);
say("Czy możemy jeszcze czytać?");
try {
byte x = b.get();
} catch (BufferUnderflowException exc) {
say("No, nie - proszę spojrzeć na ostatni limit!");
}
// Jeszcze raz odczytajmy dane z bufora
// w tym celu musimy go przewinąć
b.rewind();
showParms("Po przewinięciu", b);
say("Czytanie wszystkiego, co wpisaliśmy");
while (b.hasRemaining())
say("Jest: " + b.get());
}
}
który na wyjściu da:
Charakterystyki bufora - Po utworzeniu
capacity: 10
limit: 10
position: 0
remaining: 10
Charakterystyki bufora - Po dodaniu dwóch elementów
capacity: 10
limit: 10
position: 2
remaining: 8
Charakterystyki bufora - Po przestawieniu
capacity: 10
limit: 2
position: 0
remaining: 2
Czytamy pierwszy element: 7
Charakterystyki bufora - Po pobraniu pierwszego elementu
capacity: 10
limit: 2
position: 1
remaining: 1
Czytamy drugi element: 9
Charakterystyki bufora - Po pobraniu drugiego elementu
capacity: 10
limit: 2
position: 2
remaining: 0
Czy możemy jeszcze czytać?
No, nie - proszę spojrzeć na ostatni limit!
Charakterystyki bufora - Po przewinięciu
capacity: 10
limit: 2
position: 0
remaining: 2
Czytanie wszystkiego, co wpisaliśmy
Jest: 7
Jest: 9
Proszę uważnie przeanalizować działanie i wyniki tego programu.
Warto podkreślić, że nawigacja "po buforze" zawsze odbywa się w kontekście typu jego elementów.
Np. w buforze typu IntBuffer elementami są liczby typu int, get zwraca element
typu int, a pozycja będzie się przesuwać tak, by wskazywać na następny element
typu int (4 bajty).
Natomiast w buforze typu ByteBuffer pobieranie, zapisywanie i zmiany pozycji dotyczą pojedynczych bajtów.
Należy zwrócić szczególną uwagę na bufory bajtowe, bowiem
Bufory bajtpwe (typu ByteBuffer) odgrywają szczególną rolę, bowiem tylko one są używane w operacjach na kanalach.
K.3 NIO: kanały i bufory
Aby używać kanalów musimy importować nazwy klas z pakietu java.nio.channels.
Bufory wymagają importu java.nio.
Schematy użycia kanałów w operacjach wejścia-wyjścia przedstaiwono poniżej.
Dla ustalenia uwagi posługujemy się tu kanałami plikowymi.
Zapis do kanału
|
1. Alokacja bufora bajtowego
ByteBuffer buf = ByteBuffer.allocate(N); // N - rozmiar bufora
2. Zapis danych do bufora (np. z użyciem metod put...)
3. Uzyskanie kanału klasy FileChannel "do zapisu"
Możemy go uzyskać za pomocą metod getChannel() z klas FileOutputStream oraz
RandomAccessFile (kanał "do zapisu i do odczytu", jeśli taki był tryb otwarcia
pliku o dostępie swobodnym). Np.
FileOutputStream out = new FileOutputStream(...);
FileChannel fc = out.getChannel();
4. Zapis bufora - użycie metod write z klasy FileChannel. Np.
fc.write(buf);
5. Zamknięcie kanału:
fc.close();
Zamknięty kanał pozostaje zamknięty. Metoda isOpen() pozwala stwierdzić czy kanał jest otwarty.
|
Czytanie z kanału
|
1. Alokacja bufora bajtowego
ByteBuffer buf = ByteBuffer.allocate(N); // N - rozmiar bufora
2. Uzyskanie kanału klasy FileChannel "do odczytu"
Możemy go uzyskać za pomocą metod getChannel() z klas FileInputStream oraz
RandomAccessFile (kanał "do zapisu i do odczytu", jesli taki był tryb otwarcia
pliku o dostępie swobodnym). Np.
FileInputStream out = new FileInputStream(...);
FileChannel fc = out.getChannel();
3. Czytanie do bufora - użycie metod read z klasy FileChannel. Np.
fc.read(buf);
4. Zamknięcie kanału:
fc.close();
Zamknięty kanał pozostaje zamknięty.
5. Określenie właściwej pozycji i limitu bufora po wczytaniu do niego danych.
Np. poprzez przestawienie bufora:
buf.flip();
6. Odczytanie danych z bufora za pomocą metod get...
|
Poniższy program ilustruje powyższy schemat. Komentarze szczegółowo omawiają użyte w nim konstrukcje. Proszę zwrócić na nie baczną uwagę.
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
class SimpleChannel {
String fname = "test.tmp";
byte[] data = {1,2,3,4,5 };
SimpleChannel() {
try {
writeChannel(fname, data);
byte[] wynik = readChannel(fname);
for (int i=0; i < wynik.length; i++) System.out.println(wynik[i]);
} catch(Exception exc) {
exc.printStackTrace();
System.exit(1);
}
}
void writeChannel(String fname, byte[] data) throws Exception {
// Możemy utworzyć bufor przez opakowanie istniejącej tablicy
ByteBuffer buf = ByteBuffer.wrap(data);
FileOutputStream out = new FileOutputStream(fname);
// Uzyakanie kanału
FileChannel fc = out.getChannel();
//Zapis
fc.write(buf);
fc.close();
}
byte[] readChannel(String fname) throws Exception {
// Stworzenie strumienia na podstawie obiektu klasy File
FileInputStream in = new FileInputStream(fname;
// Uzyskanie kanału
FileChannel fc = in.getChannel();
// Metoda size() z klasy FileChannel
// zwraca long -rozmiar plku, do którego podlączony jest kanał
int size = (int) fc.size();
// Utworzenie bufora
ByteBuffer buf = ByteBuffer.allocate(size);
// Czytanie do bufora
// nbytes - liczba przeczytanych bajtów
int nbytes = fc.read(buf);
fc.close();
// Po przeczytaniu danych musimy bufor przestawić
buf.flip();
// Stworzenie tablicy na wynik czytania
// jej rozmiar będzie określony przez liczbę przeczytanuych bajtów
// którą możemy podać na dwa sposoby: poprzednie nbytes
// lub uzyskując informację o liczbie jeszcze nieodczytanych bajtów z bufora
byte[] wynik = new byte[buf.remaining()];
buf.get(wynik);
return wynik;
}
public static void main(String args[]) {
new SimpleChannel();
}
}
W klasie FileChannel zdefiniowano wiele metod pozwalających na czytanie z
- zapis do kanałów oraz pozycjonowanie i przewijanie plików, do których te
kanały są podlączone.
K.4. Widoki buforów bajtowych
Jak już wiemy, w operacjach kanalowych używa się wyłącznie buforów bajtowych,.
Zapewniają one łatwy dostęp do elementów - bajtów.
Ale jak w takim razie przetwarzać dane innych typów (niż
bajty) pobierane z kanałów lub zapisywać takie dane do kanału?
Są po temu przynajmniej dwie możliwości.
Pierwsza polega na użyciu metod klasy ByteBuffer:
ttt elt = getTtt()
putTtt(Ttt elt)
które umożliwają zapisywanie i pobieranie danych typu ttt (short, int, long, float, double, char).
Drugi sposób - znacznie ciekawszy i przyjemniejszy - polega na użyciu widoków buforów bajtowych
Metoda klasy ByteBuffer:
TttBuffer asTttBuffer()
gdzie: ttt - jeden z typów short, int, long, float, double, char
użyta wobec bufora bajtowego buf zwraca referencję do bufora klasy TttBuffer, co pozwala na działanie na danych pierwotnego bufora bajtowego buf za pomocą metod get i put klasy TttBuf czyli tak, jakby pierwotny bufor bajtowy zawierał elementy typu ttt.
Mówimy, że metoda ...asBuffer() zwraca widok bufora bajtowego jako bufora typu TttBuffer, gdyż nie następuej tu żadne kopiowanie danych i zmiany danych wykonywane
za pośrednictwem obu obiektów - bufora pierwotnego oraz jego widoku dotyczą
tego samego bufora.
Koncepcję widoku bufora bajtowego ilustruje poniższy program.
import java.nio.*;
class BufView1 {
public static void main(String args[]) {
final int SHORT_SIZE = 2;
// Alokacja buforu bajtowego,
// mogącego przechowywać do 10 liczb typu short
ByteBuffer bb = ByteBuffer.allocate(10*SHORT_SIZE);
// Widok na ten bofor jak na short-bufor
ShortBuffer sb = bb.asShortBuffer();
// Dodanie trzech liczb typu short
short a = 1, b = 2, c = 3;
sb.put(a).put(b).put(c);
// Co wpisano do bufora? Na wydruku: 1, 2, 3
sb.flip();
while (sb.hasRemaining()) System.out.println(sb.get());
// Operujemy teraz na nim za pomocą bufora bajtowego
// zmieniając bajty na pozycji 1, 3 i 5.
byte x = 4, y = 5, z = 6;
bb.put(1, x).put(3, y).put(5, z);
// Co pokaże short-bufor? Na wydruku 4, 5, 6
sb.rewind();
while (sb.hasRemaining()) System.out.println(sb.get());
}
}
Z widokami bufora bajtowego wiąże się jednak pewna pułapka.
Otóż, mimo, że bufor i jego widok dotyczą tych samych danych to jednak mamy
dwa bufory (bufor bajtowy i jego widok jakiegoś wybranego typu - np. ShortBuffer
), a w tych buforach zmiany pozycji oraz limitu są niezalezne od siebie.
Pozycja i limit bufora bajtowego są niezależne od pozycji i limitu jego widoku.
Gdybyśmy np. chcieli w poprzendim przykladzie po wpisaniu liczb short do
bufora bajtowego (za pomocą jego widoku) pokazać kolejne bajty wszystkich
wpisanych liczb, to moglibyśmy pojść błędnym tropem: po wpisaniu danych przestawić
bufor short (flip) i sądząc, że pierwotny bufor bajtowy także dostosuje swoją
pozycję i limit wypisywać bajty w pętli dopóki hasRemaining() nie zwróci
false.
Ilustruje to poniższy program (w którym używamy metody showParms
zpoprzednich przykładów dla pokazania charakterystyk bufora):
public static void main(String args[]) {
final int SHORT_SIZE = 2;
ByteBuffer bb = ByteBuffer.allocate(10*SHORT_SIZE);
ShortBuffer sb = bb.asShortBuffer();
short a = 1, b = 2, c = 3;
sb.put(a).put(b).put(c);
showParms("bufor short - po dodaniu liczb", sb);
showParms("bufor bajtowy - po dodaniu liczb", bb);
sb.flip();
showParms("bufor short - po przestawieniu", sb);
showParms("bufor bajtowy - po flip bufora short", bb);
System.out.print("Dane");
while (bb.hasRemaining()) System.out.print(" " + bb.get());
}
}
Wydruk.
Charakterystyki bufora - bufor short - po dodaniu liczb
capacity: 10
limit: 10
position: 3
remaining: 7
Charakterystyki bufora - bufor bajtowy - po dodaniu liczb
capacity: 20
limit: 20
position: 0
remaining: 20
Charakterystyki bufora - bufor short - po przestawieniu
capacity: 10
limit: 3
position: 0
remaining: 3
Charakterystyki bufora - bufor bajtowy - po flip bufora short
capacity: 20
limit: 20
position: 0
remaining: 20
Dane 0 1 0 2 0 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0
Zwrócmy uwagę: dodanie elementów do widoku bufora bajtowego nie zmienia jego
pozycji i limitu, zmienia tylko pozycję i limit w widoku, Rownież flip()
widoku nie ma wpływu na te parametry bufora bajtowego. Program wypisuje cały
bufor, bajt po bajcie, a nie bajty tych elementów, ktore zostały dodane.
Aby w tym przykładzie wypisac tylko bajty wpisanych danych, powinniścmy po
przestawieniu widoku bufora zmienić odpowiednio limit bufora bajtowego:
sb.flip();
bb.limit(sb.limit()*SHORT_SIZE);
System.out.print("Bajty wpisanych danych");
while (bb.hasRemaining()) System.out.print(" " + bb.get());
Jednak przy czytaniu danych z kanałów nie ma takiej potrzeby. Dane są wczytywane
do bufora bajtowego, a nie do jego widoku. Zatem należy przestawić bufor
bajtowy, a dopiero potem stworzyć i posługiwać się jego widokiem. Jest to sensowne,
bowiem widok bierze pod uwagę pozycję i limit bufora, na podstawie którego
jest tworzony, odpowiednio ustawiając swoją pozycję i limit.
Co pokazuje poniższy program.
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
class BuffChan {
String fname = "testfile.tmp";
// inicjacja danych testowych
void init() throws Exception {
double[] data = { 0.1, 0.2, 0.3 };
DataOutputStream out = new DataOutputStream(
new FileOutputStream(fname)
);
for (int i=0; i < data.length; i++) out.writeDouble(data[i]);
out.close();
}
BuffChan() throws Exception {
// inicjalcja danych testowych
init();
// utworzenie bufora
ByteBuffer buf = ByteBuffer.allocate(1000); // nie wiemy ile, maks 100B
// uzyskanie kanału
FileChannel fcin = new FileInputStream(fname).getChannel();
// czytanie z kanału do bufora
fcin.read(buf);
fcin.close();
// przestawienie bufora bajtowego
buf.flip();
// Utworzenie widoku bufora
DoubleBuffer dbuf = buf.asDoubleBuffer();
// Wypisanie odczytanych danych
while (dbuf.hasRemaining()) System.out.println(dbuf.get());
}
public static void main(String args[]) throws Exception {
new BuffChan();
}
}
K.5. NIO: bufory - uporządkowanie bajtów (endianess)
Przy czytaniu i zapisywaniu plików, tworzonych na różnych platformach
sprzętowych, powstaje problem uporządkowania (kolejności występowania) bajtów
danych binarnych,
Wynika to z tego w jaki sposób dane binarne przechowywane są w pamięci (i jak zapisywane w rejestrach procesora).
Porządek bajtów w reprezentacji binarnej danych określa się terminem endianess.
Istnieją dwa różne porządki:
-
Porządek BIG ENDIAN oznacza, że mniej znaczące bajty danych są zapisywane
pod większymi adresami pamięci (niejako od prawej do lewej)
-
Porządek LITLLE ENDIAN jest odwrotny: najpierw są zapisywane mniej znaczące bajty, później bardziej (od lewej do prawej).
Ilustruje to rysunek, na którym pokazano porządek bajtów
liczby typu int równej 1234567 (heksadecymalnie 0x12D687).
O tym jaki jest porządek bajtów w buforach możemy dowiedzieć się za pomocą
metody order(), która zwraca referencję do obiektu klasy ByteOrder.
W przypadku buforów bajtowych (i tylko dla nich) możemy zmienić porządek
za pomocą wywołania metody order(ByteOrder) z argumentem będącym stałą statyczną
z klasy ByteOrder:
-
order (ByteOrder.BIG_ENDIAN) - zmienia porządek na BIG ENDIAN
-
order (ByteOrder.LITTLE_ENDIAN) - zmienia porządek na LITTLE ENDIAN
Widoki bufora bajtowego mają ten porządek bajtów, który występował dla
bufora bajtowego w momencie tworzenia widoku (możemy powiedzieć, że widok
dziedziczy porządek swojego bufora, bajtowego obowiązujący w chwili tworzenia
widoku)
Ilustruje to poniższy program:
import java.nio.*;
class Endianness {
static void show(int n) {
String s = Integer.toHexString(n);
int l = s.length();
for (int i=l; i < 8; i++) s = '0' + s;
System.out.println("Liczba " + n + " hex -> " + s.toUpperCase());
}
public static void main(String args[]) {
int num = Integer.parseInt(args[0]);
ByteBuffer buf = ByteBuffer.allocate(4);
System.out.println(buf.order().toString());
IntBuffer b1 = buf.asIntBuffer();
System.out.println("Porządek b1 " + b1.order());
b1.put(num);
b1.flip();
show(b1.get());
buf.order(ByteOrder.LITTLE_ENDIAN);
System.out.println("Porządek buf " + buf.order());
System.out.println("Porządek b1 " + b1.order());
b1.rewind();
show(b1.get());
System.out.println("Porządek buf " + buf.order());
System.out.println("Porządek dziedzizcony " + buf.asIntBuffer().order());
show(buf.asIntBuffer().get());
}
}
który dla podanej jako argument liczby 987654321 wyprowadzi następujące wyniki:
BIG_ENDIAN
Porządek b1 BIG_ENDIAN
Liczba 987654321 hex -> 3ADE68B1
Porządek buf LITTLE_ENDIAN
Porządek b1 BIG_ENDIAN
Liczba 987654321 hex -> 3ADE68B1
Porządek buf LITTLE_ENDIAN
Porządek dziedzizcony LITTLE_ENDIAN
Liczba -1318527430 hex -> B168DE3A
K.6. NIO: bufory znakowe. Kodowanie i dekodowanie.
Bufory znakowe (klasa CharBuffer) reprezentują sekwencje elementów typu char (znaki unikodu).
Mają one wszystkie właściwości buforów (o których już
była mowa), ale dodatkowo zapewniają pewną specyficzną funkcjonalność.
Zauważmy, że sekwencje znaków są reprezentowane również przez obiekty klas String i StringBuffer.
Od Javy 1.4 uogólniono więc pewne elementarne operacje na sekwencjach znaków, wprowadzając interfejs CharSequence
. Interfej ten implementowany jest przez wszystkie klasy w/w klasy: String,
StringBuffer i CharBuffer, dzięki czemu możliwe jest (do pewnego stopnia)
uniwersalne operowanie na sekwencjach znaków, niezaleznie od tego, czy są
one obiektami klas String, StringBuffer czy CharBuffer.
Metody interfejsu CharSequence podano w tablicy,
char |
charAt(int index)
Zwraca znak na pozycji index |
int |
length()
Zwraca długość sekwencji znaków |
CharSequence |
subSequence(int start,
int end)
Zwraca
podsekwencję od znaku na pozycji start do znaku na pozycji end
(wyłacznie) |
String |
toString()
Zwraca
referencję do nowego obiektu klasy String, zawierającego tę sekwencję
znaków. |
Używając tych metod można napisac uniwersalną metodę operującą na sekwencjach
znaków, która jako argument może przyjmować referencję do obiektu dowolnej
z klas: String, StringBUffer i CharBuffer (należy się spodziewać, że zetsaw
metod interfejsu CharSequence zostanie w przyszłości wzbogacony, co rozszerzy
możliwości pisania uniwersalnych fragmentów kodu). Już teraz zresztą CharSequence
pełni istotną użyteczną rolę, bowiem metody klas pakietu java.util.regex,
umozliwiające anlalizę łańcuchów znakowych za pomocą wyrażeń regularnych,
operują na obiektach typu CharSequence.
Z punktu widzenia buforów znakowych, wprowadzenie interfejsu CharSequence ma dwie ważne konsekwencje:
- poza znanymi nam metodami buforowymi na buforach znakowych można operowac za pomocą metod tego interfejsu,
- klasa CharBuffer dostarcza metody wrap opakowującej dowolną sekwencję
znaków (obiekt klasy implementującej interfejs CharSequence) i dzięki temu
możemy np. w następujący sposób tworzyć bufory znakowe.:
String txt = editor.getText();
CharBuffer buf = CharBuffer.wrap(txt);
To miłe, że mamy takie bufory znakowe, ale przecież kanały mogą operowac wyłącznie na buforach bajtowych.
Przy kanałowym czytaniu i zapisie strumieni tekstowych powstaje więc problem kodowania i dekodowania informacji tekstowej.
Znany nam sposób kodowania-dekodowania za pomocą klas InputStreamReader i
OutputStreamReader dotyczy tylko plików, a przy tym nie nadaje się do zastosowania
przy polączeniach kanałowych, bowiem kanały plikowe można uzyskać tylko od
obiektów klas FileInputStream, FileOutputStream i RandomAccessFile.
Dla rozwiązania problemu dekodowania - kodowania można, co prawda, zastosować
klasę String, jak pokazuje poniższy program (konwertujący pliki z jednej
strony kodowej na inną).
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
class Ende1 {
public static void main(String[] args) {
if (args.length != 4) {
System.out.println("Syntax: in in_enc out out_enc");
System.exit(1);
}
String infile = args[0], // plik wejściowy
in_enc = args[1], // wejściowa strona kodowa
outfile = args[2], // plik wyjściowy
out_enc = args[3]; // wyjściowa strona kodowa
try {
FileChannel fcin = new FileInputStream(infile).getChannel();
FileChannel fcout = new FileOutputStream(outfile).getChannel();
ByteBuffer buf = ByteBuffer.allocate((int)fcin.size());
// czytanie z kanału
fcin.read(buf);
// przeniesienie zawartości bufora do tablicy bytes
buf.flip();
byte[] bytes = new byte[buf.capacity()];
buf.get(bytes);
// dekodowanie - za pomocą konstruktora klasy String
String txt = new String(bytes, in_enc);
// enkodowanie za pomocą metody getBytes z klasy String
// utworzenie nowego bufora dla kanału wyjściowego
// zapis do pliku poprzez kanał
bytes = txt.getBytes(out_enc);
buf = ByteBuffer.wrap(bytes);
fcout.write(buf);
fcin.close();
fcout.close();
} catch (Exception e) {
System.err.println(e);
System.exit(1);
}
}
}
ale jest to rozwiązanie dodatkowo obciążające pamięć operacyjną oraz - jak widać - mało wygodne w programowaniu.
W NIO rozwiązano problem kodowania-dekodowania za pomoca wprowadzenia nowych klas Charset, CharsetDecoder, CharsetEncoder w pakiecie java.nio.charset.
- Klasy CharsetDecoder i CharsetEncoder dostarczają
bogatych środków i pełnej kontroli nad procesem kodowania i
dekodowania.
- Klasa Charset dostarcza definicji stron kodowych, a także wygodnych
metod kodowania i dekodowania (które są łatwiejsze w użyciu niż zastosowanie
klas CharsetEncoder i CharsetDecoder).
Dekodowanie-kodowanie za pomocą Charset polega na:
- utworzeniu obiektu klasy Charset, za pomocą statycznej metody forName(String),
reprezentująccgo stronę kodową, której nazwę podano jako argument np:
Charset charset = Charset.forName("ISO-8895-2");
- dekodowaniu bajtów z bufora bajtowego z danej strony kodowej
na sekwencję znaków Unicode, zapisaną w buforze znakowym za pomocą metody
decode(...):
ByteBuffer buf = ...
CharBuffer cbuf = charset.decode(buf);
- lub kodowaniu znaków Unicodu z bufora znakowego
do bajtów, reprezentujących tekst zakodowany wedle danej strony
kodowej w buforze bajtowym:
CharBuffer cbuf = ...
ByteBuffer buf = charset.encode(cbuf);
Informację o nazwach dostępnych w danej implementacji JVM stron kodowych można
uzyskać za pomocą metody availCharsets() z klasy Charset.
Przykładowy program pokazuje zastosowanie tej procedury do konwersji plików
z jednej strony kodowej na inną (z wykorzystaniem kanałów).
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;
class Ende2 {
public static void main(String[] args) {
if (args.length != 4) {
System.out.println("Syntax: in in_enc out out_enc");
System.exit(1);
}
String infile = args[0], // plik wejściowy
in_enc = args[1], // wejściowa strona kodowa
outfile = args[2], // plik wyjściowy
out_enc = args[3]; // wyjściowa strona kodowa
try {
FileChannel fcin = new FileInputStream(infile).getChannel();
FileChannel fcout = new FileOutputStream(outfile).getChannel();
ByteBuffer buf = ByteBuffer.allocate((int)fcin.size());
// czytanie z kanału
fcin.read(buf);
// Strony kodowe
Charset inCharset = Charset.forName(in_enc),
outCharset = Charset.forName(out_enc);
// dekodowanie bufora bajtowego
buf.flip();
CharBuffer cbuf = inCharset.decode(buf);
// enkodowanie bufora znakowego
// i zapis do pliku poprzez kanał
buf = outCharset.encode(cbuf);
fcout.write(buf);
fcin.close();
fcout.close();
} catch (Exception e) {
System.err.println(e);
System.exit(1);
}
}
}
K.7. NIO: operacje kanałowe na wielu buforach (scattering i gathering)
Kanały pozwalają za jednym odwołaniem czytać dane do wielu buforów (scatter read) lub
zapisywać dane z wielu buforów (gather write).
Służą do tego metody read i write (z klas
kanałowych), które jako argument mają tablice buforów.
Jest to bardzo użyteczna właściwość, pozwala bowiem w łatwiejszy sposób programować
uzyskiwanie przez kanały danych, mających określoną strukturę, w której można
wyróżnić jakieś części o określonym rozmiarze.
Przykładem mogą być pliki graficzne lub dane z nagłówkami komunikacyjnymi.
Program testujący czytanie do wielu buforów.
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
public class ScatteringTest {
static final String fname = "scatter.tst"; // nazwa pliku testowego
public static void main(String[] args) throws Exception {
// Zapisywanie danych testowych
DataOutputStream out = new DataOutputStream(
new FileOutputStream(fname) );
short[] dat1 = { 1, 2, 3, };
double[] dat2 = { 10.1, 10.2, 10.3 };
for (int i=0; i < dat1.length; i++) out.writeShort(dat1[i]);
for (int i=0; i < dat2.length; i++) out.writeDouble(dat2[i]);
out.close();
//-----------------------------------------------------------+
// Odczytywanie danych testowych |
//-----------------------------------------------------------+
FileInputStream in = new FileInputStream(fname);
// Uzyskanie kanału
FileChannel channel = in.getChannel();
// Tablica bajt-buforów
final int SHORT_SIZE = 2, // ile bajtów ma short
DOUBLE_SIZE = 8; // ........... i double
ByteBuffer[] buffers = { ByteBuffer.allocate(dat1.length*SHORT_SIZE),
ByteBuffer.allocate(dat2.length*DOUBLE_SIZE)
};
// jedno czytanie z kanału zapisuje kilka buforów !
long r = channel.read(buffers);
System.out.println("Liczba bajtów przeczytanych do obu buforów: " + r);
// Przed uzyskiwaniem danych z buforów - trzeba je przestawić!
buffers[0].flip();
buffers[1].flip();
// Pierwssy bufor
// Widok na bufor jako na zawierający liczby short
ShortBuffer buf1 = buffers[0].asShortBuffer();
System.out.println("Dane 1");
while ( buf1.hasRemaining()) {
short elt = buf1.get();
System.out.println(elt);
}
// Drugi bufor
// Widok na bufor jako na zawierający liczby double
DoubleBuffer buf2 = buffers[1].asDoubleBuffer();
System.out.println("Dane 2");
while ( buf2.hasRemaining()) System.out.println(buf2.get());
}
}
wyprowadzi następujące wyniki:
Liczba bajtów przeczytanych do obu buforów: 30
Dane 1
1
2
3
Dane 2
10.1
10.2
10.3
Również przy zapisywaniu danych przez kanały, możliwość jednorazowego zapisu
z wielu buforów może być wielce wygodna. Np. w pewnych buforach możemy przygotować
stałe fragmenty danych i już ich nie zmieniać, inne zaś bufory wypełniać
dynamicznie zmieniającą się treścią.
Potrzebne będzie tylko jedno wywołanie metody write, by zapisać cały zestaw
buforów (co oprócz wygody programistycznej ma również pozytywny wpływ na efektywność
działania naszego programu).
Przykładowy program zapisuje kilka plików, każdy z których ma taki sam nagłówek
i zakończenie. Częsć środkową wypełniają zmieniające się - od pliku do pliku
- dane.
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;
class GatheringTest {
public static void main(String[] args) throws Exception {
// To będą stałe części każdego pliku
String sHeader = "To jest nagłówek. Może być duży";
String sFooter = "To jest zakończenie. Może być duże";
// To będą dane, które się zmieniają od pliku do pliku
byte[][] dane = { { 1, 2, 3}, // dane 1-go pliku
{ 9, 10, 11, 12 }, // dane 2-go pliku
{ 100, 101} // dane 3-go pliku
};
Charset charset = Charset.forName("windows-1250");
ByteBuffer header = charset.encode(CharBuffer.wrap(sHeader)),
footer = charset.encode(CharBuffer.wrap(sFooter));
// Drugi element tablicy buforów będzie dynamicznie się zmienial
// na razie = null
ByteBuffer[] contents = { header, null, footer };
for (int i = 0; i<dane.length; i++) {
FileChannel fc = new FileOutputStream("plik"+i).getChannel();
contents[1] = ByteBuffer.wrap(dane[i]); // podstawienie zmiennych danych
fc.write(contents); // zapis danych ze wszystkich buforów!
fc.close();
header.rewind();
footer.rewind();
}
}
}
Czytanie i pisanie "po wielu buforach" od razu, nazywane także wektorowym wejściem-wyjściem (vectored I/O) jest atomistyczne
nie tylko w tym sensie, że realizowane jest przez jedno odwołanie z poziomu
programu. Na nowoczesnych platformach systemowych daje ono dużą poprawę efektywności,
bowiem następuje znaczna redukcja liczby niskopoziomowych (na poziomie jądra
systemu) operacji we/wy oraz ich optymalizacja (w skrajnym przypadku do atomistycznej
- pojedynczej - calkowicie zoptymalizowanej niskopoziomowej operacji we/wy).
K.8. NIO: mapowanie plików
Mapowanie pliku polega na odzwierciedleniu całości lub części pliku w pamięci, bezpośrednio dostępnej dla programu.
Mapowanie pliku w NIO uzyskujemy poprzez uzyskanie kanału podłączonego do
pliku (niech oznacza go zmienna channel), a następnie użycie metody map z
klasy FileChannel. Metoda ta zwraca referencję do obiektu klasy MappedByteBuffer
(pochodnej od Buffer), który jest bajtowym buforem bezpośrednim (direct),
zawierającym bajty pliku lub jego wybranego segmentu.
FileChannel channel = ....; // podłączenie kanału do pliku
MappedByteBuffer buf = channel.map( tryb, pozycja, rozmiar);
gdzie:
- buf - bufor bajtowy, reprezentujący zwartość pliku lub jego segmentu
- tryb - jedna z opcji będących stałymi statycznymi klasy FileChannel.MapMode o nazwach
- READ_ONLY - możliwe tylko odczytywanie bufora
- READ_WRITE - możliwe zmiany w buforze i ew. automatycznej propagacji
zmian do pliku i ew. widocznośi tych zmian przez inne programy mapujące ten
sam plik,
- PRIVATE - możliwe zmiany w buforze, ale bez propagowania ich do
pliku (będą więc niewidoczne dla innych programów mapujących ten plik)
- pozycja - pzocucja pliku, od ktorej zaczyna się mapowanie
- rozmiar - rozmiar mapowanego segmentu pliku
Po zmapowaniu pliku, na uzyskanym buforze bajtowym i jego widokach możemy
wykonywać operacje, znane nam z klas buforowych, a operacje te de-facto będą
operacjami na zawartości pliku.
Dostępne tryby dzialania zależą od trybów podłączenia (otwarcia) kanału.
Tryb READ_ONLY może być użyty tylko dla kanałów otwartych do czytania, natomiast
tryby READ_WRITE oraz PRIVATE wymagają kanałów otwartych w trybie odczytu-zapisu.
Odpowiedniość między trybami kanałów, a rodzajami plików/strumieni pokazuje poniższa tablica.
Tryb mapowania
|
Tryb otwarcia kanału
|
Rodzaj pliku,
do którego podłączamy kanał
metodą getChannel()
|
READ_ONLY
|
tylko do odczytu
|
FileInputStream
RandomAccesFile otwarty w trybie tylko do odczytu ("r")
|
READ_WRITE
PRIVATE
|
do odczytu-zapisu
|
RandomAccesFile otwarty w trybie pisania-czytania ("rw")
|
Mapowanie plików może być wygodne z punktu widzenia programowania (działanie
na pliku jak na buforze i jego widokach - lub w niektórych przypadkach jak
na tablicy).
Ale nie tylko to jest zaletą mapowania plików.
Otóż uzyskiwany bufor mapujący (obiekt klasy MappedByteBuffer) jest buforem bezpośrednim, czyli alokowanym poza przestrzenią adresową programu .
W nowoczesnych systemach operacyjnych może on prawie w ogóle nie zajmować
pamięci systemu! Oczywiście jakieś fragmenty pamięci są używane dla prowadzenia
odwzorowania, ale generalnie strony pamięci wirtualnej nie są zajęte. Obszarem
stronicowanie jest sam plik!
Sprawia to, iż wiele programów naraz może mapować ten sam duży plik i działać
na nim, nawet jeśli sumaryczna wielkość wszystkich odwzorowań przekracza
wielkość dostępnej pamięci operacyjnej.
Z drugiej strony, to że bufory dla mapowanych plików są bezpośrednie (czyli
alokowane gdzieś poza programami) sprawia, że mapowane pliki mogą stanowić
wygodną realizację pamięci dzielonej (przez różne procesy) i sposobem na komunikwoanie się tych procesów.
Zobaczmy przykłady.
Pierwszy program ilustracyjny zapisuje najpierw plik testowy liczbami całkowitymi,
następnie uzyskuje kanał i mapuje plik, po czym wykonuje zmiany w tym pliku,
operując wylącznie na mapującym buforze.
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
class MapFiles1 {
String fname = "test";
public MapFiles1() throws Exception {
init(); // inicjacja pliku testowego
mapAndChange(); // mapowanie i zmiana danych pliku
checkResult(); // sprawdzenie wyników
}
void init() throws IOException {
int[] data = { 10, 11, 12, 13 };
DataOutputStream out = new DataOutputStream(
new FileOutputStream(fname)
);
for (int i=0; i<data.length; i++) out.writeInt(data[i]);
out.close();
}
void mapAndChange() throws IOException {
// Aby dokonywać zmian musimy przyłączyć kanal
// do pliku otwartego w trybie "read-write"
RandomAccessFile file = new RandomAccessFile(fname, "rw");
FileChannel channel = file.getChannel();
// Mapowanie pliku
MappedByteBuffer buf;
buf = channel.map(
FileChannel.MapMode.READ_WRITE, // tryb "odczyt-zapis"
0, // od początku pliku
(int)channel.size() // cały plik
);
// Uzyskujemy widok na bufor = zmapowany plik
IntBuffer ibuf = buf.asIntBuffer();
// Dla ciekawości: jakie charakterystyki widoku
System.out.println(ibuf + " --- Direct: " + ibuf.isDirect());
int i = 0;
while (ibuf.hasRemaining()) {
int num = ibuf.get(); // pobieramy kolejny element
ibuf.put(i++, num * 10); // zapisujemy jego wartość*10 na jego pozycji }
}
// Zapewnia, że zmiany na pewno zostaną odzwierciedlone w pliku
buf.force();
channel.close();
}
void checkResult() throws IOException {
DataInputStream in = null;
try {
in = new DataInputStream(new FileInputStream(fname));
while(true) System.out.println(in.readInt());
} catch(EOFException exc) {
return;
} finally {
in.close();
}
}
public static void main(String[] args) throws Exception {
new MapFiles1();
}
}
Program wyprowadzi:
java.nio.DirectIntBufferS[pos=0 lim=4 cap=4] --- Direct: true
100
110
120
130
Na przykładzie tego programu warto zwrócić uwagę na następujące kwestie:
- program praktycznie nie konsumuje pamięci dla przechowywania pliku
(zarówno MappedByteBuffer, jak i jego widok jako IntBuffer są buforami bezpośrednimi)
- po zmapowaniu pliku, pozycja bufora bajtowego jest ustawiona na
0, a jego pojemność i limit na liczbę zmapowanych bajtów; widoki "dziedziczą"
te ustawienia, dokonując jedynie przeliczeń limitu i pojmeności z uwzględnieniem
rozmiaru typu elementów,
- użycie metody force() z klasy MappedByteBuffer zapewnia fizyczny
zapis do pliku; sposób i częstostliwość fizycznego zapisu zmienianych w buforze
danych zależy od systemu operacyjnego, może się okazać, że nawet po zamknięciu
kanału, systemowe czy sprzętowe cache/bufory nie są wymiatane.
Drugi program ma trochę bardziej praktyczne zastosowanie. Pozwala on "w miejscu"
zmienić kodowanie podanego jako argument pliku z Windows1250 na ISO-8859-2
jednocześnie zamieniając wszystkie litery na duże.
Przy okazji zobaczymy, że:
- dekodowanie bufora bajtowego tworzy nowy bufor znakowy, który opakowuje
tablicę elementów typu char[]. Ta tablica istnieje (ona własnie, praktycznie,
jest tym buforem znakowym) i możemy mieć do niej dostęp poprzez odwołanie
array() z klasy CharBuffer (dla wszystkich klas buforowych metoda array zwraca tablicę opakowaną przez bufor, ale tylko wtedy, gdy takie opakowanie miało miejsce),
- czasem potrzebne może być zastosowanie klasy CharsetEncoder (jej
metody encode używamy po to by dokonać kodowania do już istniejącego bufora
bajtowego, w naszym przypadku - mapującego plik). Analogicznie (ale niejako
w drugą stronę) może być zastosowana klasa CharsetDecoder.
Oto tekst programu. Proszę go przetestowac na jakimś samodzielnie utworzonym
pliku html, zapisanym w stronie kodowej Windows 1250).
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;
class MapFiles2 {
public static void main(String[] args) throws Exception {
Charset inCharset = Charset.forName("windows-1250"),
outCharset = Charset.forName("ISO-8859-2");
RandomAccessFile file = new RandomAccessFile(args[0], "rw");
FileChannel fc = file.getChannel();
// Mapowanie pliku
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE,
0, (int) fc.size());
// Utworzenia bufora znakowego ze zdekodowanymi znakami
// z bufora bajtowego (mapujące plik). Konwersja: win1250->unicode
CharBuffer cbuf = inCharset.decode(mbb);
// Okazuje się, że ten nowo utworzony bufor opakowuje tablicę
// zatem możemy ją uzyskać i działać na jej elementach
// to dzialanie oznacza dzialanie na elementach bufora
char[] chArr = cbuf.array();
for (int i=0; i < chArr.length; i++)
chArr[i] = Character.toUpperCase(chArr[i]);
// Po dekodowaniu bufor bajtowy musi być przewinięty do początku
// aby koder (zob. dalej) mógł w nim zapisywać kodowane dane
mbb.rewind();
// Utworzenie kodera, zamieniającego Unicode na wyjściową stronę kodową
CharsetEncoder encoder = outCharset.newEncoder();
// Koder zapisuje istniejący bufor mbb (ten który mapuje plik)
// ostatni argument - true oznacza zakończenie pracy kodera na tym wywołaniu
encoder.encode(cbuf, mbb, true);
fc.close();
}
}
K.9. NIO: bezpośrednie transfery kanałowe
Bezpośredni transfer kanałowy polega na przeslaniu wszystkich danych
jednego kanału do drugiego kanału za pomocą jednego odwołania z poziomu programu,
z możliwym (w zależności od systemu operacyjnego i platformy sprzętowej)
pominięciem systemowych operacji wejścia-wyjścia, a zrealizowanym jako bardzo
szybki trarnsfer wykonywany przez sam sprzęt ze wsparciem ze strony jądra
systemu
Obecnie w NIO bezpośrednie transfery kanałowe dotyczą tarnsferów do/z plików.
Klasa FileChannel dostarcza dwóch metod, które pozwalają na:
- bezpośredni transfer kanałowy z dowolnego kanalu do kanalu plikowego (metoda transferTo)
- bezpośredni transfer kanalowy z kanalu plikowego do dowlonego kanału (metoda transferFrom)
Bezpośrednie transfery kanałowe w systemach, które w swoim jądrze zapewniają
wsparcie takich transferów, mogą być bardzo szybkie i efektywne.
W innych systemach (jak np. Windows), poprawa efektywności (w porównaniu
z tradycyjnymi sposobami kopiowania danych) może być różna, nawet dość nieznaczna.
Zawsze jednak zaletą stosowania metod transfer... z klasy FileChannel
pozostaje możliwośc uniknięcia potrzeby programowania (w kliku wierszach,
ale zawsze to trochę pracy) procedur kopiowania danych z/do plików.
Oto przykładowy program, porównujący dwa sposoby kopiowania plików: za pomocą
bezpośredniego transferu kanałowego i poprzez buforowane strumienie.
import java.nio.*;
import java.nio.channels.*;
import java.io.*;
class DirectTransfer {
String inFileName;
String outFileName;
DirectTransfer(String infn, String outfn) throws Exception {
inFileName = infn;
outFileName = outfn;
directTransfer();
copyByStream();
}
void directTransfer() throws Exception {
FileInputStream in = new FileInputStream(inFileName);
FileOutputStream out = new FileOutputStream(outFileName);
FileChannel fcin = in.getChannel();
FileChannel fcout = out.getChannel();
long size = fcin.size();
System.out.println("Copying file " + size + "B.");
long start = System.currentTimeMillis();
// Bezpośredni transfer
fcout.transferFrom(fcin, 0, size);
long end = System.currentTimeMillis();
System.out.println("Direct transfer time " + (end - start));
}
final int BUFSIZE = 5000000;
void copyByStream() throws Exception {
FileInputStream fin = new FileInputStream(inFileName);
BufferedInputStream in = new BufferedInputStream(fin, BUFSIZE );
FileOutputStream fout = new FileOutputStream(outFileName);
BufferedOutputStream out = new BufferedOutputStream(fout, BUFSIZE);
byte[] b = new byte[BUFSIZE];
long start = System.currentTimeMillis();
while (true) {
int n = in.read(b);
if (n == -1) break;
out.write(b, 0, n);
}
in.close();
out.close();
long end = System.currentTimeMillis();
System.out.println("Stream time " + (end - start));
}
public static void main(String[] args) throws Exception {
new DirectTransfer(args[0], args[1]);
}
}
Możliwy wynik:
Copying file 51793936B.
Direct transfer time 2470
Stream time 6150