Gestion simple des délais d'expiration du réseau

De nombreux programmeurs redoutent l'idée de gérer les délais d'expiration du réseau. Une crainte courante est qu'un client réseau simple à thread unique sans prise en charge du délai d'expiration se transforme en un cauchemar multithread complexe, avec des threads séparés nécessaires pour détecter les délais d'expiration du réseau et une forme de processus de notification au travail entre le thread bloqué et l'application principale. Bien que ce soit une option pour les développeurs, ce n'est pas la seule. Gérer les délais d'expiration du réseau ne doit pas être une tâche difficile et, dans de nombreux cas, vous pouvez éviter complètement d'écrire du code pour des threads supplémentaires.

Lorsque vous travaillez avec des connexions réseau ou tout type de périphérique d'E / S, il existe deux classifications d'opérations:

  • Opérations de blocage : blocage de lecture ou d'écriture, l'opération attend jusqu'à ce que le périphérique d'E / S soit prêt
  • Opérations non bloquantes : une tentative de lecture ou d'écriture est effectuée, l'opération s'interrompt si le périphérique d'E / S n'est pas prêt

La mise en réseau Java est, par défaut, une forme de blocage des E / S. Ainsi, lorsqu'une application réseau Java lit à partir d'une connexion socket, elle attendra généralement indéfiniment s'il n'y a pas de réponse immédiate. Si aucune donnée n'est disponible, le programme continuera à attendre et aucun autre travail ne pourra être effectué. Une solution, qui résout le problème mais introduit un peu de complexité supplémentaire, consiste à faire effectuer l'opération par un deuxième thread; de cette façon, si le deuxième thread est bloqué, l'application peut toujours répondre aux commandes de l'utilisateur, ou même terminer le thread bloqué si nécessaire.

Cette solution est souvent employée, mais il existe une alternative beaucoup plus simple. Java prend également en charge réseau sans blocage d' E / S, qui peut être activé sur tout Socket, ServerSocketou DatagramSocket. Il est possible de spécifier la durée maximale pendant laquelle une opération de lecture ou d'écriture se bloquera avant de retourner le contrôle à l'application. Pour les clients réseau, il s'agit de la solution la plus simple et offre un code plus simple et plus gérable.

Le seul inconvénient des E / S réseau non bloquantes sous Java est qu'elles nécessitent un socket existant. Ainsi, bien que cette méthode soit parfaite pour les opérations normales de lecture ou d'écriture, une opération de connexion peut se bloquer pendant une période beaucoup plus longue, car il n'y a pas de méthode pour spécifier un délai d'expiration pour les opérations de connexion. De nombreuses applications nécessitent cette capacité; vous pouvez, cependant, éviter facilement le travail supplémentaire d'écriture de code supplémentaire. J'ai écrit une petite classe qui vous permet de spécifier une valeur de délai d'expiration pour une connexion. Il utilise un deuxième thread, mais les détails internes sont supprimés. Cette approche fonctionne bien, car elle fournit une interface d'E / S non bloquante et les détails du deuxième thread sont masqués.

E / S réseau non bloquantes

La manière la plus simple de faire quelque chose s'avère souvent la meilleure. S'il est parfois nécessaire d'utiliser des threads et des E / S bloquantes, dans la majorité des cas les E / S non bloquantes se prêtent à une solution beaucoup plus claire et élégante. Avec seulement quelques lignes de code, vous pouvez incorporer des supports de temporisation pour n'importe quelle application socket. Tu ne me crois pas? Continuer à lire.

Lorsque Java 1.1 a été publié, il incluait des modifications d'API dans le java.netpackage qui permettaient aux programmeurs de spécifier des options de socket. Ces options donnent aux programmeurs un meilleur contrôle sur la communication socket. Une option en particulier, SO_TIMEOUTest extrêmement utile, car elle permet aux programmeurs de spécifier la durée pendant laquelle une opération de lecture bloquera. Nous pouvons spécifier un délai court, voire aucun, et rendre notre code réseau non bloquant.

Voyons comment cela fonctionne. Une nouvelle méthode setSoTimeout ( int )a été ajoutée aux classes de socket suivantes:

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

Cette méthode nous permet de spécifier une durée maximale, en millisecondes, que les opérations réseau suivantes vont bloquer:

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

Chaque fois qu'une de ces méthodes est appelée, l'horloge commence à tourner. Si l'opération n'est pas bloquée, elle se réinitialisera et ne redémarrera qu'une fois l'une de ces méthodes appelée à nouveau; par conséquent, aucun délai d'expiration ne peut se produire à moins que vous n'effectuiez une opération d'E / S réseau. L'exemple suivant montre à quel point il peut être facile de gérer les délais d'expiration, sans recourir à plusieurs threads d'exécution:

// Créer un socket de datagramme sur le port 2000 pour écouter les paquets UDP entrants DatagramSocket dgramSocket = new DatagramSocket (2000); // Désactive le blocage des opérations d'E / S, en spécifiant un délai d'expiration de cinq secondes dgramSocket.setSoTimeout (5000);

L'attribution d'une valeur de délai d'attente empêche nos opérations réseau de se bloquer indéfiniment. À ce stade, vous vous demandez probablement ce qui se passera lorsqu'une opération réseau expirera. Plutôt que de renvoyer un code d'erreur, qui peut ne pas toujours être vérifié par les développeurs, un java.io.InterruptedIOExceptionmessage est renvoyé. La gestion des exceptions est un excellent moyen de gérer les conditions d'erreur et nous permet de séparer notre code normal de notre code de gestion des erreurs. En outre, qui vérifie religieusement chaque valeur de retour pour une référence nulle? En lançant une exception, les développeurs sont obligés de fournir un gestionnaire de capture pour les délais d'expiration.

L'extrait de code suivant montre comment gérer une opération d'expiration lors de la lecture à partir d'un socket TCP:

// Définit le délai d'expiration du socket pour dix secondes connection.setSoTimeout (10000); try {// Créer un DataInputStream pour la lecture à partir du socket DataInputStream din = new DataInputStream (connection.getInputStream ()); // Lire les données jusqu'à la fin des données pour (;;) {String line = din.readLine (); if (ligne! = null) System.out.println (ligne); sinon pause; }} // Exception levée lorsque le délai d'expiration du réseau se produit catch (InterruptedIOException iioe) {System.err.println ("L'hôte distant a expiré pendant l'opération de lecture"); } // Exception levée lorsqu'une erreur d'E / S réseau générale se produit catch (IOException ioe) {System.err.println ("Erreur d'E / S réseau -" + ioe); }

Avec seulement quelques lignes de code supplémentaires pour un try {}bloc catch, il est extrêmement facile de détecter les délais d'expiration du réseau. Une application peut alors répondre à la situation sans se bloquer. Par exemple, il peut commencer par avertir l'utilisateur ou par tenter d'établir une nouvelle connexion. Lors de l'utilisation de sockets de datagramme, qui envoient des paquets d'informations sans garantir la livraison, une application peut répondre à un délai d'expiration du réseau en renvoyant un paquet qui avait été perdu en transit. La mise en œuvre de ce support de délai d'expiration prend très peu de temps et conduit à une solution très propre. En effet, le seul moment où les E / S non bloquantes ne sont pas la solution optimale est lorsque vous devez également détecter des délais d'expiration sur les opérations de connexion, ou lorsque votre environnement cible ne prend pas en charge Java 1.1.

Gestion du délai d'expiration sur les opérations de connexion

Si votre objectif est d'obtenir une détection et une gestion complètes du délai d'expiration, vous devrez envisager des opérations de connexion. Lors de la création d'une instance de java.net.Socket, une tentative d'établissement d'une connexion est effectuée. Si la machine hôte est active, mais qu'aucun service n'est en cours d'exécution sur le port spécifié dans le java.net.Socketconstructeur, un ConnectionExceptionsera renvoyé et le contrôle reviendra à l'application. Cependant, si la machine est en panne ou s'il n'y a pas de route vers cet hôte, la connexion socket finira par expirer d'elle-même beaucoup plus tard. En attendant, votre application reste figée et il n'y a aucun moyen de modifier la valeur du délai d'expiration.

Bien que l'appel du constructeur de socket finisse par revenir, il introduit un délai significatif. Une façon de traiter ce problème est d'employer un deuxième thread, qui effectuera la connexion potentiellement bloquante, et d'interroger continuellement ce thread pour voir si une connexion a été établie.

Cela ne conduit cependant pas toujours à une solution élégante. Oui, vous pouvez convertir vos clients réseau en applications multithread, mais la quantité de travail supplémentaire requise pour ce faire est souvent prohibitive. Cela rend le code plus complexe et, lors de l'écriture d'une simple application réseau, l'effort requis est difficile à justifier. Si vous écrivez de nombreuses applications réseau, vous vous surprendrez à réinventer fréquemment la roue. Il existe cependant une solution plus simple.

J'ai écrit une classe simple et réutilisable que vous pouvez utiliser dans vos propres applications. La classe génère une connexion de socket TCP sans blocage pendant de longues périodes. Vous appelez simplement une getSocketméthode, en spécifiant le nom d'hôte, le port et le délai d'expiration, et recevez une socket. L'exemple suivant montre une demande de connexion:

// Se connecter à un serveur distant par nom d'hôte, avec un délai d'expiration de quatre secondes Socket connection = TimedSocket.getSocket ("server.my-network.net", 23, 4000); 

Si tout se passe bien, une socket sera retournée, tout comme les java.net.Socketconstructeurs standards . Si la connexion ne peut pas être établie avant l'expiration du délai spécifié, la méthode s'arrêtera et lèvera un java.io.InterruptedIOException, comme le feraient les autres opérations de lecture de socket lorsqu'un délai d'expiration a été spécifié à l'aide d'une setSoTimeoutméthode. Assez facile, hein?

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.

Toutes ces méthodes fournissent une interface contrôlée à l' SocketThreadinstance, sans permettre la modification externe des variables membres privées. L'exemple de code suivant montre la run()méthode du thread . Quand, et si, le constructeur de socket retourne a Socket, il sera assigné à une variable membre privée, à laquelle les méthodes d'accès donnent accès. La prochaine fois qu'un état de connexion est interrogé, en utilisant la SocketThread.isConnected()méthode, le socket sera disponible pour utilisation. La même technique est utilisée pour détecter les erreurs; si un java.io.IOExceptionest intercepté, il sera stocké dans un membre privé, auquel on peut accéder via les méthodes d' accès isError()et getException().