PDA

View Full Version : [JAVA]SocketChannel, NoBlocking e read


Frank1962
09-11-2006, 10:21
Ho creato un server java utilizzando la proprietà non bloccante che il ServerSocketChannel offre in modo tale da registrare al Selector tutti i socket che si connettono al mio server con l'opzione 'SelectionKey.OP_READ' in modo tale da sapere quando posso leggere da un socket dei dati che mi arrivano: il dubbio che mi sorge però riguarda all'utilizzo della read per andare a leggere i dati e metterli nel buffer .....devo utilizzare un ciclo while finchè lo stream non mi ritorna -1 per leggere tutti i dati oppue sarà la select che mi ritornerà sempre ogni volta che nel canale rimarranno dei dati da leggere?
In molte implementazioni ho visto che quando la read ritorna -1 si chiude direttamente il canale come se fosse una disconnessione e non come un valore che indica che non ci sono più dati da leggere.
Help pls! :D

grazie
ciao

PGI-Bis
09-11-2006, 17:53
Se intendo correttamente il tuo dubbio, la risposta è "leggi solo i byte disponibili" e non "leggi finchè ci sono byte da leggere".

La lettura è bloccante: se il client vuole trasmettere un gigabyte di macerie e tu tenti di leggerli tutti in una volta il selettore si siede lì e aspetta che tu abbia finito di leggerli.

"non blocking" è l'accettazione delle connessioni. Un ServerSocket bloccante, una volta che abbia ricevuto una connessione, dialoga solo con il Socket connesso a meno che tu non passi il procedimento di gestione della connessione intervenuta ad un Thread diverso.

Il selettore associato ad un ServerSocket non bloccante spezza l'insieme dei procedimenti necessari per la comunicazione in tanti segmenti singolarmente bloccanti e il funzionamento dell'ingranaggio si basa sulla possibilità di dividere quelle operazioni in ulteriori segmenti la cui esecuzione è complessivamente distribuita dal selettore.

Dunque, leggi un numero prefissato di byte per volta, finchè non raggiungi il fatidico meno uno. Ad esempio, ogni volta che il selettore di passa la chiave leggibile, tu tenti di leggere 512 byte. Magari ce ne sono 6, magari ce ne sono cinquemila, ma tu gli fai sputare quelli che può con un solo read e comunque non più di 8, 20 512, un milione, quello che ti sembri il blocco delle dimensioni più opportune:

SocketChannel client = (SocketChannel)currentKey.channel();
FIXED_SIZE_BUFFER.clear();
int read = client.read(FIXED_SIZE_BUFFER);
FIXED_SIZE_BUFFER.flip();
//butti FIXED_SIZE_BUFFER in un secchio di byte associato al canale client

Ora, quando read è menu uno chiudi il canale. Be', non è detto. Dipende se la comunicazione è "spara e dimentica", un po' come per le comunicazioni http, oppure è permanente, come per una chat piuttosto che per un server che ospita la stanza di un gioco on-line.

Se devi trasferire un file e morta lì è chiaro che quando il client o il server ha finito di spedire, termina anche lo scopo della connessione.

Se invece dopo la ricezione del messaggio dal client (read == -1) il client si aspetta una risposta oppure è possibile che il client invii più di un messaggio in tempi diversi, allora la chiave deve permanere.

Puoi scegliere, a quel punto, di cambiare le operazioni a cui la chiave sia interessata (key.interestsOps(SelectionKey.OP_READ) se prima era un OP_WRITE) oppure mantenere la chiave nel selettore, ad esempio nel caso in cui già fosse registrata come OP_READ | OP_WRITE. In quel caso decidi come e quando trasmettere non sulla base dell'interesse dimostrato dalla chiave, ma affindandoti ad una meccanica da te stabilita.

Frank1962
09-11-2006, 19:37
La lettura è bloccante: se il client vuole trasmettere un gigabyte di macerie e tu tenti di leggerli tutti in una volta il selettore si siede lì e aspetta che tu abbia finito di leggerli.

il problema sorge quando il thread che gestisce il socketchannel che sta inviando i dati è diverso da quello del thread che gestisce il mio ServerSocketChannel ....che succede se mentre il thread gestore del socket sta aspettando per leggere dei dati con una read il serversocketchannel rileva che sono arrivati dei dati disponibili da leggere e passa a un thread servente il socketchannel pensando erroneamente che non ci sia nessuno che sta servendo quel socket?
Un'altra domanda che vorrei porti è se c'è la possibilità di effettuare una read con timeout? ....nel senso che dopo un tot di tempo mi ritorna il controllo anche se non ha effettuato nessuna lettura (in modo tale da chiudere il socket; così come una specie di timeout).

PGI-Bis
09-11-2006, 21:08
Con "la lettura è bloccante" intendo dire che se leggi da un socketchannel non bloccante nello stesso modo in cui leggi da un socketchannel bloccante ottieni una lettura bloccante.

Dato un buffer da 100 byte, l'operazione:

socket.read(buffer);

su un socketchannel bloccante tenta di leggere 100 byte. Ma nonostante il "bloccante", può leggerne meno di 100: dipende da quanti byte sono disponibili nel buffer del socketchannel. Così, per leggere 100 byte, diciamo una cosa tipo:

int totale = 0;
int read = 0;
while(read = socket.read(buffer) >= 0 && (totale += read) < 100) {
totale += read;
}

Qui o arrivano 100 byte o arriva un -1, che indica fine delle trasmissioni. Vale la pena notare che il semplice:

socket.read(buffer);

è sì bloccante, ma è bloccante rispetto alla quantità di byte presenti nel buffer del socketchannel al momento dell'invocazione read, non alla quantità di byte che si possono infilare in buffer. La cosa è irrilevante se gestisci un client con un Thread a parte perchè superi il blocco imposto dalla necessità di aspettare che arrivino 100 byte (o che arrivi il -1) proprio dedicando un flusso di esecuzione parallelo a questa attesa.

Per un socketchannel non bloccante il read fa praticamente le stesse cose ma, in teoria, può anche fare di meno: al massimo legge tanti byte quanti ce ne sono nel buffer del socket ma potrebbe benissimo restituire il controllo prima. L'unica cosa che è certa è che non comporta mai un blocco, a prescindere dalle dimensioni del bytebuffer parametro e da quanti byte ci sono nel buffer del socketchannel.

E qui arriva il selettore: con il selettore puoi fare in modo di leggere tutti e 100 i byte senza la necessità che questa operazioni richieda il blocco del Thread che gestisce la comunicazione. A conti fatti, leggi i 100 byte a blocchi: ogni volta che il selettore restituisce la chiave OP_READ associata a quel socketchannel tu leggi tutti i byte che puoi, senza bloccare, fino ad un totale, contando quelli letti nei passaggi precedenti, di 100.

Se vuoi impostare un timeout per un socket non bloccante, tutto quello che devi fare è registrare il momento in cui inizi la lettura e, ogni volta che ti viene data la chiave "isReadable" associata a quel canale, controllare se è spirata la scadenza di lettura (confrontanto il momento in cui esegui un read con il momento in cui hai tentato di leggere qualcosa da quel canale per la prima volta).

Frank1962
10-11-2006, 01:46
Quindi secondo te è lecito che, dopo che la select mi seleziona un socketchannel per la lettura, vado a fare un ciclo while su tale socket finchè non ricevo il fatidico -1? ( while(var!=-1) { } )
E se capita un piccolo "dealy" che, nell'ipotesi di un server web, mi comporta la ricezione a metà di un metodo POST con un messaggio di tot byte e, dopo qualche ms, la ricezione dell'altra metà come mi devo comportare? ...aspettare che la select mi ritorni di nuovo oppure il ciclo while mi "fermerà" il controllo creandomi un piccolo ritardo?
Se consideriamo la lunghezza massima di un segmento TCP, Maximum Segment Size (MSS), come, per esempio, di 1500byte e il metodo POST inviatomi dal client lungo più di 3000byte vorrà dire che per forza di cose i dati che compongono la richiesta http saranno inviati su più segmenti tcp e quindi è plausibile che mi possa capitarmi un dealy un pò "pesante" che vada ad incidere sulla lettura totale della richiesta http .....ecco perchè mi domandavo se c'era la possibilità che nel mezzo della lettura di tale richiesta ci possa essere più di un ritorno della select in lettura.

PGI-Bis
10-11-2006, 15:54
No perchè quel ciclo blocca il selettore.

La lettura dei byte in arrivo è parziale per definizione, nel senso che la natura non bloccante rende molto più probabile che un messaggio non sia interamente recepito con un solo read(buffer).

Quello che devi fare è, semplicemente, continuare ad accumulare i byte ottenuti da un read per il SocketChannel la cui chiave sia "readable" finchè non incontri un terminatore (ad esempio read restituisce -1).

Per accumulare i byte puoi usare l'allegato della chiave. Un SelectionKey ha un allegato arbitrariamente impostabile. Se, per avventura, quell'allegato fosse un ByteArrayOutputStream (ma è meglio che sia un Channel), quando la chiave restituita dal selettore è "readable" tu diresti:

ByteArrayOutputStream allegato =
(ByteArrayOutputStream)chiave.attachment();
int letti = chiave.channel().read(buffer);
buffer.flip();
//metti i byte di buffer in allegato
if(letti == -1) {
//passa i byte dell'allegato al processore dei messaggi
}

Crei l'allegato nel momento in cui registri il client presso il selettore che, in sè, è una cosa piuttosto semplice:

if(currentKey.isAcceptable()) {
SocketChannel client = accept(currentKey); //accept qui è un metodo ad hoc
client.configureBlocking(false);
SelectionKey clientKey = client.register(...op_read | op_write);
ByteArrayOuputStream attachment = new ByteArrayOutputStream();
clientKey.attack(attachment);
}

Frank1962
10-11-2006, 19:25
No perchè quel ciclo blocca il selettore.

La lettura dei byte in arrivo è parziale per definizione, nel senso che la natura non bloccante rende molto più probabile che un messaggio non sia interamente recepito con un solo read(buffer).

Quello che devi fare è, semplicemente, continuare ad accumulare i byte ottenuti da un read per il SocketChannel la cui chiave sia "readable" finchè non incontri un terminatore (ad esempio read restituisce -1).
.....
si ma prendiamo un esempio: se il client deve mandare un messaggio POST di dimensioni addirittura maggiori al buffer tcp di trasmissione del client stesso può capitare che il processo server si trovi con una read che ritorna -1 anche se in realtà non è arrivato tutto il messaggio di post? (nel senso che nell'effettuare un write su un socket, quindi nel buffer tcp di trasmissione, questa può bloccarsi per un pò se i dati che gli sono passati come argomento sono di dimensioni maggiori dello spazio libero disponibile in quel momento nel buffer)
è strano come in molti libri anche di ultima pubblicazione non venga trattato l'argomento dei socket non bloccanti, nel testo "Java manuale ufficiale" alla 4° edizione l'argomento non è minimamente toccato come del resto l'utilizzo della classe Selector!

PGI-Bis
11-11-2006, 02:16
Il -1 significa "end of stream", non "end of message". Cosa significhi "end of stream" per un Socket non è propriamente determinato. Ad esempio sappiamo che -1 è il valore ottenuto da un read sul canale di un socket per cui sia stato invocato "shutdownInput".

E' possibile che arrivi un -1 quando si legga da un Socket pur avendo questi ancora byte da inviare? Direi di no ma, sai com'è, tecnologia, magia...

Comunque -1 in lettura non significa che il SocketChannel sia da chiudere. In un SocketChannel è bidirezionale: ha due piste, una in lettura e una in scrittura. Nulla vieta che il flusso in del socket sia chiuso e quello out sia aperto. E' evidente, pertanto, che una chiusura dell'intero socket nel caso in cui la lettura restituisca -1 è fatta sulla base dell'assunto che lo scopo della connessione sia assolto al termine dell'invio (o ricezione, dipende se la vediamo dal lato client o server) del messaggio.

Se conti sulla capacità del client di trasmettere più d'un messaggio non userai -1 come terminatore esclusivo ma come terminatore eventuale. Imponi una condizione principale che segnali la ricezione dei byte necessari e sufficienti alla ricostruzione del messaggio inviato fondata su una convenzione.

Ad esempio, puoi stabilire che i messaggi abbiano la forma:

[4 byte = N][N byte]

cioè ogni messaggio inizia con un intero indicante il numero di byte che costituiscono il messaggio. A questo punto leggerai i primi quattro byte, determinerai N e, una volta ricevuti, applicherai un insieme di letture successive che termineranno quando il totale di byte letti sia pari a N.

Oppure usi un carattere ASCII, il tipico "new line indica fine del messaggio".

O usi la convenzione HTTP: anche lì ci sono una miriade di norme che determinano "quando un messaggio può considerarsi concluso" che nulla hanno ovviamente a che fare con il -1.