Einfache Handhabung von Netzwerk-Timeouts

Viele Programmierer fürchten den Gedanken, mit Netzwerk-Timeouts umzugehen. Eine häufige Befürchtung besteht darin, dass ein einfacher Netzwerkclient mit einem Thread ohne Timeout-Unterstützung zu einem komplexen Multithread-Albtraum wird, in dem separate Threads zum Erkennen von Netzwerk-Timeouts und eine Art Benachrichtigungsprozess zwischen dem blockierten Thread und der Hauptanwendung erforderlich sind. Dies ist zwar eine Option für Entwickler, aber nicht die einzige. Der Umgang mit Netzwerk-Timeouts muss keine schwierige Aufgabe sein, und in vielen Fällen können Sie das Schreiben von Code für zusätzliche Threads vollständig vermeiden.

Bei der Arbeit mit Netzwerkverbindungen oder einem beliebigen E / A-Gerät gibt es zwei Klassifizierungen von Vorgängen:

  • Blockieren von Vorgängen : Lese- oder Schreibstillstände, Vorgang wartet, bis das E / A-Gerät bereit ist
  • Nicht blockierende Vorgänge : Es wird ein Lese- oder Schreibversuch unternommen. Der Vorgang wird abgebrochen, wenn das E / A-Gerät nicht bereit ist

Java-Netzwerke sind standardmäßig eine Form des Blockierens von E / A. Wenn eine Java-Netzwerkanwendung von einer Socket-Verbindung liest, wartet sie im Allgemeinen auf unbestimmte Zeit, wenn keine sofortige Antwort erfolgt. Wenn keine Daten verfügbar sind, wartet das Programm weiter und es können keine weiteren Arbeiten durchgeführt werden. Eine Lösung, die das Problem löst, aber ein wenig zusätzliche Komplexität mit sich bringt, besteht darin, dass ein zweiter Thread die Operation ausführt. Auf diese Weise kann die Anwendung, wenn der zweite Thread blockiert wird, weiterhin auf Benutzerbefehle reagieren oder den blockierten Thread bei Bedarf sogar beenden.

Diese Lösung wird oft verwendet, aber es gibt eine viel einfachere Alternative. Java unterstützt nicht blockierend auch Netzwerk - I / O, die auf jedem aktiviert werden können Socket, ServerSocketoder DatagramSocket. Es ist möglich, die maximale Zeitdauer anzugeben, die ein Lese- oder Schreibvorgang blockiert, bevor die Steuerung an die Anwendung zurückgegeben wird. Für Netzwerkclients ist dies die einfachste Lösung und bietet einfacheren, besser verwaltbaren Code.

Der einzige Nachteil bei nicht blockierenden Netzwerk-E / A unter Java besteht darin, dass ein vorhandener Socket erforderlich ist. Während diese Methode für normale Lese- oder Schreibvorgänge perfekt ist, kann eine Verbindungsoperation für einen viel längeren Zeitraum blockieren, da es keine Methode zum Festlegen einer Zeitüberschreitungsperiode für Verbindungsvorgänge gibt. Viele Anwendungen erfordern diese Fähigkeit. Sie können jedoch die zusätzliche Arbeit des Schreibens von zusätzlichem Code leicht vermeiden. Ich habe eine kleine Klasse geschrieben, mit der Sie einen Zeitüberschreitungswert für eine Verbindung angeben können. Es wird ein zweiter Thread verwendet, aber die internen Details werden weg abstrahiert. Dieser Ansatz funktioniert gut, da er eine nicht blockierende E / A-Schnittstelle bietet und die Details des zweiten Threads nicht sichtbar sind.

Nicht blockierende Netzwerk-E / A.

Der einfachste Weg, etwas zu tun, erweist sich oft als der beste Weg. Während es manchmal notwendig ist, Threads zu verwenden und E / A zu blockieren, bietet sich in den meisten Fällen eine nicht blockierende E / A für eine weitaus klarere und elegantere Lösung an. Mit nur wenigen Codezeilen können Sie Timeout-Unterstützung für jede Socket-Anwendung integrieren. Glaubst du mir nicht? Weiter lesen.

Als Java 1.1 veröffentlicht wurde, enthielt es API-Änderungen am java.netPaket, mit denen Programmierer Socket-Optionen angeben konnten. Diese Optionen geben Programmierern eine bessere Kontrolle über die Socket-Kommunikation. Insbesondere eine Option SO_TIMEOUTist äußerst nützlich, da Programmierer damit festlegen können, wie lange ein Lesevorgang blockiert werden soll. Wir können eine kurze oder gar keine Verzögerung angeben und unseren Netzwerkcode nicht blockieren.

Schauen wir uns an, wie das funktioniert. setSoTimeout ( int )Den folgenden Socket-Klassen wurde eine neue Methode hinzugefügt:

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

Mit dieser Methode können wir eine maximale Zeitüberschreitungslänge in Millisekunden angeben, die von den folgenden Netzwerkoperationen blockiert wird:

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

Immer wenn eine dieser Methoden aufgerufen wird, beginnt die Uhr zu ticken. Wenn der Vorgang nicht blockiert ist, wird er zurückgesetzt und erst neu gestartet, wenn eine dieser Methoden erneut aufgerufen wird. Infolgedessen kann niemals eine Zeitüberschreitung auftreten, es sei denn, Sie führen eine Netzwerk-E / A-Operation aus. Das folgende Beispiel zeigt, wie einfach es sein kann, Zeitüberschreitungen zu handhaben, ohne auf mehrere Ausführungsthreads zurückzugreifen:

// Erstellen Sie einen Datagramm-Socket an Port 2000, um auf eingehende UDP-Pakete zu warten. DatagramSocket dgramSocket = new DatagramSocket (2000); // Deaktivieren Sie blockierende E / A-Vorgänge, indem Sie ein Timeout von fünf Sekunden angeben. DgramSocket.setSoTimeout (5000);

Das Zuweisen eines Timeout-Werts verhindert, dass unsere Netzwerkoperationen auf unbestimmte Zeit blockiert werden. An diesem Punkt fragen Sie sich wahrscheinlich, was passieren wird, wenn ein Netzwerkbetrieb abläuft. Anstatt einen Fehlercode zurückzugeben, der möglicherweise nicht immer von Entwicklern überprüft wird, java.io.InterruptedIOExceptionwird a ausgelöst. Die Ausnahmebehandlung ist eine hervorragende Möglichkeit, mit Fehlerbedingungen umzugehen, und ermöglicht es uns, unseren normalen Code von unserem Fehlerbehandlungscode zu trennen. Außerdem, wer überprüft religiös jeden Rückgabewert auf eine Nullreferenz? Durch das Auslösen einer Ausnahme müssen Entwickler einen Catch-Handler für Zeitüberschreitungen bereitstellen.

Das folgende Codefragment zeigt, wie eine Zeitüberschreitungsoperation beim Lesen von einem TCP-Socket behandelt wird:

// Socket-Timeout für zehn Sekunden einstellen connection.setSoTimeout (10000); try {// Erstelle einen DataInputStream zum Lesen aus dem Socket DataInputStream din = new DataInputStream (connection.getInputStream ()); // Daten bis zum Ende der Daten lesen für (;;) {String line = din.readLine (); if (line! = null) System.out.println (line); sonst brechen; }} // Ausnahme ausgelöst, wenn ein Netzwerk-Timeout auftritt catch (InterruptedIOException iioe) {System.err.println ("Zeitüberschreitung des Remote-Hosts während des Lesevorgangs"); } // Ausnahme ausgelöst, wenn ein allgemeiner Netzwerk-E / A-Fehler auftritt catch (IOException ioe) {System.err.println ("Netzwerk-E / A-Fehler -" + ioe); }}

Mit nur wenigen zusätzlichen Codezeilen für einen try {}Catch-Block ist es extrem einfach, Netzwerk-Timeouts abzufangen. Eine Anwendung kann dann auf die Situation reagieren, ohne sich selbst zu blockieren. Sie können beispielsweise den Benutzer benachrichtigen oder versuchen, eine neue Verbindung herzustellen. Bei Verwendung von Datagramm-Sockets, die Informationspakete senden, ohne die Zustellung zu garantieren, kann eine Anwendung auf ein Netzwerk-Timeout reagieren, indem sie ein Paket erneut sendet, das während der Übertragung verloren gegangen ist. Die Implementierung dieser Timeout-Unterstützung nimmt sehr wenig Zeit in Anspruch und führt zu einer sehr sauberen Lösung. In der Tat ist die nicht blockierende E / A nicht die optimale Lösung, wenn Sie auch Zeitüberschreitungen bei Verbindungsvorgängen erkennen müssen oder wenn Ihre Zielumgebung Java 1.1 nicht unterstützt.

Timeout-Behandlung bei Verbindungsvorgängen

Wenn Sie eine vollständige Erkennung und Behandlung von Zeitüberschreitungen erreichen möchten, müssen Sie Verbindungsvorgänge in Betracht ziehen. Beim Erstellen einer Instanz von java.net.Socketwird versucht, eine Verbindung herzustellen. Wenn der Hostcomputer aktiv ist, aber auf dem im java.net.SocketKonstruktor angegebenen Port kein Dienst ausgeführt wird , ConnectionExceptionwird a ausgelöst und die Steuerung kehrt zur Anwendung zurück. Wenn der Computer jedoch ausfällt oder keine Route zu diesem Host vorhanden ist, tritt die Socket-Verbindung viel später von selbst auf. In der Zwischenzeit bleibt Ihre Anwendung eingefroren und es gibt keine Möglichkeit, den Timeout-Wert zu ändern.

Obwohl der Aufruf des Socket-Konstruktors irgendwann zurückkehrt, führt dies zu einer erheblichen Verzögerung. Eine Möglichkeit, dieses Problem zu lösen, besteht darin, einen zweiten Thread zu verwenden, der die möglicherweise blockierende Verbindung ausführt, und diesen Thread kontinuierlich abzufragen, um festzustellen, ob eine Verbindung hergestellt wurde.

Dies führt jedoch nicht immer zu einer eleganten Lösung. Ja, Sie könnten Ihre Netzwerkclients in Multithread-Anwendungen konvertieren, aber häufig ist der dafür erforderliche zusätzliche Arbeitsaufwand unerschwinglich. Dies macht den Code komplexer, und wenn nur eine einfache Netzwerkanwendung geschrieben wird, ist der erforderliche Aufwand schwer zu rechtfertigen. Wenn Sie viele Netzwerkanwendungen schreiben, werden Sie das Rad häufig neu erfinden. Es gibt jedoch eine einfachere Lösung.

Ich habe eine einfache, wiederverwendbare Klasse geschrieben, die Sie in Ihren eigenen Anwendungen verwenden können. Die Klasse generiert eine TCP-Socket-Verbindung, ohne für längere Zeit zu blockieren. Sie rufen einfach eine getSocketMethode auf, geben den Hostnamen, den Port und die Zeitüberschreitungsverzögerung an und erhalten einen Socket. Das folgende Beispiel zeigt eine Verbindungsanforderung:

// Über Hostname mit einem Timeout von vier Sekunden eine Verbindung zu einem Remote-Server herstellen. Socket connection = TimedSocket.getSocket ("server.my-network.net", 23, 4000); 

Wenn alles gut geht, wird ein Socket zurückgegeben, genau wie bei den Standardkonstruktoren java.net.Socket. Wenn die Verbindung nicht hergestellt werden kann, bevor Ihr angegebenes Zeitlimit auftritt, stoppt die Methode und löst ein aus java.io.InterruptedIOException, genau wie bei anderen Socket-Lesevorgängen, wenn ein Zeitlimit mithilfe einer setSoTimeoutMethode angegeben wurde. Ziemlich einfach, oder?

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.

Alle diese Methoden bieten eine kontrollierte Schnittstelle zur SocketThreadInstanz, ohne die externe Änderung privater Mitgliedsvariablen zuzulassen. Das folgende Codebeispiel zeigt die run()Methode des Threads . Wenn und wenn der Socket-Konstruktor a zurückgibt Socket, wird er einer privaten Mitgliedsvariablen zugewiesen, auf die die Zugriffsmethoden Zugriff gewähren. Wenn das nächste Mal ein Verbindungsstatus mithilfe der SocketThread.isConnected()Methode abgefragt wird , steht der Socket zur Verwendung zur Verfügung. Dieselbe Technik wird verwendet, um Fehler zu erkennen. Wenn a java.io.IOExceptionabgefangen wird, wird es in einem privaten Mitglied gespeichert, auf das über die Methoden isError()und getException()accessor zugegriffen werden kann.