Prosta obsługa limitów czasu sieci

Wielu programistów obawia się obsługi limitów czasu sieci. Powszechną obawą jest to, że prosty, jednowątkowy klient sieciowy bez obsługi limitu czasu przerodzi się w złożony wielowątkowy koszmar, z oddzielnymi wątkami potrzebnymi do wykrywania przekroczeń czasu sieci i pewną formą procesu powiadamiania działającego między zablokowanym wątkiem a główną aplikacją. Chociaż jest to jedna opcja dla programistów, nie jest to jedyna. Radzenie sobie z przekroczeniem limitu czasu sieci nie musi być trudnym zadaniem, aw wielu przypadkach można całkowicie uniknąć pisania kodu dla dodatkowych wątków.

Podczas pracy z połączeniami sieciowymi lub dowolnym typem urządzenia we / wy istnieją dwie klasyfikacje operacji:

  • Operacje blokujące : Blokowanie odczytu lub zapisu, operacja czeka, aż urządzenie we / wy będzie gotowe
  • Operacje nieblokujące : podjęto próbę odczytu lub zapisu, operacja jest przerywana, jeśli urządzenie we / wy nie jest gotowe

Sieć Java jest domyślnie formą blokowania operacji wejścia / wyjścia. W związku z tym, gdy aplikacja sieciowa Java odczytuje dane z połączenia z gniazdem, zwykle czeka przez czas nieokreślony, jeśli nie ma natychmiastowej odpowiedzi. Jeśli żadne dane nie są dostępne, program będzie czekał i nie będzie można wykonać dalszej pracy. Jednym z rozwiązań, które rozwiązuje problem, ale wprowadza trochę dodatkowej złożoności, jest wykonanie operacji przez drugi wątek; w ten sposób, jeśli drugi wątek zostanie zablokowany, aplikacja może nadal odpowiadać na polecenia użytkownika, a nawet w razie potrzeby przerwać zatrzymany wątek.

To rozwiązanie jest często stosowane, ale istnieje znacznie prostsza alternatywa. Java obsługuje również powodują blokowania sieci I / O, które można aktywować na dowolnym Socket, ServerSocketlub DatagramSocket. Możliwe jest określenie maksymalnego czasu, przez jaki operacja odczytu lub zapisu zatrzyma się przed zwróceniem sterowania z powrotem do aplikacji. Dla klientów sieciowych jest to najłatwiejsze rozwiązanie i oferuje prostszy, łatwiejszy w zarządzaniu kod.

Jedyną wadą nieblokującego sieciowego we / wy w Javie jest to, że wymaga istniejącego gniazda. Tak więc, chociaż ta metoda jest idealna do normalnych operacji odczytu lub zapisu, operacja łączenia może utknąć na znacznie dłuższy okres, ponieważ nie ma metody określania limitu czasu operacji łączenia. Wiele aplikacji wymaga tej zdolności; możesz jednak łatwo uniknąć dodatkowej pracy związanej z pisaniem dodatkowego kodu. Napisałem małą klasę, która pozwala określić wartość limitu czasu dla połączenia. Używa drugiego wątku, ale wewnętrzne szczegóły są wyabstrahowane. To podejście działa dobrze, ponieważ zapewnia nieblokujący interfejs we / wy, a szczegóły drugiego wątku są ukryte.

Nieblokujące sieciowe wejścia / wyjścia

Najprostszy sposób zrobienia czegoś często okazuje się najlepszy. Chociaż czasami konieczne jest użycie wątków i blokowanie operacji we / wy, w większości przypadków nieblokujące operacje we / wy nadają się do znacznie jaśniejszego i bardziej eleganckiego rozwiązania. Mając tylko kilka wierszy kodu, możesz włączyć obsługę limitów czasu dla dowolnej aplikacji używającej gniazd. Nie wierzysz mi? Czytaj.

Kiedy Java 1.1 została wydana, zawierała zmiany API w java.netpakiecie, które umożliwiały programistom określanie opcji gniazd. Te opcje dają programistom większą kontrolę nad komunikacją przez gniazdo. Szczególnie jedna opcja SO_TIMEOUTjest niezwykle przydatna, ponieważ umożliwia programistom określenie czasu, przez jaki operacja odczytu będzie blokowana. Możemy określić krótkie opóźnienie lub wcale, i sprawić, by nasz kod sieciowy nie był blokowany.

Przyjrzyjmy się, jak to działa. setSoTimeout ( int )Do następujących klas gniazd dodano nową metodę :

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

Ta metoda pozwala nam określić maksymalny limit czasu w milisekundach, który będą blokować następujące operacje sieciowe:

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

Za każdym razem, gdy wywoływana jest jedna z tych metod, zegar zaczyna tykać. Jeśli operacja nie zostanie zablokowana, zostanie zresetowana i uruchomiona ponownie dopiero po ponownym wywołaniu jednej z tych metod; w rezultacie przekroczenie limitu czasu nigdy nie wystąpi, chyba że wykonasz operację we / wy sieci. Poniższy przykład pokazuje, jak łatwo można obsłużyć przekroczenia limitów czasu bez uciekania się do wielu wątków wykonania:

// Utwórz gniazdo datagramowe na porcie 2000, aby nasłuchiwać przychodzących pakietów UDP DatagramSocket dgramSocket = new DatagramSocket (2000); // Wyłącz blokowanie operacji we / wy, określając pięciosekundowy limit czasu dgramSocket.setSoTimeout (5000);

Przypisanie wartości limitu czasu zapobiega nieskończonemu blokowaniu naszych operacji sieciowych. W tym momencie prawdopodobnie zastanawiasz się, co się stanie, gdy upłynie limit czasu działania sieci. Zamiast zwracać kod błędu, który nie zawsze jest sprawdzany przez programistów, generowany jest komunikat a java.io.InterruptedIOException. Obsługa wyjątków to doskonały sposób radzenia sobie z warunkami błędu i pozwala nam oddzielić nasz normalny kod od naszego kodu obsługi błędów. Poza tym, kto religijnie sprawdza każdą zwracaną wartość pod kątem zerowego odniesienia? Zgłaszając wyjątek, programiści są zmuszeni zapewnić procedurę obsługi catch dla limitów czasu.

Poniższy fragment kodu pokazuje, jak obsłużyć operację przekroczenia limitu czasu podczas odczytu z gniazda TCP:

// Ustawia limit czasu gniazda na dziesięć sekund connection.setSoTimeout (10000); try {// Utwórz DataInputStream do odczytu z gniazda DataInputStream din = new DataInputStream (connection.getInputStream ()); // Czytaj dane do końca danych for (;;) {String line = din.readLine (); if (linia! = null) System.out.println (linia); inaczej złamać; }} // Zgłoszono wyjątek, gdy przekroczono limit czasu sieci catch (InterruptedIOException iioe) {System.err.println ("Przekroczono limit czasu hosta zdalnego podczas operacji odczytu"); } // Wyjątek zgłoszony, gdy wystąpi ogólny błąd we / wy sieci catch (IOException ioe) {System.err.println ("Błąd sieci we / wy -" + ioe); }

Mając tylko kilka dodatkowych linii kodu dla try {}bloku catch, niezwykle łatwo jest wychwycić limity czasu sieci. Aplikacja może wtedy zareagować na sytuację bez zwlekania. Na przykład może rozpocząć się od powiadomienia użytkownika lub próby ustanowienia nowego połączenia. Podczas korzystania z gniazd datagramowych, które wysyłają pakiety informacji bez gwarancji dostarczenia, aplikacja może odpowiedzieć na przekroczenie limitu czasu sieci, ponownie wysyłając pakiet utracony podczas przesyłania. Wdrożenie obsługi tego limitu czasu zajmuje bardzo mało czasu i prowadzi do bardzo czystego rozwiązania. Rzeczywiście, jedyny przypadek, w którym nieblokujące we / wy nie jest optymalnym rozwiązaniem, występuje wtedy, gdy trzeba również wykryć przekroczenia limitów czasu operacji łączenia lub gdy środowisko docelowe nie obsługuje języka Java 1.1.

Obsługa limitu czasu operacji łączenia

Jeśli Twoim celem jest osiągnięcie pełnego wykrywania i obsługi przekroczenia limitu czasu, musisz rozważyć operacje łączenia. Podczas tworzenia instancji java.net.Socketpodejmowana jest próba nawiązania połączenia. Jeśli komputer hosta jest aktywny, ale żadna usługa nie działa na porcie określonym w java.net.Socketkonstruktorze, ConnectionExceptionzostanie wyrzucony znak , a sterowanie wróci do aplikacji. Jeśli jednak maszyna jest wyłączona lub nie ma trasy do tego hosta, połączenie przez gniazdo w końcu wygaśnie samoistnie znacznie później. W międzyczasie aplikacja pozostaje zamrożona i nie ma możliwości zmiany wartości limitu czasu.

Chociaż wywołanie konstruktora gniazda w końcu powróci, wprowadza znaczne opóźnienie. Jednym ze sposobów rozwiązania tego problemu jest zastosowanie drugiego wątku, który wykona potencjalnie blokujące połączenie i ciągłe sprawdzanie tego wątku, aby sprawdzić, czy połączenie zostało nawiązane.

Nie zawsze jednak prowadzi to do eleganckiego rozwiązania. Tak, można przekształcić klientów sieci w aplikacje wielowątkowe, ale często ilość dodatkowej pracy wymaganej w tym celu jest wygórowana. To sprawia, że ​​kod jest bardziej złożony, a podczas pisania tylko prostej aplikacji sieciowej ilość wymaganego wysiłku jest trudna do uzasadnienia. Jeśli piszesz wiele aplikacji sieciowych, często odkrywasz koło na nowo. Jest jednak prostsze rozwiązanie.

Napisałem prostą klasę wielokrotnego użytku, której możesz używać we własnych aplikacjach. Klasa generuje połączenie TCP przez gniazdo bez opóźnień przez długi czas. Po prostu wywołujesz getSocketmetodę, określasz nazwę hosta, port i opóźnienie limitu czasu i otrzymujesz gniazdo. Poniższy przykład przedstawia żądanie połączenia:

// Połącz się ze zdalnym serwerem przez nazwę hosta, z czterosekundowym limitem czasu Socket connection = TimedSocket.getSocket ("server.my-network.net", 23, 4000); 

Jeśli wszystko pójdzie dobrze, zostanie zwrócone gniazdo, tak jak standardowe java.net.Socketkonstruktory. Jeśli nie można nawiązać połączenia przed upływem określonego limitu czasu, metoda zatrzyma się i wyrzuci java.io.InterruptedIOException, podobnie jak inne operacje odczytu gniazda, gdy limit czasu został określony przy użyciu setSoTimeoutmetody. Całkiem proste, co?

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.

Wszystkie te metody zapewniają kontrolowany interfejs dla SocketThreadinstancji, nie pozwalając na zewnętrzną modyfikację prywatnych zmiennych składowych. Poniższy przykład kodu przedstawia run()metodę wątku . Kiedy i jeśli konstruktor gniazda zwróci wartość Socket, zostanie ona przypisana do prywatnej zmiennej składowej, do której dostęp zapewniają metody dostępu. Przy następnym zapytaniu o stan połączenia przy użyciu tej SocketThread.isConnected()metody gniazdo będzie dostępne do użycia. Ta sama technika służy do wykrywania błędów; jeśli java.io.IOExceptionzostanie złapany, zostanie zapisany w prywatnym elemencie członkowskim, do którego można uzyskać dostęp za pomocą metod isError()i getException()akcesor.