Canali
e Buffer
Cos'è un Buffer, nella nuova accezione del
JDK1.4? E' sostanzialmente una classe contenitore.
Quindi una cosa simile a java.util.Vector, per capirci.
Però ha due differenze fondamentali:
-
È un contenitore di tipi primitivi
- È
direttamente associabile ad un file
La
prima proprietà ci convince del fatto che si
tratta di un contenitore lineare, a capacità
finita. Nel quale possiamo spostarci attraverso un
indice che marca la posizione di lettura/scrittura
raggiunta fino a quel momento. La seconda proprietà
ci dice che, da oggi in avanti, potremo scrivere e
leggere dati primitivi da un file senza preoccuparci
di formattarlo o tokenizzarlo in qualche modo. Questo
significa, banalmente, che se dobbiamo scrivere un
file contenente una serie di numeri in virgola mobile
potremo semplicemente inserirli in un DoubleBuffer
e poi associarli ad un file in scrittura. E poi navigarlo
in lettura come crediamo, e cioè in modo assoluto
passandogli l'ordinale del numero che vogliamo leggere
dal file, oppure in modo relativo, cioè leggi
il prossimo, leggi il precedente. Un Buffer si istanzia
col metodo statico wrap, ad esempio:
int
myints[] = { 0, 1, 2, 3 };
IntWrapper iw = IntWrapper.wrap(myints);
che
leggeremo così:
while
(iw.hasRemaining())
System.out.println((int)iw.get());
Per
utilizzare un buffer in corrispondenza di una operazione
di I/O, dobbiamo aprire un canale. Cos'è un
canale, a questo punto? Il JavaDoc ci dice che è
un "nexus" per operazioni di I/O. Cioè
fondamentalmente si tratta di un mezzo di collegamento
tra un oggetto software e una periferica hardware
qualsiasi in grado di fare I/O. Allora è come
uno stream, direte voi. Questo è parzialmente
vero. Un canale ha due proprietà particolari:
è possibile chiuderlo in modo definitivo (nel
senso che un'operazione di I/O su un canale chiuso
verrà in ogni caso respinta) e non necessariamente
dal processo che lo ha aperto (dunque è asynchronously
closeable) e inoltre è possibile interrompere
le operazioni di I/O di thread concorrenti: vale a
dire che se il mio thread scopre che il canale che
deve usare è bloccato da un altro thread, può
interromperlo chiamando il metodo interrupt del thread
bloccante, che riceverà dal canale un'eccezione
di tipo ClosedByInterruptException, e quindi il thread
s'interromperà (tecnicamente, imposterà
a true il valore della variabile statica interrupted
di java.lang.thread). Un canale, dunque, è
un'astrazione molto potente e ci permetterà
di utilizzare, in particolare, il mattone principale
su cui è fondata la trasmissione dei dati su
Internet, il socket. Ma di questo parleremo più
avanti.
Supponiamo di aver aperto un file attraverso un Canale:
è necessario a questo punto mappare in memoria
il contenuto del file, per poi poterlo incapsulare
in un Buffer. La classe per farlo si chiama MappedByteBuffer,
che è istanziata dal metodo map di un FileChannel.
Tra gli argomenti di questo metodo troviamo la dimensione
del file, e se questo file è in lettura o scrittura.
Siccome MappedByteBuffer è un ByteBuffer, quest'ultimo
tipo specifico di Buffer può essere convertito
in uno qualsiasi degli altri (IntBuffer, DoubleBuffer,
CharBuffer ecc.). Se volessimo aprire un file di testo
ed estrarne un CharBuffer, ecco come dovremmo fare:
FileInputStream
fis = new FileInputStream(f);
FileChannel fc = fis.getChannel();
int sz = (int)fc.size();
MappedByteBuffer bb = fc.map(FileChannel.MAP_RO, 0,
sz);
CharBuffer cb = bb.asCharBuffer();
L'uso
di questi Buffer, oltre che essere molto semplice,
ci risolve una serie di problemi quotidiani non da
poco, come il salvataggio di file di configurazione,
la ricerca veloce all'interno di un insieme molto
grande di dati e via dicendo. Manca solo una cosa,
e mi sembra molto probabile che qualcuno l'abbia già
implementata (del resto tutta la filosofia di questi
Buffer non fa che richiamare i principi base di XML):
un bel DOMBuffer che implementi Buffer. Pensate: associare
ad un file scritto in un qualsiasi linguaggio XML,
ad esempio XHTML, un Buffer che tenga in memoria tutta
la sua struttura ad albero, con metodi getNode(),
getRoot() e via dicendo. Tutto questo in maniera automatica,
incapsulando le ostiche chiamate alle classi standard
di JAXP (Java API for XML Processing).
Coding
e decoding di charset differenti
Come ho detto, queste nuove funzioni di I/O nascono
soprattutto dalle nuove necessità di sviluppo
che Internet ha posto alla comunità di sviluppatori.
Una di queste è la codifica dei set di caratteri,
che i vari enti standardizzatori mondiali (per noi
c'è ISO) hanno da tempo categorizzato. Java
supporta il set di caratteri Unicode (per l'esattezza
Unicode 2.1 a partire dal JDK 1.1.7), peccato che
quasi nessun documento su Internet giri con quel formato.
Unicode è a 16 bit, come tutti sanno, ma la
gran parte dei set di caratteri antecedenti (UTF-8,
per esempio, che è il formato su cui girano
la maggior parte delle pagine HTML) ne supporta solo
otto. Bene, a partire da questa versione di Java avremo
a disposizione la classe java.nio.charset che ci permetterà
di utilizzare il charset che preferiamo (mappandolo
su Unicode), a patto che questo charset sia supportato
dallo IANA Charset Registry (http://www.iana.org/assignments/character-sets).
Utilizzare un charset significa codificare e decodificare
uno stream di byte, quindi è possibile associare
una specifica codifica ad un ByteBuffer, e da questo
ottenere, attraverso il metodo decode() il relativo
CharBuffer. Ecco come, ad esempio:
Charset
charset = Charset.forName("ISO-8859-1");
CharsetDecoder decoder = charset.newDecoder();
FileInputStream fis = new FileInputStream(f);
FileChannel fc = fis.getChannel();
int sz = (int)fc.size();
MappedByteBuffer bb = fc.map(FileChannel.MAP_RO, 0,
sz);
CharBuffer cb = decoder.decode(bb);
Pattern
matching
La
possibilità di utilizzare le espressioni regolari
non è una novità per chi programma,
per dirne una, in JavaScript, ma finora non c'era
modo di usarle in Java se non attraverso librerie
esterne oppure utilizzando StringTokenizer - che fa
sì una specie di pattern matching, ma non attraverso
le espressioni regolari. Benché occorra probabilmente
un intero articolo per trattarle almeno superficialmente,
posso dire che le espressioni regolari rappresentano
una notazione per filtrare un insieme di caratteri
in entrata attraverso un pattern (es.: i caratteri
alfanumerici, i caratteri di escape, le frasi che
cominciano con H maiuscolo, le stringhe che contengono
uno e un solo carattere @ e via dicendo). Il risultato
è il match tra i caratteri in entrata e le
regole che sono specificate dal pattern. Chi utilizza
Unix, fa pattern matching quando utilizza sed o awk.
Chi utilizza il DOS, benché anche questo non
supporti le espressioni regolari, fa pattern matching
quando scrive, per esempio,
dir
pro??.gif
Le
espressioni regolari sono una particolare sintassi
che specifica come descrivere il pattern. Qualche
esempio:
Con
Java 1.4 il supporto al pattern matching attraverso
le espressioni regolari è finalmente supportato.
Il fatto che lo si evidenzi all'interno delle nuove
funzionalità di I/O va visto essenzialmente
perché il pattern matching di fatto viene svolto
su di un CharBuffer (per l'esattezza su di una CharSequence,
che è la classe madre di CharBuffer, di String
e di StringBuffer), e quindi, volendo, su un Canale.
L'intero nuovo package java.util.regex consta delle
due nuove classi dal nome piuttosto self-explicative
di Pattern e Matcher. Come è facile intuire
con Pattern definiremo (compileremo, anzi, per dirla
con il termine usato dai progettisti Sun) il pattern
su cui Matcher effettuerà la sua comparazione.
Per esempio, per stabilire un pattern che filtri,
all'interno di un testo qualsiasi (multilinea!), una
stringa che termina con un ritorno carrello, scriveremo:
Pattern
linea = Pattern.compile("*\r?\n", Pattern.MULTILINE);
Per
effettuare un match su un testo in entrata, supponendo
di avere a disposizione un CharBuffer che si chiami
cb, utilizzeremo:
Matcher
match = linea.matcher(cb);
A
questo punto Match diventa un contenitore che è
navigabile semplicemente così:
while(m.find())
System.out.println("Trovato un match!");
Un'altra
applicazione molto utile delle espressioni regolari
riguarda il controllo di particolari formattazioni
di stringhe di input. Il seguente, per esempio, controlla
che input non abbia caratteri non validi per essere
un indirizzo di e-mail.
String
input = "tizio@sito.net";
Pattern p = Pattern.compile("[^A-Za-z0-9\\.\\@_\\-~#]+");
Matcher m = p.matcher(input);
if (m.find())
System.err.println("Indirizzo e-mail non valido.");
Socket
Channel e operazioni asincrone sul canale
Ma
tutte queste novità - leggi charset, canali,
buffer di caratteri - vi fanno venire in mente qualcosa?
Certamente vi saranno milioni di campi di applicazione,
ma a me ne viene in mente uno che li comprende tutti:
la scrittura di un server Web. In effetti, ora esiste
un modo per scrivere e leggere in modo efficiente
e scalabile (quindi utilizzando tecniche di multithreading
in cui ciascun thread possa agire in modo non bloccante
sul canale) via socket. Mi spiego meglio. Fino ad
ora, avessimo voluto scrivere un Web Server in Java
- a meno di utilizzare tecniche o librerie estranee
alla Java API - avremmo dovuto limitarci ad un thread
per connessione, e questo thread avrebbe il compito
di monitorare ciò che accade sulla connessione.
Ebbene, un canale ci permette di operare in modo asincrono
su una connessione. Ciò vuol dire che, una
volta aperto il canale, possiamo descrivergli gli
eventi a cui siamo interessati - per esempio: avvenuta
connessione, avvenuta lettura, avvenuta scrittura
- e aspettare che ci venga notificato quando l'evento
si verifica. Ad esempio, per un client HTTP:
SocketChannel
ch = SocketChannel.open();
ch.configureBlocking(false);
ch.connect( new InetSocketAddress(args[0],80 );
Selector sel = Selector.open();
ch.register(sel, SelectionKey.OP_CONNECT);
while (sel.select(500)){
[
]
}
Dunque:
il tipo di canale per questo genere di operazioni
è il SocketChannel. Specificheremo che vogliamo
utilizzarlo in modo non esclusivo (non bloccante).
Colleghiamo il canale alla URL voluta (args[0]) e
alla porta voluta. A questo punto associamo un Selector
al SocketChannel, specificando che siamo interessati
all'evento "avvenuta connessione" (OP_CONNECT).
Poi mettiamo il thread in attesa che l'evento si verifichi
(per 500 millisecondi). Ricordate quanto ho detto
prima a proposito dei canali, e che cioè è
possibile interromperli o chiuderli in modo asincrono
da un altro processo? Ecco spiegata la tecnica della
concorrenza su un solo canale (connessione) da parte
di più thread concorrenti. Cioè, al
verificarsi di un evento, il thread interrompe le
eventuali operazioni sul canale, lo usa, e lo rilascia,
in modo del tutto automatico e senza che lo sviluppatore
debba preoccuparsene. Ad esempio:
if
(ch.isConnectionPending())
ch.finishConnect();
else
ch.write(
);
Qui
supponiamo di essere all'interno del ciclo while di
cui sopra e di utilizzare il canale per effettuare
la connessione. Siccome il canale è non-bloccante
e asincrono, l'operazione di connessione richiesta
prima può o non può essere stata accolta.
Se non lo è stata, o si è verificato
un errore, nel qual caso riceveremo un' IOException,
oppure la connessione aspetta un aknowledge da parte
nostra, cosa che viene effettuata con finishConnect()
(notate come questo sia il vero spirito Internet,
e quindi sviluppare applicazioni asincrone quando
si utilizza un protocollo come HTTP risulti molto
più semplice e logico). Se la connessione è
attiva sul canale, è possibile inviare la nostra
sequenza di byte. Ricordo che questo accade nel preciso
momento in cui il sistema ci ha comunicato che l'evento
OP_CONNECT si è verificato. Al contrario, avessimo
scelto di utilizzare un canonico approccio bloccante,
avremmo dovuto avvertire che cominciavamo una sequenza
di connessione, con il metodo begin() di AbstractChannel
(il genitore di Channel), aspettare la stringa di
feedback del Web Server, quindi inviare l'acknowledge
e chiudere il canale, con end() di AbstractChannel.
boolean
completed = false;
try {
begin();
completed = ...; // Operazioni di
IO bloccanti
return ...;
}
finally {
end(completed);
}
Conclusioni
L'introduzione dei concetti di canale di I/O in Java,
con la sua possibile gestione attraverso la notifica
asincrona di eventi, è un significativo passo
in avanti e mi sembra che rappresenti un adeguamento
al megatrend in atto che prevede l'utilizzo di Internet
come supporto per l'invocazione remota dei metodi
(leggi SOAP e WebServices[3]) utilizzando XML con
tutti i suoi charset) come documento di scambio. Oltre
a questo, l'introduzione dei Buffer è certamente
benvenuta e la sua semplicità di utilizzo potrà
rendere questo il modo preferenziale per effettuare
I/O di dati strutturati.
Riferimenti
[1] Bruce Eckel - Thinking in Java 2nd Ed. - Cap.
10
[2] John Zukowski - New I/0 Functionality for JavaTM
2 Standard Edition 1.4 - http://developer.java.sun.com/developer/technicalArticles/releases/nio/
[3] BluePrints for Web Services - Initial Thoughts
- http://java.sun.com/j2ee/blueprints/WS_article.html
Esempi
Scarica gli esempi
descritti in questo articolo
Alessio
Saltarin è laureato in Ingegneria. Si occupa
di progettazione e sviluppo di sistemi e applicazioni
Web, in particolare usando Java 2 Enterprise Edition.
|