Java sockets I/O: esto, esto ja asynkroninen

Kuva: Evi Radauscher Unsplash

I/O: ta kuvatessa termejä estottomuus ja asynkroninen käytetään usein keskenään, mutta niiden välillä on merkittävä ero. Tässä artikkelissa on kuvattu teoreettisia ja käytännön eroja esto-ja asynkroninen pistorasiat I / O toimintaa Java.

pistorasiat ovat päätepisteitä, joilla voidaan suorittaa kaksisuuntaista viestintää TCP-ja UDP-protokollilla. Java sockets-sovellusliittymät ovat adaptereita käyttöjärjestelmien vastaaville toiminnoille. Sockets communication POSIX-yhteensopivissa käyttöjärjestelmissä (Unix, Linux, Mac OS X, BSD, Solaris, AIX jne.) esittää Berkeley sockets. Sockets viestintä Windowsissa suoritetaan Winsock joka perustuu myös Berkeley pistorasiat lisätoimintoja noudattaa Windows-ohjelmointimalli.

tämän artikkelin POSIX-määritelmiä

käytetään yksinkertaistettuja määritelmiä POSIX-spesifikaatiosta.

estetty lanka — lanka, joka odottaa jonkin ehdon ennen kuin se voi jatkaa suoritusta.

esto — pistorasian ominaisuus, joka saa puhelut siihen odottamaan pyydetyn toiminnon suorittamista ennen palaamista.

estottomuus-pistorasian ominaisuus, joka saa siihen soitetut puhelut palaamaan viipymättä, kun havaitaan, että pyydettyä toimintoa ei voida suorittaa ilman tuntematonta viivettä.

synkroninen I/O — operaatio-I/O-operaatio, joka aiheuttaa pyytävän langan tukkeutumisen, kunnes kyseinen I/O-operaatio on valmis.

asynkroninen I/O — operaatio-I/O-operaatio, joka ei itsessään aiheuta pyytävän langan tukkeutumista; tämä tarkoittaa, että lanka ja I/O-operaatio voivat olla käynnissä samanaikaisesti.

joten POSIX-spesifikaation mukaan termien estoton ja asynkroninen ero on ilmeinen:

  • estottomuus-ominaisuus pistorasiassa, joka aiheuttaa sille soitetut puhelut palaamaan viipymättä
  • asynkroninen I/O — ominaisuus I/O — operaatiossa (lukeminen tai kirjoittaminen), joka toimii samanaikaisesti pyytävän langan kanssa

I/O-mallit

seuraavat I/O-mallit ovat yleisimpiä POSIX-yhteensopiville käyttöjärjestelmät:

  • esto I/O malli
  • esto I/O malli
  • I/O multiplexing malli
  • signaalikäyttöinen I/O malli
  • asynkroninen I/O malli

esto I/O malli

esto i/o-mallissa sovellus tekee Estojärjestelmäkutsun, kunnes tieto on vastaanotettu ytimeen ja kopioitu ytimen avaruudesta käyttäjäavaruuteen.

esto I / O malli

 esto I / O malli

plussat:

  • yksinkertaisin I / O-malli

Cons:

  • sovellus on estetty

Estoton I / O-malli

estottomassa I / O-mallissa sovellus tekee järjestelmäkutsun, joka palauttaa heti toisen kahdesta vastauksesta:

  • jos I/O-toiminto voidaan suorittaa välittömästi, tiedot palautetaan
  • jos I/O-toimintoa ei voida suorittaa välittömästi, palautetaan virhekoodi, joka ilmoittaa, että I/O-toiminto estyisi tai laite ei ole tilapäisesti käytettävissä

I/O-toiminnon suorittamiseksi, sovelluksen tulee odottaa (toistaa järjestelmäkutsuja) loppuun asti.

ei-estävä I / O malli

 estämätön I / O malli

plussat:

  • hakemusta ei ole estetty

miinuksia:

  • sovelluksen pitäisi olla varattu-odottaa valmistumista, joka aiheuttaisi monia käyttäjän ytimen kontekstikytkimiä
  • tämä malli voi tuoda I/O-latenssin, koska ytimen tietojen saatavuuden ja sovelluksen tietojen lukemisen välillä voi olla aukko

i/o multiplexing model

i/o multiplexing model

I/O multiplexing model (tunnetaan myös nimellä ei-esto i / o-malli esto-ilmoituksilla), sovellus tekee esto select-järjestelmäkutsun aloittaakseen toiminnan seurannan monilla kuvaajilla. Kunkin asiasanan osalta on mahdollista pyytää ilmoitusta sen valmiudesta tiettyihin I / O-operaatioihin (yhteys, lukeminen tai kirjoittaminen, Virheilmoitus jne.). Kun select – järjestelmäkutsu palauttaa, että ainakin yksi asiasana on valmis, sovellus tekee estottoman puhelun ja kopioi tiedot ytimen avaruudesta käyttäjäavaruuteen.

I / O multiplexing model

 I / O multiplexing model

plussat:

  • on mahdollista suorittaa I / O-operaatioita useilla kuvaajilla yhdellä säie

Cons:

  • sovellus on edelleen estetty select-järjestelmäkutsussa
  • kaikki käyttöjärjestelmät eivät tue tätä mallia tehokkaasti

Signaalikäyttöisessä I/O-mallissa

signaalikäyttöisessä I/O-mallissa sovellus soittaa estottoman puhelun ja rekisteröi signaalinkäsittelijän. Kun asiasana on valmis I / O-operaatioon, sovellukseen luodaan signaali. Tämän jälkeen signaalinkäsittelijä kopioi tiedot ytimen avaruudesta käyttäjäavaruuteen.

signaalikäyttöinen I / O-malli

signaalikäyttöinen I/O-malli

plussat:

  • sovellus ei ole estetty
  • signaalit voivat tarjota hyvän suorituskyvyn

miinukset:

  • kaikki käyttöjärjestelmät eivät tue signaaleja

asynkronisessa I/O-mallissa

asynkronisessa I/O-mallissa (tunnetaan myös nimellä päällekkäinen I/O-malli) sovellus tekee estottoman puhelun ja aloittaa taustaoperaation ytimessä. Kun toiminto on valmis (tieto vastaanotetaan ytimeen ja kopioidaan ytimen avaruudesta käyttäjäavaruuteen), luodaan täydennyskutsu I/O-toiminnon lopettamiseksi.

ero asynkronisen I/O-mallin ja signaalikäyttöisen I/O-mallin välillä on se, että signaalikäyttöisellä I/O-mallilla ydin kertoo sovellukselle, milloin I/O-operaatio voidaan aloittaa, mutta asynkronisella I/O-mallilla ydin kertoo sovellukselle, milloin I/O-operaatio on suoritettu.

asynkroninen I / O-malli

asynkroninen I/O-malli

plussat:

  • sovellus ei ole estetty
  • tämä malli voi tarjota parhaan suorituskyvyn

miinukset:

  • monimutkaisin I/O-malli toteuttaa
  • kaikki käyttöjärjestelmät eivät tue tätä mallia tehokkaasti

Java I / O API

Java IO API perustuu virtoihin (InputStream, OutputStream), jotka edustavat estävää, yksisuuntaista datavirtaa.

Java NIO API

Java NIO API perustuu kanava -, Puskuri -, Valintaluokkiin, jotka ovat adaptereita käyttöjärjestelmien matalan tason I/O-operaatioihin.

Kanavaluokka edustaa yhteyttä yksikköön (laite, tiedosto, pistorasia, ohjelmistokomponentti jne.), joka kykenee suorittamaan I/O-operaatioita (lukeminen tai kirjoittaminen).

yksisuuntaisiin virtoihin verrattuna kanavat ovat kaksisuuntaisia.

Puskuriluokka on kiinteäkoon datasäiliö, jossa on muita menetelmiä tietojen lukemiseen ja kirjoittamiseen. Kaikki Kanavatiedot käsitellään puskurin kautta, mutta ei koskaan suoraan: kaikki kanavalle lähetetyt tiedot kirjoitetaan puskuriin, kaikki kanavalta vastaanotetut tiedot luetaan puskuriin.

verrattuna virtoihin, jotka ovat tavupainotteisia, kanavat ovat lohkopainotteisia. Tavu-orientoitunut I/O on yksinkertaisempi, mutta joillekin I / O-entiteeteille se voi olla melko hidas. Lohkokeskeinen I / O voi olla paljon nopeampi, mutta on monimutkaisempi.

Valintaluokka mahdollistaa useiden rekisteröityjen Valintakanavaobjektien tapahtumien tilaamisen yhdellä puhelulla. Kun tapahtumat saapuvat, valitsinobjekti lähettää ne vastaaville tapahtumankäsittelijöille.

Java NIO2 API

Java Nio2 API perustuu asynkronisiin kanaviin (AsynchronousServerSocketChannel, AsynchronousSocketChannel jne.), jotka tukevat asynkronisia I/O-operaatioita (liittäminen, lukeminen tai kirjoittaminen, virheiden käsittely).

asynkronisissa kanavissa on kaksi mekanismia, joilla voidaan ohjata asynkronisia I/O-operaatioita. Ensimmäinen mekanismi on palauttamalla java.util.samanaikainen.Future object, joka mallintaa vireillä olevaa operaatiota ja jota voidaan käyttää valtion tiedusteluun ja tuloksen saamiseen. Toinen mekanismi on siirtämällä operaatioon Jaava.nio.kanava.CompletionHandler object, joka määrittelee käsittelymenetelmät, jotka suoritetaan operaation päätyttyä tai epäonnistuttua. Molempien mekanismien tarjoama API on vastaava.

asynkroniset kanavat tarjoavat tavanomaisen tavan suorittaa asynkronisia operaatioita alustariippumattomasti. Kuitenkin, määrä, että Java sockets API voi hyödyntää natiivi asynkronisia ominaisuuksia käyttöjärjestelmän, riippuu tuesta kyseiselle alustalle.

Socket echo server

suurin osa edellä mainituista I/O-malleista on toteutettu täällä echo-palvelimissa ja Java sockets-Sovellusliittymillä varustetuissa asiakkaissa. Echon palvelimet ja asiakkaat toimivat seuraavan algoritmin mukaan:

  1. palvelin kuuntelee pistorasiaa rekisteröidyssä TCP-portissa 7000
  2. asiakas yhdistää dynaamisen TCP-portin pistorasiasta palvelinpistorasiaan
  3. asiakas lukee tulomerkkijonon konsolista ja lähettää tavut pistorasiastaan palvelinpistorasiaan
  4. palvelin vastaanottaa tavut pistorasiastaan ja lähettää ne takaisin asiakaspistorasiaan
  5. asiakas saa tavut pistorasiastaan ja kirjoittaa kaikumuotoisen merkkijonon konsoliin
  6. kun asiakas saa saman määrän tavuja kuin on lähettänyt, se irtoaa palvelimesta
  7. kun palvelin saa erikoismerkkijonon, se lakkaa kuuntelemasta

tässä merkkijonojen ja tavujen välinen muunnos suoritetaan eksplisiittisesti UTF-8-koodauksella.

lisäksi echo-palvelimille annetaan vain yksinkertaistettuja koodeja. Linkki echo-palvelimien ja-asiakkaiden täydellisiin koodeihin on esitetty johtopäätöksessä.

Io echo-palvelimen estäminen

seuraavassa esimerkissä esto I/O-malli on toteutettu echo-palvelimessa Java IO API: lla.

ServerSocket.hyväksy menetelmälohkot, kunnes yhteys hyväksytään. Sisäänvirtaus.Lue menetelmälohkoja, kunnes syöttötiedot ovat saatavilla tai asiakas on katkaistu. Ulosvirtaus.Kirjoita menetelmälohkoja, kunnes kaikki lähtötiedot on kirjoitettu.

public class IoEchoServer { public static void main(String args) throws IOException {
ServerSocket serverSocket = new ServerSocket(7000); while (active) {
Socket socket = serverSocket.accept(); // blocking InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream(); int read;
byte bytes = new byte;
while ((read = is.read(bytes)) != -1) { // blocking
os.write(bytes, 0, read); // blocking
} socket.close();
} serverSocket.close();
}
}

esto NIO echo server

seuraavassa esimerkissä esto I/O-malli on toteutettu echo-palvelimessa Java NIO API: lla.

ServerSocketChannel-ja SocketChannel-objektit on oletusarvoisesti määritetty estotilassa. ServerSocketChannel.hyväksy menetelmä estää ja palauttaa SocketChannel objektin, kun yhteys on hyväksytty. ServerSocket.Lue menetelmälohkoja, kunnes syöttötiedot ovat saatavilla tai asiakas on katkaistu. ServerSocket.Kirjoita menetelmälohkoja, kunnes kaikki lähtötiedot on kirjoitettu.

public class NioBlockingEchoServer { public static void main(String args) throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress("localhost", 7000)); while (active) {
SocketChannel socketChannel = serverSocketChannel.accept(); // blocking ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true) {
buffer.clear();
int read = socketChannel.read(buffer); // blocking
if (read < 0) {
break;
} buffer.flip();
socketChannel.write(buffer); // blocking
} socketChannel.close();
} serverSocketChannel.close();
}
}

Estoton NIO echo server

seuraavassa esimerkissä Estoton I/O-malli on toteutettu echo-palvelimessa Java NIO API: lla.

ServerSocketChannel-ja SocketChannel-objektit on määritetty nimenomaisesti estottomassa tilassa. ServerSocketChannel.hyväksy menetelmä ei estä ja palauttaa null jos yhteyttä ei ole hyväksytty vielä tai SocketChannel objekti muuten. ServerSocket.read ei estä ja palauttaa arvon 0, jos Tietoja ei ole saatavilla tai positiivinen määrä tavuja lukee toisin. ServerSocket.kirjoitustapa ei estä, jos pistorasian ulostulopuskurissa on vapaata tilaa.

public class NioNonBlockingEchoServer { public static void main(String args) throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(7000)); while (active) {
SocketChannel socketChannel = serverSocketChannel.accept(); // non-blocking
if (socketChannel != null) {
socketChannel.configureBlocking(false); ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true) {
buffer.clear();
int read = socketChannel.read(buffer); // non-blocking
if (read < 0) {
break;
} buffer.flip();
socketChannel.write(buffer); // can be non-blocking
} socketChannel.close();
}
} serverSocketChannel.close();
}
}

Multiplexing NIO echo server

seuraavassa esimerkissä multiplexing I/O-malli on toteutettu echo server Java NIO API: ssa.

alustuksen aikana useita serversocketchannel-objekteja, jotka on määritetty estottomassa tilassa, rekisteröidään samaan valitsinobjektiin Selectionkeyn kanssa.Op_accept-argumentti määrittää, että yhteyden hyväksymistapahtuma on mielenkiintoinen.

pääsilmukassa valitsin.valitse menetelmälohkot, kunnes vähintään yksi rekisteröidyistä tapahtumista tapahtuu. Sitten valitsin.selectedKeys-menetelmä palauttaa joukon selectionkey-objekteja, joille tapahtumat ovat tapahtuneet. Iteroimalla kautta SelectionKey objects, on mahdollista määrittää, mitä I / O tapahtuma (yhteyden, hyväksyä, lukea, kirjoittaa) on tapahtunut ja mitkä pistorasiat objektit (ServerSocketChannel, SocketChannel) on liitetty kyseiseen tapahtumaan.

valintanäppäimen ilmoittaminen siitä, että kanava on valmis johonkin toimintaan, on vihje, ei takuu.

public class NioMultiplexingEchoServer { public static void main(String args) throws IOException {
final int ports = 8;
ServerSocketChannel serverSocketChannels = new ServerSocketChannel; Selector selector = Selector.open(); for (int p = 0; p < ports; p++) {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannels = serverSocketChannel;
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress("localhost", 7000 + p)); serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
} while (active) {
selector.select(); // blocking Iterator<SelectionKey> keysIterator = selector.selectedKeys().iterator();
while (keysIterator.hasNext()) {
SelectionKey key = keysIterator.next(); if (key.isAcceptable()) {
accept(selector, key);
}
if (key.isReadable()) {
keysIterator.remove();
read(selector, key);
}
if (key.isWritable()) {
keysIterator.remove();
write(key);
}
}
} for (ServerSocketChannel serverSocketChannel : serverSocketChannels) {
serverSocketChannel.close();
}
}
}

kun SelectionKey-objekti ilmoittaa, että yhteyden hyväksymistapahtuma on tapahtunut, se on tehnyt ServerSocketChannel.hyväksy puhelu (joka voi olla ei-esto) hyväksyä yhteyden. Tämän jälkeen uusi SocketChannel-objekti asetetaan estottomaan tilaan ja se rekisteröidään samaan valitsinobjektiin Selectionkeyn kanssa.OP_READ-argumentti täsmentää, että nyt lukemistapahtuma on mielenkiintoinen.

private static void accept(Selector selector, SelectionKey key) throws IOException {
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = serverSocketChannel.accept(); // can be non-blocking
if (socketChannel != null) {
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
}
}

kun SelectionKey-objekti ilmoittaa, että lukutapahtuma on tapahtunut, siitä tehdään sukkanauha.read call (joka voi olla ei-esto) lukea tietoja SocketChannel-objektista uuteen bytebyffer-objektiin. Tämän jälkeen SocketChannel-objekti rekisteröidään samaan Valitsinobjektiin Valitsinavaimen kanssa.Op_write argumentti määrittää, että nyt tapahtuma kirjoittaa on mielenkiintoinen. Lisäksi tätä ByteBuffer-objektia käytetään rekisteröinnin aikana liitetiedostona.

private static void read(Selector selector, SelectionKey key) throws IOException {
SocketChannel socketChannel = (SocketChannel) key.channel(); ByteBuffer buffer = ByteBuffer.allocate(1024);
socketChannel.read(buffer); // can be non-blocking buffer.flip();
socketChannel.register(selector, SelectionKey.OP_WRITE, buffer);
}

kun SelectionKeys-objekti ilmoittaa kirjoitustapahtuman tapahtuneen, se on tehnyt sukkanauhan.Kirjoita puhelu (joka voi olla ei-esto) kirjoittaa tietoja SocketChannel objekti bytebyffer objekti, uutettu SelectionKey.kiinnitysmenetelmä. Sen jälkeen Sukkakanava.cloase puhelu sulkee yhteyden.

private static void write(SelectionKey key) throws IOException {
SocketChannel socketChannel = (SocketChannel) key.channel(); ByteBuffer buffer = (ByteBuffer) key.attachment(); socketChannel.write(buffer); // can be non-blocking
socketChannel.close();
}

jokaisen lukemisen tai kirjoittamisen jälkeen SelectionKey-objekti poistetaan selectionkey-objektien joukosta sen uudelleenkäytön estämiseksi. Mutta selectionkey objekti yhteyden hyväksymistä ei poisteta on kyky tehdä seuraavan vastaavan toiminnon.

asynkroninen NIO2 echo-palvelin

seuraavassa esimerkissä asynkroninen I/O-malli on toteutettu echo-palvelimessa Java NIO2 API: lla. Asynchronousserversocketchannel, AsynchronousSocketChannel luokat täällä käytetään loppuun käsittelijät mekanismi.

Asynkroninen Kääntökanava.accept-menetelmä käynnistää asynkronisen yhteyden hyväksymisoperaation.

public class Nio2CompletionHandlerEchoServer { public static void main(String args) throws IOException {
AsynchronousServerSocketChannel serverSocketChannel = AsynchronousServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(7000)); AcceptCompletionHandler acceptCompletionHandler = new AcceptCompletionHandler(serverSocketChannel);
serverSocketChannel.accept(null, acceptCompletionHandler); System.in.read();
}
}

kun yhteys hyväksytään (tai operaatio epäonnistuu), kutsutaan AcceptCompletionHandler-luokkaa, jota asynkroninen Lukkokanava kutsuu.read (ByteBuffer destination, a attachment, CompletionHandler<Integer,? super a> handler) menetelmä käynnistää asynkronisen lukuoperaation asynkronisesta Lukuoperaatiosta uuteen ByteBuffer-objektiin.

class AcceptCompletionHandler implements CompletionHandler<AsynchronousSocketChannel, Void> { private final AsynchronousServerSocketChannel serverSocketChannel; AcceptCompletionHandler(AsynchronousServerSocketChannel serverSocketChannel) {
this.serverSocketChannel = serverSocketChannel;
} @Override
public void completed(AsynchronousSocketChannel socketChannel, Void attachment) {
serverSocketChannel.accept(null, this); // non-blocking ByteBuffer buffer = ByteBuffer.allocate(1024);
ReadCompletionHandler readCompletionHandler = new ReadCompletionHandler(socketChannel, buffer);
socketChannel.read(buffer, null, readCompletionHandler); // non-blocking
} @Override
public void failed(Throwable t, Void attachment) {
// exception handling
}
}

kun lukuoperaatio päättyy (tai epäonnistuu), kutsutaan Lukuoperaatioksi ReadCompletionHandler-luokkaa, jota asynkroninen Lukuoperaatio käyttää.write (ByteBuffer source, a attachment, CompletionHandler<Integer,? super a> handler) – menetelmä käynnistää ByteBuffer-objektista asynkronisen kirjoitusoperaation Asynchronoussocketchannel-objektille.

class ReadCompletionHandler implements CompletionHandler<Integer, Void> { private final AsynchronousSocketChannel socketChannel;
private final ByteBuffer buffer; ReadCompletionHandler(AsynchronousSocketChannel socketChannel, ByteBuffer buffer) {
this.socketChannel = socketChannel;
this.buffer = buffer;
} @Override
public void completed(Integer bytesRead, Void attachment) {
WriteCompletionHandler writeCompletionHandler = new WriteCompletionHandler(socketChannel);
buffer.flip();
socketChannel.write(buffer, null, writeCompletionHandler); // non-blocking
} @Override
public void failed(Throwable t, Void attachment) {
// exception handling
}
}

kirjoitusoperaation päättyessä (tai epäonnistuessa) kutsutaan WriteCompletionHandler-luokkaa, jota asynkroninen Lukukanava kutsuu.sulje menetelmä sulkee yhteyden.

class WriteCompletionHandler implements CompletionHandler<Integer, Void> { private final AsynchronousSocketChannel socketChannel; WriteCompletionHandler(AsynchronousSocketChannel socketChannel) {
this.socketChannel = socketChannel;
} @Override
public void completed(Integer bytesWritten, Void attachment) {
try {
socketChannel.close();
} catch (IOException e) {
// exception handling
}
} @Override
public void failed(Throwable t, Void attachment) {
// exception handling
}
}

tässä esimerkissä asynkroniset I/O-operaatiot suoritetaan ilman kiinnitystä, koska kaikki tarvittavat objektit (Asynchronoussocketchannel, ByteBuffer) välitetään rakentajan argumentteina asianmukaisille viimeistelyn käsittelijöille.

johtopäätös

pistorasioiden tietoliikenteen I/O-mallin valinta riippuu liikenteen parametreista. Jos I / O-pyynnöt ovat pitkiä ja harvinaisia, asynkroninen I/O on yleensä hyvä valinta. Jos I / O-pyynnöt ovat kuitenkin lyhyitä ja nopeita, kernel-kutsujen käsittelyn ylimeno voi tehdä synkronisesta I/O: sta paljon paremman.

huolimatta siitä, että Java tarjoaa standarditavan sockets I/O: n esittämiseen eri käyttöjärjestelmissä, todellinen suorituskyky voi vaihdella merkittävästi niiden toteutuksesta riippuen. Näitä eroja on mahdollista alkaa tutkia Dan Kegelin tunnetulla artikkelilla the C10K problem.

Täydelliset koodiesimerkit ovat saatavilla GitHub-arkistossa.

Vastaa

Sähköpostiosoitettasi ei julkaista.