Лесно боравене с мрежовите изчаквания

Много програмисти се страхуват от мисълта да се справят с мрежовите таймаути. Често срещаният страх е, че прост мрежов клиент с една нишка без поддръжка на таймаут ще се превърне в сложен многонишко кошмар, с отделни нишки, необходими за откриване на мрежови изчаквания, и някаква форма на процес на уведомяване при работа между блокираната нишка и основното приложение. Въпреки че това е една опция за разработчици, тя не е единствената. Справянето с мрежовите таймаути не трябва да е трудна задача и в много случаи можете напълно да избегнете писането на код за допълнителни нишки.

Когато работите с мрежови връзки или друг тип I / O устройства, има две класификации на операциите:

  • Блокиране на операции : Четене или запис на сергии, операцията изчаква, докато I / O устройството е готово
  • Неблокиращи операции : Извършен е опит за четене или запис, операцията се прекъсва, ако I / O устройството не е готово

Java мрежата по подразбиране е форма на блокиране на I / O. По този начин, когато Java мрежово приложение чете от сокет връзка, обикновено ще чака неопределено време, ако няма незабавен отговор. Ако няма налични данни, програмата ще продължи да чака и не може да се извърши допълнителна работа. Едно от решенията, което решава проблема, но въвежда малко допълнителна сложност, е втората нишка да изпълни операцията; по този начин, ако втората нишка бъде блокирана, приложението все още може да реагира на потребителски команди или дори да прекрати спряната нишка, ако е необходимо.

Това решение често се използва, но има много по-проста алтернатива. Java поддържа също nonblocking мрежа I / O, която може да се задейства при всеки Socket, ServerSocketили DatagramSocket. Възможно е да се определи максималната продължителност на времето, през която операцията за четене или запис ще спре, преди връщането на контрола обратно в приложението. За мрежовите клиенти това е най-лесното решение и предлага по-опростен и по-управляем код.

Единственият недостатък на неблокиращия мрежов вход / изход под Java е, че той изисква съществуващ сокет. По този начин, докато този метод е идеален за нормални операции за четене или запис, операцията за свързване може да спре за много по-дълъг период, тъй като няма метод за определяне на период на изчакване за операции на свързване. Много приложения изискват тази способност; можете обаче лесно да избегнете допълнителната работа по писане на допълнителен код. Написах малък клас, който ви позволява да посочите стойност на времето за изчакване за връзка. Той използва втора нишка, но вътрешните детайли се абстрахират. Този подход работи добре, тъй като осигурява неблокиращ I / O интерфейс, а подробностите за втората нишка са скрити от погледа.

Неблокиращ мрежов вход / изход

Най-простият начин да направите нещо често се оказва най-добрият начин. Въпреки че понякога е необходимо да се използват нишки и блокиране на I / O, в повечето случаи неблокиращите I / O се поддават на далеч по-ясно и по-елегантно решение. Само с няколко реда код можете да включите поддръжка за изчакване за всяко приложение на сокет. Не ми вярвате? Прочетете.

Когато Java 1.1 беше пусната, тя включва API промени в java.netпакета, които позволяват на програмистите да определят опции за сокет. Тези опции дават на програмистите по-голям контрол върху сокет комуникацията. Една от опциите по-специално SO_TIMEOUTе изключително полезна, защото позволява на програмистите да определят времето, което операцията за четене ще блокира. Можем да посочим кратко закъснение или изобщо никакво и да направим нашия мрежов код неблокиращ.

Нека да разгледаме как работи това. Добавен е нов метод setSoTimeout ( int )към следните класове сокети:

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

Този метод ни позволява да зададем максимална продължителност на изчакване, в милисекунди, която следните мрежови операции ще блокират:

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

Всеки път, когато се извика един от тези методи, часовникът започва да тиктака. Ако операцията не бъде блокирана, тя ще се нулира и ще се рестартира само след като един от тези методи бъде извикан отново; в резултат на това никога не може да настъпи време за изчакване, освен ако не извършите операция за мрежови вход / изход. Следващият пример показва колко лесно може да се справи с изчакванията, без да се прибягва до множество нишки на изпълнение:

// Създаване на сокет за дейтаграми на порт 2000 за прослушване на входящи UDP пакети DatagramSocket dgramSocket = нов DatagramSocket (2000); // Деактивиране на блокирането на входно / изходни операции, като се посочи петсекундно изчакване dgramSocket.setSoTimeout (5000);

Присвояването на стойност на времето за изчакване предотвратява блокирането на нашите мрежови операции за неопределено време. В този момент вероятно се чудите какво ще се случи, когато дадена мрежова операция изтече. Вместо да връща код за грешка, който не винаги може да бъде проверен от разработчиците, java.io.InterruptedIOExceptionсе хвърля a . Обработката на изключения е отличен начин за справяне със състояния на грешки и ни позволява да отделим нашия нормален код от нашия код за обработка на грешки. Освен това, кой религиозно проверява всяка връщана стойност за нулева препратка? Изхвърляйки изключение, разработчиците са принудени да предоставят манипулатор на улов за изчаквания.

Следният кодов фрагмент показва как да се обработва операция за изчакване при четене от TCP сокет:

// Задайте времето за изчакване на сокета за десет секунди connection.setSoTimeout (10000); опитайте {// Създайте DataInputStream за четене от сокет DataInputStream din = new DataInputStream (connection.getInputStream ()); // Четене на данни до края на данните за (;;) {String line = din.readLine (); if (линия! = нула) System.out.println (линия); иначе почивка; }} // Изключване, хвърлено при изчакване на мрежата catch (InterruptIOException iioe) {System.err.println ("Времето за отдалечен хост изтече по време на операцията за четене"); } // Изключване, хвърлено при възникване на обща мрежова I / O грешка catch (IOException ioe) {System.err.println ("Network I / O error -" + ioe); }

Само с няколко допълнителни реда код за try {}блок за хващане е изключително лесно да уловите мрежовите таймаути. След това приложението може да реагира на ситуацията, без да се отлага. Например, тя може да започне чрез уведомяване на потребителя или чрез опит за установяване на нова връзка. Когато се използват сокети за дейтаграми, които изпращат пакети с информация, без да гарантират доставка, приложението може да отговори на изчакване в мрежата, като изпрати отново пакет, който е загубен при транспортиране. Внедряването на тази поддръжка за изчакване отнема много малко време и води до много чисто решение. Всъщност единственият път, когато неблокиращият вход / изход не е оптималното решение, е когато трябва да откриете и таймаути при операции на свързване или когато целевата ви среда не поддържа Java 1.1.

Обработка на времето за изчакване при операции на свързване

Ако целта ви е да постигнете пълно откриване и обработка на времето за изчакване, тогава ще трябва да помислите за операции по свързване. Когато се създава екземпляр от java.net.Socket, се прави опит за установяване на връзка. Ако хост машината е активна, но на порта, посочен в java.net.Socketконструктора, не се изпълнява услуга , ConnectionExceptionще се хвърли а и контролата ще се върне в приложението. Ако обаче машината не работи или ако няма път към този хост, връзката на сокета в крайна сметка ще изтече сама по себе си много по-късно. Междувременно приложението ви остава замразено и няма начин да промените стойността на времето за изчакване.

Въпреки че извикването на конструктора на сокети в крайна сметка ще се върне, това води до значително забавяне. Един от начините за справяне с този проблем е да се използва втора нишка, която ще изпълни потенциално блокиращото свързване, и непрекъснато да анкетира тази нишка, за да види дали е установена връзка.

Това обаче не винаги води до елегантно решение. Да, можете да конвертирате мрежовите си клиенти в многонишкови приложения, но често количеството допълнителна работа, необходимо за това, е непосилно. Това прави кода по-сложен и когато пишете само обикновено мрежово приложение, необходимото усилие е трудно да се оправдае. Ако пишете много мрежови приложения, ще откриете, че често преоткривате колелото. Има обаче по-просто решение.

Написах прост, многократно използван клас, който можете да използвате в собствените си приложения. Класът генерира TCP сокет връзка, без да се отлага за дълги периоди от време. Просто извиквате getSocketметод, указвайки име на хост, порт и забавяне на времето за изчакване и получавате сокет. Следващият пример показва заявка за връзка:

// Свързване с отдалечен сървър по име на хост, с четири секунди изчакване Socket връзка = TimedSocket.getSocket ("server.my-network.net", 23, 4000); 

Ако всичко върви добре, сокет ще бъде върнат, точно както стандартните java.net.Socketконструктори. Ако връзката не може да бъде установена преди да е настъпил посоченият ви изчакване, методът ще спре и ще изведе java.io.InterruptedIOException, точно както биха направили други операции за четене на сокети, когато времето за изчакване е посочено с помощта на setSoTimeoutметод. Доста лесно, а?

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.

Всички тези методи осигуряват контролиран интерфейс към SocketThreadекземпляра, без да позволяват външна модификация на променливи с частен член. Следващият пример на код показва run()метода на нишката . Когато и ако конструкторът на сокет връща a Socket, той ще бъде присвоен на променлива на частен член, до която методите за достъп осигуряват достъп. Следващият път, когато се изиска състояние на връзката, използвайки SocketThread.isConnected()метода, сокетът ще бъде достъпен за използване. Същата техника се използва за откриване на грешки; ако a java.io.IOExceptionе уловен, той ще се съхранява в частен член, който може да бъде достъпен чрез методите isError()and getException()accessor.