Gestione semplice dei timeout di rete

Molti programmatori temono l'idea di gestire i timeout di rete. Un timore comune è che un semplice client di rete a thread singolo senza supporto timeout si trasformi in un incubo multithread complesso, con thread separati necessari per rilevare i timeout di rete e una qualche forma di processo di notifica al lavoro tra il thread bloccato e l'applicazione principale. Sebbene questa sia un'opzione per gli sviluppatori, non è l'unica. Gestire i timeout di rete non deve essere un compito difficile e in molti casi è possibile evitare completamente di scrivere codice per thread aggiuntivi.

Quando si lavora con connessioni di rete o qualsiasi tipo di dispositivo I / O, ci sono due classificazioni di operazioni:

  • Operazioni di blocco : la lettura o la scrittura si blocca, l'operazione attende che il dispositivo I / O sia pronto
  • Operazioni non bloccanti : viene eseguito un tentativo di lettura o scrittura, l'operazione viene interrotta se il dispositivo I / O non è pronto

Il networking Java è, per impostazione predefinita, una forma di blocco dell'I / O. Pertanto, quando un'applicazione di rete Java legge da una connessione socket, generalmente attenderà indefinitamente se non c'è una risposta immediata. Se non sono disponibili dati, il programma continuerà ad attendere e non sarà possibile eseguire ulteriori operazioni. Una soluzione, che risolve il problema ma introduce un po 'di complessità in più, è avere un secondo thread che esegua l'operazione; in questo modo, se il secondo thread viene bloccato, l'applicazione può ancora rispondere ai comandi dell'utente o anche terminare il thread bloccato, se necessario.

Questa soluzione viene spesso utilizzata, ma esiste un'alternativa molto più semplice. Java supporta anche non bloccante rete di I / O, che può essere attivata su qualunque Socket, ServerSocketo DatagramSocket. È possibile specificare il periodo di tempo massimo durante il quale un'operazione di lettura o scrittura si bloccherà prima di restituire il controllo all'applicazione. Per i client di rete, questa è la soluzione più semplice e offre un codice più semplice e più gestibile.

L'unico svantaggio dell'I / O di rete non bloccante in Java è che richiede un socket esistente. Pertanto, sebbene questo metodo sia perfetto per le normali operazioni di lettura o scrittura, un'operazione di connessione può bloccarsi per un periodo molto più lungo, poiché non esiste un metodo per specificare un periodo di timeout per le operazioni di connessione. Molte applicazioni richiedono questa capacità; puoi, tuttavia, evitare facilmente il lavoro extra di scrivere codice aggiuntivo. Ho scritto una piccola classe che ti consente di specificare un valore di timeout per una connessione. Utilizza un secondo thread, ma i dettagli interni vengono astratti. Questo approccio funziona bene, in quanto fornisce un'interfaccia I / O non bloccante ei dettagli del secondo thread sono nascosti alla vista.

I / O di rete non bloccante

Il modo più semplice di fare qualcosa spesso risulta essere il modo migliore. Sebbene a volte sia necessario utilizzare thread e I / O di blocco, nella maggior parte dei casi l'I / O non bloccante si presta a una soluzione molto più chiara ed elegante. Con solo poche righe di codice, puoi incorporare i supporti di timeout per qualsiasi applicazione socket. Non mi credi? Continuare a leggere.

Quando è stato rilasciato Java 1.1, includeva modifiche API al java.netpacchetto che consentivano ai programmatori di specificare le opzioni del socket. Queste opzioni danno ai programmatori un maggiore controllo sulla comunicazione socket. Un'opzione in particolare, SO_TIMEOUTè estremamente utile, perché consente ai programmatori di specificare la quantità di tempo che un'operazione di lettura verrà bloccata. Possiamo specificare un breve ritardo, o nessuno, e rendere il nostro codice di rete non bloccante.

Diamo un'occhiata a come funziona. Un nuovo metodo setSoTimeout ( int )è stato aggiunto alle seguenti classi socket:

  • java.net.Socket
  • java.net.DatagramSocket
  • java.net.ServerSocket

Questo metodo ci consente di specificare una durata massima del timeout, in millisecondi, che le seguenti operazioni di rete bloccheranno:

  • ServerSocket.accept()
  • SocketInputStream.read()
  • DatagramSocket.receive()

Ogni volta che viene chiamato uno di questi metodi, l'orologio inizia a ticchettare. Se l'operazione non viene bloccata, verrà ripristinata e riavviata solo una volta che uno di questi metodi viene richiamato di nuovo; di conseguenza, non può verificarsi alcun timeout a meno che non si esegua un'operazione di I / O di rete. L'esempio seguente mostra quanto sia facile gestire i timeout, senza ricorrere a più thread di esecuzione:

// Crea un socket datagramma sulla porta 2000 per ascoltare i pacchetti UDP in arrivo DatagramSocket dgramSocket = new DatagramSocket (2000); // Disabilita il blocco delle operazioni di I / O, specificando un timeout di cinque secondi dgramSocket.setSoTimeout (5000);

L'assegnazione di un valore di timeout impedisce il blocco delle nostre operazioni di rete a tempo indeterminato. A questo punto, probabilmente ti starai chiedendo cosa succederà quando un'operazione di rete scade. Invece di restituire un codice di errore, che potrebbe non essere sempre controllato dagli sviluppatori, java.io.InterruptedIOExceptionviene generato un. La gestione delle eccezioni è un modo eccellente per trattare le condizioni di errore e ci consente di separare il nostro codice normale dal nostro codice di gestione degli errori. Inoltre, chi controlla religiosamente ogni valore restituito per un riferimento nullo? Generando un'eccezione, gli sviluppatori sono costretti a fornire un gestore di catch per i timeout.

Il seguente frammento di codice mostra come gestire un'operazione di timeout durante la lettura da un socket TCP:

// Imposta il timeout del socket per dieci secondi connection.setSoTimeout (10000); try {// Crea un DataInputStream per la lettura dal socket DataInputStream din = new DataInputStream (connection.getInputStream ()); // Legge i dati fino alla fine dei dati per (;;) {String line = din.readLine (); if (riga! = null) System.out.println (riga); altrimenti si rompono; }} // Eccezione generata quando si verifica il timeout di rete catch (InterrruptIOException iioe) {System.err.println ("Host remoto scaduto durante l'operazione di lettura"); } // Eccezione generata quando si verifica un errore di I / O di rete generale catch (IOException ioe) {System.err.println ("Errore di I / O di rete -" + ioe); }

Con solo poche righe di codice in più per un try {}blocco catch, è estremamente facile rilevare i timeout di rete. Un'applicazione può quindi rispondere alla situazione senza bloccarsi. Ad esempio, potrebbe iniziare avvisando l'utente o tentando di stabilire una nuova connessione. Quando si utilizzano i socket del datagramma, che inviano pacchetti di informazioni senza garantire la consegna, un'applicazione potrebbe rispondere a un timeout di rete reinviando un pacchetto che era stato perso durante il transito. L'implementazione di questo supporto timeout richiede pochissimo tempo e porta a una soluzione molto pulita. In effetti, l'unica volta in cui l'I / O non bloccante non è la soluzione ottimale è quando è necessario rilevare anche i timeout nelle operazioni di connessione o quando l'ambiente di destinazione non supporta Java 1.1.

Gestione del timeout nelle operazioni di connessione

Se il tuo obiettivo è ottenere il rilevamento e la gestione completi del timeout, dovrai considerare le operazioni di connessione. Quando si crea un'istanza di java.net.Socket, viene effettuato un tentativo di stabilire una connessione. Se la macchina host è attiva, ma nessun servizio è in esecuzione sulla porta specificata nel java.net.Socketcostruttore, ConnectionExceptionverrà lanciato un e il controllo tornerà all'applicazione. Tuttavia, se la macchina è inattiva, o se non è presente alcun percorso per quell'host, la connessione socket alla fine scadrà da sola molto più tardi. Nel frattempo, l'applicazione rimane bloccata e non è possibile modificare il valore di timeout.

Sebbene la chiamata al costruttore del socket alla fine restituirà, introduce un ritardo significativo. Un modo per risolvere questo problema è utilizzare un secondo thread, che eseguirà la connessione potenzialmente bloccante, e interrogare continuamente quel thread per vedere se è stata stabilita una connessione.

Ciò, tuttavia, non sempre porta a una soluzione elegante. Sì, potresti convertire i tuoi client di rete in applicazioni multithread, ma spesso la quantità di lavoro extra richiesta per farlo è proibitiva. Rende il codice più complesso e, quando si scrive solo una semplice applicazione di rete, è difficile giustificare lo sforzo richiesto. Se scrivi molte applicazioni di rete, ti ritroverai a reinventare spesso la ruota. Tuttavia, esiste una soluzione più semplice.

Ho scritto una classe semplice e riutilizzabile che puoi usare nelle tue applicazioni. La classe genera una connessione socket TCP senza interruzioni per lunghi periodi di tempo. Si chiama semplicemente un getSocketmetodo, specificando il nome host, la porta e il ritardo di timeout e si riceve un socket. L'esempio seguente mostra una richiesta di connessione:

// Connessione a un server remoto tramite nome host, con un timeout di quattro secondi Connessione socket = TimedSocket.getSocket ("server.my-network.net", 23, 4000); 

Se tutto va bene, verrà restituito un socket, proprio come i java.net.Socketcostruttori standard . Se la connessione non può essere stabilita prima che si verifichi il timeout specificato, il metodo si interromperà e lancerà un messaggio java.io.InterruptedIOException, proprio come farebbero altre operazioni di lettura del socket quando è stato specificato un timeout utilizzando un setSoTimeoutmetodo. Abbastanza facile, eh?

Encapsulating multithreaded network code into a single class

While the TimedSocket class is a useful component in itself, it's also a very good learning aid for understanding how to deal with blocking I/O. When a blocking operation is performed, a single-threaded application will become blocked indefinitely. If multiple threads of execution are used, however, only one thread need stall; the other thread can continue to execute. Let's take a look at how the TimedSocket class works.

When an application needs to connect to a remote server, it invokes the TimedSocket.getSocket() method and passes details of the remote host and port. The getSocket() method is overloaded, allowing both a String hostname and an InetAddress to be specified. This range of parameters should be sufficient for the majority of socket operations, though custom overloading could be added for special implementations. Inside the getSocket() method, a second thread is created.

The imaginatively named SocketThread will create an instance of java.net.Socket, which can potentially block for a considerable amount of time. It provides accessor methods to determine if a connection has been established or if an error has occurred (for example, if java.net.SocketException was thrown during the connect).

While the connection is being established, the primary thread waits until a connection is established, for an error to occur, or for a network timeout. Every hundred milliseconds, a check is made to see if the second thread has achieved a connection. If this check fails, a second check must be made to determine whether an error occurred in the connection. If not, and the connection attempt is still continuing, a timer is incremented and, after a small sleep, the connection will be polled again.

This method makes heavy use of exception handling. If an error occurs, then this exception will be read from the SocketThread instance, and it will be thrown again. If a network timeout occurs, the method will throw a java.io.InterruptedIOException.

The following code snippet shows the polling mechanism and error-handling code.

for (;;) { // Check to see if a connection is established if (st.isConnected()) { // Yes ... assign to sock variable, and break out of loop sock = st.getSocket(); break; } else { // Check to see if an error occurred if (st.isError()) { // No connection could be established throw (st.getException()); } try { // Sleep for a short period of time Thread.sleep ( POLL_DELAY ); } catch (InterruptedException ie) {} // Increment timer timer += POLL_DELAY; // Check to see if time limit exceeded if (timer > delay) { // Can't connect to server throw new InterruptedIOException ("Could not connect for " + delay + " milliseconds"); } } } 

Inside the blocked thread

While the connection is regularly polled, the second thread attempts to create a new instance of java.net.Socket. Accessor methods are provided to determine the state of the connection, as well as to get the final socket connection. The SocketThread.isConnected() method returns a boolean value to indicate whether a connection has been established, and the SocketThread.getSocket() method returns a Socket. Similar methods are provided to determine if an error has occurred, and to access the exception that was caught.

Tutti questi metodi forniscono un'interfaccia controllata SocketThreadall'istanza, senza consentire la modifica esterna delle variabili membro private. Il seguente esempio di codice mostra il run()metodo del thread . Quando e se il costruttore del socket restituisce a Socket, verrà assegnato a una variabile membro privata, a cui i metodi di accesso forniscono l'accesso. La prossima volta che viene richiesto uno stato di connessione, utilizzando il SocketThread.isConnected()metodo, il socket sarà disponibile per l'uso. La stessa tecnica viene utilizzata per rilevare gli errori; se java.io.IOExceptionviene rilevato, verrà archiviato in un membro privato, a cui è possibile accedere tramite i metodi di accesso isError()e getException().