Java sockets I / O: blokkering, ikke-blokkering og asynkron

Foto Av Evi Radauscher På Unsplash

når man beskriver I/O, brukes begrepene ikke-blokkerende og asynkrone ofte om hverandre, men det er en betydelig forskjell mellom dem. I denne artikkelen beskrives de teoretiske og praktiske forskjellene mellom ikke-blokkerende og asynkrone sockets i/O-operasjoner I Java.

Sockets er endepunkter for å utføre toveiskommunikasjon VIA tcp-og UDP-protokoller. Java sockets Apier er adaptere for den tilsvarende funksjonaliteten til operativsystemene. Sockets kommunikasjon I POSIX-kompatible operativsystemer (Unix, Linux, Mac OS X, BSD, Solaris, AIX, etc.) utføres Av Berkeley sockets. Sockets kommunikasjon I Windows utføres Av Winsock som også er basert På Berkeley sockets med ekstra funksjonalitet for å overholde Windows-programmeringsmodellen.

POSIX-definisjonene

i denne artikkelen brukes forenklede definisjoner fra POSIX-spesifikasjonen.

Blokkert tråd – en tråd som venter på en tilstand før den kan fortsette utførelsen.

Blokkering-en egenskap for en stikkontakt som fører til at samtaler til den venter på at den forespurte handlingen skal utføres før du returnerer.

Ikke-blokkering-en egenskap av en stikkontakt som fører til at samtaler til å returnere uten forsinkelse, når det oppdages at den forespurte handlingen ikke kan fullføres uten en ukjent forsinkelse.

Synkron i/U — operasjon-En I / U-operasjon som gjør at den forespurte tråden blokkeres til den I / u-operasjonen er fullført.

Asynkron i/U-operasjon-en i / U-operasjon som ikke i seg selv forårsaker at den forespurte tråden blokkeres. dette innebærer at tråden og i / u-operasjonen kan kjøre samtidig.

Så, i HENHOLD TIL POSIX-spesifikasjonen, er forskjellen mellom vilkårene ikke-blokkerende og asynkron åpenbar:

  • ikke-blokkerende — en egenskap for en stikkontakt som fører til at anrop til den kommer tilbake uten forsinkelse
  • asynkron i/O — a-egenskap på en I/o-operasjon (lesing eller skriving) som kjører samtidig med den forespurte tråden

i/o-modeller

FØLGENDE i/O-modeller er de vanligste FOR POSIX-kompatible operativsystemer:

  • blokkerende i/o-modell
  • ikke-blokkerende i/O-modell
  • signaldrevet i/O-modell
  • asynkron i/O-modell

Blokkerende i/O-modell

i den blokkerende i/o-modellen gjør programmet Et Blokkeringssystemkall til data mottas i kjernen Og kopieres Fra kjerneplass til brukerplass.

blokkere i / O-modell

 blokkere i / O-modell

Pros:

  • den enkleste i / o-modellen å implementere

Ulemper:

  • programmet er blokkert

Ikke-blokkerende i / O-modell

i den ikke-blokkerende i/O-modellen gjør programmet et systemkall som umiddelbart returnerer ett av to svar:

  • hvis I/O-operasjonen kan fullføres umiddelbart, returneres dataene
  • hvis i/O-operasjonen ikke kan fullføres umiddelbart, returneres en feilkode som indikerer at i/O-operasjonen vil blokkere eller enheten er midlertidig utilgjengelig

for å fullføre i / O-operasjonen, bør programmet vente opptatt (gjenta systemanrop) til ferdigstillelse.

ikke-blokkerende i/O-modell

ikke-blokkerende i / O-modell

Pros:

  • søknaden er ikke blokkert

Ulemper:

  • programmet bør opptatt-vente til ferdigstillelse, som ville føre til mange bruker-kernel kontekst brytere
  • denne modellen kan innføre I/O latens fordi det kan være et gap mellom datatilgjengelighet i kjernen og data lesing av programmet

i/o multipleksing modell

i i/o multipleksing modell (også kjent som ikke-blokkerende i/o-modell med blokkerende varsler), gjør programmet En Blokkering velg systemanrop for å begynne å overvåke aktivitet på mange beskrivere. For hver beskrivelse er det mulig å be om varsel om beredskap for visse i / O-operasjoner (tilkobling, lesing eller skriving, feilforekomst, etc.). Når select system call returnerer at minst en deskriptor er klar, gjør programmet et ikke-blokkerende anrop og kopierer dataene fra kjerneplass til brukerplass.

i / o multipleksing modell

 i / o multipleksing modell

Pros:

  • Det er mulig å utføre i / O-operasjoner på flere beskrivere i en tråd

Ulemper:

  • programmet er fortsatt blokkert på select system call
  • Ikke alle operativsystemer støtter denne modellen effektivt

Signaldrevet i/O-modell

i den signaldrevne i/O-modellen foretar programmet et ikke-blokkerende anrop og registrerer en signalbehandler. Når en deskriptor er klar for en I/O-operasjon, genereres et signal for applikasjonen. Deretter kopierer signalbehandleren dataene fra kjerneplassen til brukerrommet.

signaldrevet i / O-modell

 signaldrevet i / O-modell

Pros:

  • søknaden er ikke blokkert
  • Signaler kan gi god ytelse

Ulemper:

  • Ikke alle operativsystemer støtter signaler

Asynkron i/O-modell

i den asynkrone i/O-modellen (også kjent som den overlappede i/O-modellen) gjør programmet det ikke-blokkerende anropet og starter en bakgrunnsoperasjon i kjernen. Når operasjonen er fullført (data mottas i kjernen og kopieres fra kjerneplass til brukerplass), genereres en tilbakeringing for å fullføre i / U-operasjonen.

en forskjell mellom den asynkrone i/O-modellen og den signaldrevne i/O-modellen er at med signaldrevet i / O forteller kjernen applikasjonen når en i / O-operasjon kan startes, men med den asynkrone i/O-modellen forteller kjernen applikasjonen når en i / O-operasjon er fullført.

asynkron i / O-modell

 asynkron i / O-modell

Pros:

  • søknaden er ikke blokkert
  • denne modellen kan gi den beste ytelsen

Ulemper:

  • den mest kompliserte i/O-modellen å implementere
  • Ikke alle operativsystemer støtter denne modellen effektivt

Java I/O-Apier

Java IO API er basert på strømmer (InputStream, OutputStream) som representerer blokkering, en-retnings datastrøm.

Java NIO API

Java Nio API er basert På Kanal -, Buffer -, Velgerklasser, som er adaptere til lavt nivå i / O-operasjoner av operativsystemer.

Kanalklassen representerer en tilkobling til en enhet (maskinvareenhet, fil, socket, programvarekomponent, etc) som er i stand til å utføre i/O-operasjoner (lese eller skrive).

i sammenligning med enveisstrømmer er kanalene toveis.

Bufferklassen er en databeholder med fast størrelse med flere metoder for å lese og skrive data. Alle Kanaldata håndteres Via Buffer, men aldri direkte: alle data som sendes Til En Kanal skrives inn i En Buffer, alle data som mottas fra En Kanal leses inn i En Buffer.

i sammenligning med bekker, som er byte-orientert, kanaler er blokk-orientert. Byte-orientert i / O er enklere, men for noen i / O-enheter kan være ganske treg. Blokkorientert I / O kan være mye raskere, men er mer komplisert.

Selector-klassen tillater å abonnere på hendelser fra mange registrerte SelectableChannel-objekter i en enkelt samtale. Når hendelser kommer, sender Et Velgerobjekt dem til de tilsvarende hendelsesbehandlerne.

Java NIO2 API

Java NIO2 API er basert på asynkrone kanaler (AsynchronousServerSocketChannel, AsynchronousSocketChannel, etc) som støtter asynkrone i/O-operasjoner (tilkobling, lesing eller skriving, feilhåndtering).

de asynkrone kanalene har to mekanismer for å kontrollere asynkrone i / U-operasjoner. Den første mekanismen er ved å returnere en java.util.samtidige.Fremtidig objekt, som modellerer en ventende operasjon og kan brukes til å spørre staten og få resultatet. Den andre mekanismen er ved å sende til operasjonen en java.nio.kanal.CompletionHandler-objekt, som definerer behandlingsmetoder som utføres etter at operasjonen er fullført eller mislyktes. Den angitte API for begge mekanismer er ekvivalente.

Asynkrone kanaler gir en standard måte å utføre asynkron drift plattform-uavhengig. Beløpet Som Java sockets API kan utnytte innfødte asynkrone evner i et operativsystem, vil imidlertid avhenge av støtten til den plattformen.

Socket echo server

De Fleste i / O-modellene nevnt ovenfor er implementert her i echo-servere og klienter med Java sockets Apier. Echo-serverne og klientene fungerer ved hjelp av følgende algoritme:

  1. en server lytter til en kontakt på en registrert tcp-port 7000
  2. en klient kobler fra en kontakt på en dynamisk tcp-port til serverkontakten
  3. klienten leser en inndatastreng fra konsollen og sender byte fra kontakten til serverkontakten
  4. serveren mottar byte fra kontakten og sender dem tilbake til klientkontakten
  5. klienten mottar byte fra sin socket og skriver ekko strengen på konsollen
  6. når klienten mottar samme antall byte som den har sendt, KOBLER den fra serveren
  7. når serveren mottar en spesiell streng, slutter den å lytte

konverteringen mellom strenger og byte her utføres eksplisitt i utf-8-koding.

Videre bare forenklede koder for echo servere er gitt. Koblingen til de komplette kodene for echo-servere og klienter er gitt i konklusjonen.

Blokkering AV IO echo server

i følgende eksempel implementeres den blokkerende i/O-modellen i en echo-server med Java IO API.

ServerSocket.godta metodeblokker til en tilkobling er akseptert. InputStream.les metodeblokker til inndataene er tilgjengelige, eller en klient er frakoblet. OutputStream.skriv metodeblokker til alle utdataene er skrevet.

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();
}
}

Blokkering AV nio echo server

i følgende eksempel implementeres den blokkerende i/O-modellen i en echo-server med Java NIO API.

ServerSocketChannel-og SocketChannel-objektene er som standard konfigurert i blokkeringsmodus. ServerSocketChannel.godta metodeblokker og returnerer Et SocketChannel-objekt nar en tilkobling er akseptert. ServerSocket.les metodeblokker til inndataene er tilgjengelige, eller en klient er frakoblet. ServerSocket.skriv metodeblokker til alle utdataene er skrevet.

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();
}
}

ikke-blokkerende nio echo server

i følgende eksempel implementeres den ikke-blokkerende i/O-modellen i en echo-server med Java NIO API.

ServerSocketChannel-og SocketChannel-objektene er eksplisitt konfigurert i ikke-blokkeringsmodus. ServerSocketChannel.godta-metoden blokkerer ikke og returnerer null hvis ingen tilkobling er godtatt ennå eller et SocketChannel-objekt ellers. ServerSocket.les blokkerer ikke og returnerer 0 hvis ingen data er tilgjengelige eller et positivt antall byte leses ellers. ServerSocket.skrivemetode blokkerer ikke hvis det er ledig plass i stikkontaktens utgangsbuffer.

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();
}
}

Multipleksing nio echo server

i følgende eksempel er multipleksing i/O-modellen implementert i En echo server Java NIO API.

under initialiseringen registreres flere ServerSocketChannel-objekter, som er konfigurert i ikke-blokkeringsmodus, på samme Velgerobjekt med SelectionKey.Op_accept argument for å angi at en hendelse av tilkobling aksept er interessant.

I hovedsløyfen, Velgeren.velg metodeblokker til minst en av de registrerte hendelsene oppstår. Deretter Velger.selectedKeys-metoden returnerer et sett Av SelectionKey-objektene som det har oppstått hendelser for. Iterating Gjennom SelectionKey objekter, er det mulig å finne ut hva i / o hendelse (koble, godta, lese, skrive) har skjedd og hvilke sockets objekter (ServerSocketChannel, SocketChannel) har vært knyttet til den hendelsen.

Indikasjon av en valgnøkkel at en kanal er klar for en operasjon er et hint, ikke en garanti.

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();
}
}
}

når Et SelectionKey-objekt angir at en tilkoblingsaksepthendelse har skjedd, blir Den Gjort Til ServerSocketChannel.godta anrop (som kan være en ikke-blokkering) for å godta tilkoblingen. Etter det, en ny Stikkontaktkanalobjekt er konfigurert i ikke-blokkerende modus og er registrert på samme Velgerobjekt Med SelectionKey.Op_read argument for å spesifisere at nå er en lesehendelse interessant.

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);
}
}

når Et SelectionKey-objekt indikerer at en lesehendelse har skjedd, er Den Laget Av SocketChannel.les samtalen (som kan v re en ikke-blokkering) for a lese data fra SocketChannel-objektet til et nytt ByteByffer-objekt. Deretter registreres SocketChannel-objektet på samme Velgerobjekt med SelectionKey.Op_write argument for å spesifisere at nå er en hendelse av skriving interessant. I tillegg brukes Dette ByteBuffer-objektet under registreringen som et vedlegg.

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);
}

når Et SelectionKeys-objekt indikerer at en skrivehendelse har skjedd, blir Den SocketChannel.skriv samtale (som kan være en ikke-blokkering) for å skrive data til SocketChannel-objektet fra ByteByffer-objektet, hentet fra SelectionKey.vedlegg metode. Etter Det, Stikkontaktenkanal.cloase-anropet lukker tilkoblingen.

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();
}

Etter hver lesing eller skriving Fjernes SelectionKey-objektet fra Settet Med selectionkey-objekter for å hindre gjenbruk. Men SelectionKey-objektet for tilkoblingsaksept fjernes ikke for å kunne gjøre neste lignende operasjon.

Asynkron nio2 echo server

i følgende eksempel implementeres den asynkrone i/O-modellen i en echo-server med Java NIO2 API. Asynchronousserversocketchannel, asynchronoussocketchannel klasser her brukes med ferdigstillelse handlers mekanisme.

AsynchronousServerSocketChannel.godta metoden starter en asynkron tilkobling aksept operasjon.

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();
}
}

Når en tilkobling er godtatt (eller operasjonen mislykkes), Kalles Klassen AcceptCompletionHandler, som Av AsynchronousSocketChannel.les (ByteBuffer destinasjon, et vedlegg, CompletionHandler<Heltall,? super a > behandler) – metoden starter en asynkron leseoperasjon fra asynchronoussocketchannel-objektet til et Nytt ByteBuffer-objekt.

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
}
}

Når leseoperasjonen er fullført (eller mislykkes), Kalles ReadCompletionHandler-klassen, som Av asynchronoussocketchannel.skriv (ByteBuffer kilde, et vedlegg, CompletionHandler< Heltall,? super a > behandler) – metoden starter en asynkron skriveoperasjon Til asynchronoussocketchannel-objektet fra ByteBuffer-objektet.

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
}
}

når skriveoperasjonen er fullført (eller mislykkes), Kalles klassen WriteCompletionHandler, som Av asynchronoussocketchannel.lukk metode lukker tilkoblingen.

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
}
}

i dette eksemplet utføres asynkrone i/U-operasjoner uten vedlegg, fordi alle nødvendige objekter (Asynchronoussocketchannel, ByteBuffer) sendes som konstruktørargumenter for de riktige fullføringsbehandlerne.

Konklusjon

valget av i/O-modellen for stikkontakter er avhengig av trafikkparametrene. Hvis i / O-forespørsler er lange og sjeldne, er asynkron I/O generelt et godt valg. Men hvis i / O-forespørsler er korte og raske, kan overhead for behandling av kjernekall gjøre synkron I/O mye bedre.

Til Tross For At Java gir en standard måte å utføre sockets I / O i de forskjellige operativsystemene, kan den faktiske ytelsen variere betydelig avhengig av implementeringen. Det er mulig å begynne å studere disse forskjellene Med Dan Kegels kjente artikkel THE C10K problem.

Komplette kodeeksempler er tilgjengelige i GitHub-depotet.

Legg igjen en kommentar

Din e-postadresse vil ikke bli publisert.