Jednostavno rukovanje mrežnim vremenskim ograničenjima

Mnogi programeri plaše se pomisli da će riješiti mrežna isteka. Uobičajeni strah je da će se jednostavni mrežni klijent s jednim navojem bez podrške za vremensko ograničenje pretvoriti u složenu višenitnu noćnu moru, s odvojenim nitima potrebnim za otkrivanje mrežnih vremenskih ograničenja, te nekim oblikom postupka obavještavanja između blokirane niti i glavne aplikacije. Iako je ovo jedna opcija za programere, nije jedina. Suočavanje s mrežnim prekidima ne mora biti težak zadatak, a u mnogim slučajevima možete potpuno izbjeći pisanje koda za dodatne niti.

Kada radite s mrežnim vezama ili bilo kojom vrstom I / O uređaja, postoje dvije klasifikacije operacija:

  • Blokiranje operacija : Stanje čitanja ili pisanja, operacija čeka dok I / O uređaj ne bude spreman
  • Neblokirajuće operacije : Pokušaj čitanja ili pisanja je izvršen, operacija se prekida ako I / O uređaj nije spreman

Java umrežavanje je prema zadanim postavkama oblik blokiranja U / I. Dakle, kada Java mrežna aplikacija čita s priključka na utičnicu, obično će čekati neograničeno ako nema trenutnog odgovora. Ako nema dostupnih podataka, program će nastaviti čekati i ne može se raditi dalje. Jedno rješenje koje rješava problem, ali unosi malo dodatne složenosti, jest da druga nit izvede operaciju; na taj način, ako se druga nit blokira, aplikacija i dalje može reagirati na korisničke naredbe ili čak zaustaviti zaustavljeni nit ako je potrebno.

Ovo se rješenje često koristi, ali postoji puno jednostavnija alternativa. Java također podržava nonblocking mreža I / O, koja se može aktivirati na bilo koji Socket, ServerSocketili DatagramSocket. Moguće je odrediti maksimalno vrijeme tijekom kojeg će se operacija čitanja ili pisanja zaustaviti prije vraćanja kontrole natrag u aplikaciju. Za mrežne klijente ovo je najlakše rješenje i nudi jednostavniji kod kojim se lakše može upravljati.

Jedini nedostatak neblokiranog mrežnog I / O pod Javom je taj što zahtijeva postojeću utičnicu. Dakle, iako je ova metoda savršena za normalne operacije čitanja ili pisanja, operacija povezivanja može zastati mnogo dulje razdoblje, jer ne postoji metoda za određivanje razdoblja prekida za operacije povezivanja. Mnoge aplikacije zahtijevaju ovu sposobnost; međutim, lako možete izbjeći dodatni posao pisanja dodatnog koda. Napisao sam malu klasu koja vam omogućuje da odredite vrijednost vremenskog ograničenja za vezu. Koristi drugu nit, ali unutarnji detalji su apstrahirani. Ovaj pristup dobro funkcionira jer pruža neblokirajuće I / O sučelje, a detalji druge niti skriveni su od pogleda.

Neblokirajući mrežni I / O

Najjednostavniji način da se nešto učini često se pokaže najboljim načinom. Iako je ponekad potrebno koristiti niti i blokiranje U / I, u većini slučajeva neblokiranje U / I daje daleko jasnije i elegantnije rješenje. Sa samo nekoliko redaka koda možete uključiti podršku za istek vremena za bilo koju aplikaciju soketa. Ne vjerujete mi? Nastavi čitati.

Kada je Java 1.1 objavljena, uključivala je API promjene java.netpaketa koje su programerima omogućavale da odrede opcije soketa. Ove opcije programerima daju veću kontrolu nad socket komunikacijom. Jedna je opcija posebno SO_TIMEOUTizuzetno korisna jer programerima omogućuje određivanje vremena koje će operacija čitanja blokirati. Možemo odrediti kratko odgađanje ili nikakvo i učiniti naš mrežni kod neblokiranim.

Pogledajmo kako to funkcionira. Nova metoda setSoTimeout ( int )dodana je u sljedeće klase soketa:

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

Ova metoda omogućuje nam da odredimo maksimalnu dužinu vremenskog ograničenja, u milisekundama, koju će blokirati sljedeće mrežne operacije:

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

Kad god se pozove jedna od ovih metoda, sat počinje otkucavati. Ako operacija nije blokirana, resetirat će se i ponovno pokrenuti samo nakon što se ponovo pozove jedna od ovih metoda; kao rezultat toga, nikad neće doći do vremenskog ograničenja ako ne izvršite mrežnu I / O operaciju. Sljedeći primjer pokazuje koliko lako može biti rukovanje vremenskim ograničenjima, bez pribjegavanja više niti izvršavanja:

// Stvorimo datagramsku utičnicu na portu 2000 za preslušavanje dolaznih UDP paketa DatagramSocket dgramSocket = novi DatagramSocket (2000); // Onemogućiti blokiranje U / I operacija, određivanjem pet sekundi isteka dgramSocket.setSoTimeout (5000);

Dodjeljivanje vrijednosti vremenskog ograničenja sprečava naše mrežne operacije da se blokiraju na neodređeno vrijeme. U ovom se trenutku vjerojatno pitate što će se dogoditi kada mrežna operacija istekne. Umjesto vraćanja koda pogreške, koji programeri možda neće uvijek provjeriti, java.io.InterruptedIOExceptionbaca se a. Rukovanje iznimkama izvrstan je način rješavanja stanja pogrešaka i omogućuje nam razdvajanje našeg uobičajenog koda od našeg koda za postupanje s pogreškama. Osim toga, tko religiozno provjerava svaku povratnu vrijednost za nulu referencu? Ubacivanjem iznimke, programeri su prisiljeni pružiti obrađivač ulova za vremenska ograničenja.

Sljedeći isječak koda pokazuje kako se rukuje operacijom vremenskog ograničenja prilikom čitanja iz TCP utičnice:

// Postavite vrijeme čekanja utičnice na deset sekundi connection.setSoTimeout (10000); pokušajte {// Stvorite DataInputStream za čitanje iz utičnice DataInputStream din = new DataInputStream (connection.getInputStream ()); // Čitanje podataka do kraja podataka za (;;) {String line = din.readLine (); if (linija! = null) System.out.println (linija); inače prekid; }} // Iznimka izbačena kada dođe do mrežnog isteka vremena catch (InterruptedIOException iioe) {System.err.println ("Vremensko ograničenje udaljenog hosta isteklo tijekom operacije čitanja"); } // Iznimka izbačena kada se dogodi opća mrežna I / O pogreška catch (IOException ioe) {System.err.println ("Pogreška I / O mreže -" + ioe); }

Uz samo nekoliko dodatnih redaka koda za try {}catch block, izuzetno je jednostavno uhvatiti mrežna vremena. Tada aplikacija može odgovoriti na situaciju, a da se sama ne zaustavi. Na primjer, mogao bi započeti obavještavanjem korisnika ili pokušajem uspostavljanja nove veze. Kada se koriste utičnice datagrama, koje šalju pakete informacija bez jamstva isporuke, aplikacija bi mogla odgovoriti na mrežno vrijeme čekanja ponovnim slanjem paketa koji je izgubljen u prijevozu. Implementacija ove podrške za vremensko ograničenje oduzima vrlo malo vremena i dovodi do vrlo čistog rješenja. Zapravo, jedini put koji neblokirajući I / O nije optimalno rješenje je kada također trebate otkriti vremenske ograničenja u operacijama povezivanja ili kada vaše ciljno okruženje ne podržava Javu 1.1.

Rukovanje vremenskim ograničenjem na operacijama povezivanja

Ako je vaš cilj postići potpuno otkrivanje i rukovanje vremenskim ograničenjem, tada ćete morati razmotriti operacije povezivanja. Prilikom izrade instance java.net.Socketpokušava se uspostaviti veza. Ako je računalo domaćin aktivno, ali nijedna usluga nije pokrenuta na priključku navedenom u java.net.Socketkonstruktoru, ConnectionExceptionbacit će se a i kontrola će se vratiti u aplikaciju. Međutim, ako je stroj isključen ili ako ne postoji put do tog domaćina, veza utičnice na kraju će sama po sebi isteći mnogo kasnije. U međuvremenu vaša aplikacija ostaje zamrznuta i ne postoji način za promjenu vrijednosti vremenskog ograničenja.

Iako će se poziv konstruktora soketa na kraju vratiti, to uvodi značajno kašnjenje. Jedan od načina rješavanja ovog problema je zapošljavanje druge niti koja će izvoditi potencijalno blokirajuće povezivanje i kontinuirano ispitivanje te niti kako bi se utvrdilo je li veza uspostavljena.

Međutim, to ne dovodi uvijek do elegantnog rješenja. Da, svoje mrežne klijente možete pretvoriti u višenitne programe, ali često je potreban dodatni rad koji je potreban za to. To čini kod složenijim, a pri pisanju samo jednostavne mrežne aplikacije teško je opravdati potrebni napor. Ako napišete puno mrežnih aplikacija, često biste pronalazili kotač. Postoji, međutim, jednostavnije rješenje.

Napisao sam jednostavnu klasu za višekratnu upotrebu koju možete koristiti u vlastitim aplikacijama. Klasa generira vezu TCP utičnice bez zastoja na dulje vremensko razdoblje. Jednostavno pozovete getSocketmetodu, navodeći naziv hosta, priključak i kašnjenje i primite utičnicu. Sljedeći primjer prikazuje zahtjev za povezivanjem:

// Povezivanje s udaljenim poslužiteljem po imenu hosta, s četiri sekunde vremenskog ograničenja Socket connection = TimedSocket.getSocket ("server.my-network.net", 23, 4000); 

Ako sve bude u redu, vratit će se socket, baš kao i standardni java.net.Socketkonstruktori. Ako se veza ne može uspostaviti prije nego što nastupi vaše navedeno vremensko ograničenje, metoda će se zaustaviti i izbacit će java.io.InterruptedIOException, baš kao što bi to učinile i druge operacije čitanja utičnice kad je vremensko ograničenje određeno setSoTimeoutmetodom. Prilično lako, ha?

Inkapsuliranje višetrenog mrežnog koda u jednu klasu

Iako je TimedSocketpredavanje korisna komponenta samo po sebi, također je vrlo dobro pomagalo u učenju za razumijevanje kako se nositi s blokiranjem I / O-a. Kada se izvrši operacija blokiranja, aplikacija s jednim navojem blokirat će se na neodređeno vrijeme. Ako se koristi više niti izvršenja, međutim, samo jedna nit treba zaustaviti; druga nit se može nastaviti izvršavati. Pogledajmo kako TimedSocketrazred funkcionira.

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.

Sve ove metode pružaju kontrolirano sučelje za SocketThreadinstancu, ne dopuštajući vanjsku modifikaciju varijabli privatnih članova. Sljedeći primjer koda prikazuje run()metodu niti . Kada i ako konstruktor utičnice vrati a Socket, dodijelit će se varijabli privatnog člana, kojoj pristupne metode omogućuju pristup. Sljedeći put kada se putem SocketThread.isConnected()metode postavi upit o stanju veze , utičnica će biti dostupna za upotrebu. Ista se tehnika koristi za otkrivanje pogrešaka; ako java.io.IOExceptionje a uhvaćen, pohranit će se u privatnog člana, kojem se može pristupiti putem metoda isError()i getException()accessor.