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

foto af Evi Radauscher på Unsplash

når man beskriver I/O, bruges udtrykkene ikke-blokerende og asynkrone ofte om hverandre, men der er en betydelig forskel mellem dem. I denne artikel beskrives de teoretiske og praktiske forskelle mellem ikke-blokerende og asynkrone stikkontakter I/O-operationer i Java.

stikkontakter er slutpunkter til at udføre tovejskommunikation via TCP-og UDP-protokoller. Java sockets API ‘ er er adaptere til den tilsvarende funktionalitet i operativsystemerne. Sockets kommunikation i styresystemer, der er i overensstemmelse med Posis (f.eks.) udføres af Berkeley sockets. Sockets kommunikation i vinduer udføres af Berkeley sockets, der også er baseret på Berkeley sockets med ekstra funktionalitet for at overholde Programmeringsmodellen.

Posis definitionerne

i denne artikel anvendes forenklede definitioner fra Posis specifikationen.

blokeret tråd — en tråd, der venter på en tilstand, før den kan fortsætte udførelsen.

blokering — en egenskab ved en stikkontakt, der får opkald til den til at vente på, at den ønskede handling udføres, før den vender tilbage.

ikke-blokering — en egenskab ved en stikkontakt, der får opkald til den til at vende tilbage uden forsinkelse, når det opdages, at den anmodede handling ikke kan gennemføres uden en ukendt forsinkelse.

synkron I/O — operation-en I/O-operation, der får den anmodende tråd til at blive blokeret, indtil den I/O-operation er afsluttet.

asynkron I/O — operation-en I/O-operation, der ikke i sig selv forårsager, at den anmodende tråd blokeres; dette indebærer, at tråden og I/O-operationen muligvis kører samtidigt.

så ifølge POSIKS-specifikationen er forskellen mellem udtrykkene ikke-blokerende og asynkron åbenlys:

  • ikke-blokerende — en egenskab ved en stikkontakt, der får opkald til den til at vende tilbage uden forsinkelse
  • asynkron I/O — en egenskab ved en I/O-operation (læsning eller skrivning), der kører samtidigt med den anmodende tråd

I/O-modeller

følgende I/O-modeller er de mest almindelige for den position-kompatible operativsystemer:

  • blokerende I/O-model
  • ikke-blokerende I/O-model
  • I/O-multipleksemodel
  • signaldrevet I/O-model
  • asynkron I/O-model

blokerende I/O-model

i blokerende I/O-modellen foretager applikationen et Blokeringssystemopkald, indtil data modtages i kernen og kopieres fra kernelrum til brugerrum.

blokerende I / O-model

 blokerende I / O-model

fordele:

  • den enkleste I / O-model til implementering

ulemper:

  • applikationen er blokeret

ikke-blokerende I / O-model

i den ikke-blokerende I/O-model foretager applikationen et systemopkald, der straks returnerer et af to svar:

  • hvis I/O-operationen kan afsluttes med det samme, returneres dataene
  • hvis I/O-operationen ikke kan afsluttes med det samme, returneres en fejlkode, der angiver, at I/O-operationen ville blokere, eller enheden er midlertidigt utilgængelig

for at afslutte I/O-operationen skal applikationen travlt vente (foretage gentagne systemopkald) indtil afslutningen.

ikke-blokerende I / O-model

ikke-blokerende I / O-model

fordele:

  • applikationen er ikke blokeret

ulemper:

  • applikationen skal travlt-vente til færdiggørelse, hvilket ville medføre mange bruger-kerne kontekstkontakter
  • denne model kan introducere i/O-latenstid, fordi der kan være et mellemrum mellem datatilgængeligheden i kernen og dataaflæsningen af applikationen

I/O-multipleksemodel

i I/O-multipleksemodellen (også kendt som ikke-standard-modellen), der er-blokering i/O-model med blokerende meddelelser), applikationen foretager et blokerende systemopkald for at begynde at overvåge aktivitet på mange deskriptorer. For hver deskriptor er det muligt at anmode om meddelelse om dets beredskab til visse I/O-operationer (forbindelse, læsning eller skrivning, fejlforekomst osv.). Når select system call returnerer, at mindst en deskriptor er klar, foretager applikationen et ikke-blokerende opkald og kopierer dataene fra kernelrum til brugerrum.

i / O multipleksmodel

I / O multipleksmodel

fordele:

  • det er muligt at udføre I / O-operationer på flere deskriptorer i en tråd

ulemper:

  • applikationen er stadig blokeret på select system call
  • ikke alle operativsystemer understøtter denne model effektivt

Signaldrevet I/O-model

i den signaldrevne I/O-model foretager applikationen et ikke-blokerende opkald og registrerer en signalhåndterer. Når en deskriptor er klar til en I/O-operation, genereres et signal til applikationen. Derefter kopierer signalhåndtereren dataene fra kernelrummet til brugerrummet.

signaldrevet I / O-model

 signaldrevet I / O-model

fordele:

  • programmet er ikke blokeret
  • signaler kan give god ydeevne

ulemper:

  • ikke alle operativsystemer understøtter signaler

asynkron I/O-model

i den asynkrone I/O-model (også kendt som den overlappede i/O-model) applikationen foretager det ikke-blokerende opkald og starter en baggrundsoperation i kernen. Når handlingen er afsluttet (data modtages i kernen og kopieres fra kernelrum til brugerrum), genereres en tilbagekald af færdiggørelse for at afslutte I/O-operationen.

en forskel mellem den asynkrone I/O-model og den signalstyrede I/O-model er, at med signaldrevet i/O fortæller kernen applikationen, hvornår en I/O-operation kan startes, men med den asynkrone I/O-model fortæller kernen applikationen, når en I/O-operation er afsluttet.

asynkron I / O-model

asynkron I / O-model

fordele:

  • programmet er ikke blokeret
  • denne model kan give den bedste ydelse

ulemper:

  • den mest komplicerede I/O-model til implementering
  • ikke alle operativsystemer understøtter denne model effektivt

Java I / O API ‘ er

Java io API er baseret på strømme (InputStream, OutputStream), der repræsenterer blokering, en-retningsbestemt datastrøm.

Java NIO API

Java NIO API er baseret på kanalen, Buffer, Vælgerklasser, der er adaptere til lavt niveau I/O-operationer af operativsystemer.

Kanalklassen repræsenterer en forbindelse til en enhed (udstyrsenhed, fil, stik, programmelkomponent osv.), Der er i stand til at udføre I/O-operationer (læsning eller skrivning).

i sammenligning med Uni-directional streams er kanaler tovejs.

Bufferklassen er en fast størrelse databeholder med yderligere metoder til at læse og skrive data. Alle kanaldata håndteres gennem Buffer, men aldrig direkte: alle data, der sendes til en kanal, skrives i en Buffer, alle data, der modtages fra en kanal, læses i en Buffer.

i sammenligning med strømme, der er byte-orienterede, er kanaler blokorienterede. Byte-orienteret i/O er enklere, men for nogle I / O-enheder kan det være ret langsomt. Blokorienteret I / O kan være meget hurtigere, men er mere kompliceret.

Vælgerklassen giver mulighed for at abonnere på begivenheder fra mange registrerede valgbare Kanalobjekter i et enkelt opkald. Når begivenheder ankommer, sender et Vælgerobjekt dem til de tilsvarende hændelseshåndterere.

Java NIO2 API

Java NIO2 API er baseret på asynkrone kanaler (asynchronousserversocketchannel, asynchronoussocketchannel osv.), der understøtter asynkrone I/O-operationer (tilslutning, læsning eller skrivning, fejlhåndtering).

de asynkrone kanaler giver to mekanismer til styring af asynkrone I/O-operationer. Den første mekanisme er ved at returnere en java.util.samtidig.Fremtidig objekt, som modellerer en verserende operation og kan bruges til at forespørge om tilstanden og opnå resultatet. Den anden mekanisme er ved at overføre til operationen en java.nio.kanal.CompletionHandler objekt, som definerer handler metoder, der udføres efter operationen er afsluttet eller mislykkedes. Den leverede API for begge mekanismer er ækvivalente.

asynkrone kanaler giver en standard måde at udføre asynkrone operationer platform-uafhængigt. Det beløb, som Java sockets API kan udnytte native asynkrone funktioner i et operativsystem, afhænger dog af understøttelsen af denne platform.

Socket echo server

de fleste af de ovennævnte I/O-modeller implementeres her i echo-servere og klienter med Java sockets API ‘ er. Echo-serverne og klienterne fungerer efter følgende algoritme:

  1. en server lytter til en sokkel på en registreret TCP-port 7000
  2. en klient forbinder fra en sokkel på en dynamisk TCP-port til serverstikket
  3. klienten læser en inputstreng fra konsollen og sender bytes fra dens sokkel til serverstikket
  4. serveren modtager bytes fra dens sokkel og sender dem tilbage til klientstikket
  5. klienten modtager bytes fra sin stikkontakt og skriver den ekkoede streng på konsollen
  6. når klienten modtager det samme antal bytes, som den har sendt, afbrydes den fra serveren
  7. når serveren modtager en speciel streng, stopper den med at lytte

konverteringen mellem strenge og bytes her udføres eksplicit i UTF-8-kodning.

Yderligere findes kun forenklede koder til echo-servere. Linket til de komplette koder for echo-servere og klienter findes i konklusionen.

blokering af IO echo server

i det følgende eksempel implementeres den blokerende I/O-model i en echo-server med Java io API.

ServerSocket.accepter metodeblokke, indtil en forbindelse accepteres. InputStream.Læs metode blokerer indtil input data er tilgængelige, eller en klient er afbrudt. OutputStream.skriv metode blokke, indtil alle outputdata 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();
}
}

blokering af NIO echo server

i det følgende eksempel implementeres den blokerende I/O-model i en echo-server med Java NIO API.

ServerSocketChannel-og SocketChannel-objekterne er som standard konfigureret i blokeringstilstand. ServerSocketChannel.accepter metode blokerer og returnerer en SocketChannel objekt, når en forbindelse accepteres. ServerSocket.Læs metode blokerer indtil input data er tilgængelige, eller en klient er afbrudt. ServerSocket.skriv metode blokke, indtil alle outputdata 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-blokerende NIO echo server

i det følgende eksempel implementeres den ikke-blokerende I/O-model i en echo-server med Java NIO API.

ServerSocketChannel-og SocketChannel-objekterne er eksplicit konfigureret i ikke-blokerende tilstand. ServerSocketChannel.accept-metoden blokerer ikke og returnerer null, hvis der endnu ikke accepteres nogen forbindelse eller et SocketChannel-objekt på anden måde. ServerSocket.Læs blokerer ikke og returnerer 0, hvis der ikke er tilgængelige data, eller hvis et positivt antal bytes læses på anden måde. ServerSocket.skrivemetoden blokerer ikke, hvis der er ledig plads i stikkontaktens outputbuffer.

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

multipleks NIO echo server

i det følgende eksempel implementeres multipleks I/O-modellen i en echo server Java NIO API.

under initialiseringen registreres flere ServerSocketChannel-objekter, der er konfigureret i ikke-blokerende tilstand, på det samme Vælgerobjekt med SelectionKey.Op_accept argument for at angive, at en begivenhed af tilslutning accept er interessant.

i hovedsløjfen, vælgeren.vælg metodeblokke, indtil mindst en af de registrerede begivenheder finder sted. Så vælgeren.selectedKeys-metoden returnerer et sæt af de SelectionKey-objekter, for hvilke der er sket hændelser. Iterating gennem SelectionKey objects, er det muligt at bestemme, hvad I/O begivenhed (forbinde, acceptere, læse, skrive) er sket, og hvilke sockets objekter (ServerSocketChannel, SocketChannel) har været forbundet med denne begivenhed.

angivelse af en valgtast, at en kanal er klar til en operation, er et tip, 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 angiver, at der er sket en forbindelsesaccepthændelse, er det lavet ServerSocketChannel.Accepter opkald (som kan være en ikke-blokerende) for at acceptere forbindelsen. Derefter konfigureres et nyt Stikkanalobjekt i ikke-blokerende tilstand og registreres på det samme Vælgerobjekt med SelectionKey.Op_read argument for at angive, at nu en begivenhed af læsning er 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 angiver, at der er sket en læsebegivenhed, er det lavet en SocketChannel.Læs opkald (som kan være en ikke-blokerende) for at læse data fra SocketChannel-objektet til et nyt ByteByffer-objekt. Derefter registreres SocketChannel-objektet på det samme Vælgerobjekt med SelectionKey.Op_skriv argument for at angive, at nu en begivenhed af skrive er interessant. Derudover bruges dette ByteBuffer-objekt under registreringen som en vedhæftet fil.

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 angiver, at der er sket en skrivehændelse, er det lavet SocketChannel.skriv opkald (som kan være en ikke-blokerende) for at skrive data til Stiketkanalobjektet fra ByteByffer-objektet, ekstraheret fra valgnøglen.vedhæftningsmetode. Efter det, Stiketkanal.cloase call lukker forbindelsen.

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

efter hver læsning eller skrivning fjernes SelectionKey-objektet fra sættet af SelectionKey-objekterne for at forhindre genbrug. Men SelectionKey-objektet til forbindelsesaccept fjernes ikke for at have evnen til at foretage den næste lignende operation.

asynkron NIO2 echo server

i det følgende eksempel implementeres den asynkrone I/O-model i en echo-server med Java NIO2 API. Asynchronousserversocketchannel, asynchronoussocketchannel klasser her bruges med færdiggørelse handlere mekanisme.

AsynchronousServerSocketChannel.accept metode initierer en asynkron tilslutning accept operation.

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 forbindelse accepteres (eller operationen mislykkes), kaldes AcceptCompletionHandler-klassen, som ved asynkronstikkanal.Læs (ByteBuffer destination, en vedhæftet fil, CompletionHandler<heltal,? super a> handler) metode initierer en asynkron læseoperation fra asynkron Socketchannel-objektet til et nyt 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 læseoperationen er afsluttet (eller mislykkes), kaldes ReadCompletionHandler-klassen, som ved den asynkronesocketchannel.skrive (ByteBuffer kilde, en vedhæftet fil, CompletionHandler<heltal,? super a> handler) metode initierer en asynkron skrive operation til asynkron Socketchannel objekt fra ByteBuffer objekt.

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 skriveoperationen er afsluttet (eller mislykkes), kaldes Skrivefuldførelsesklassen, som ved den asynkronesocketkanal.luk metode lukker forbindelsen.

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 eksempel udføres asynkrone I/O-operationer uden vedhæftning, fordi alle de nødvendige objekter (asynkronstikkanal, ByteBuffer) sendes som konstruktørargumenter for de relevante færdiggørelseshåndterere.

konklusion

valget af I/O-modellen til stikkontakter kommunikation afhænger af trafikparametrene. Hvis I / O-anmodninger er lange og sjældne, er asynkron I/O generelt et godt valg. Men hvis I/O-anmodninger er korte og hurtige, kan omkostningerne ved behandling af kerneopkald gøre synkron i / O meget bedre.

på trods af at Java giver en standard måde at udføre sockets I/O i de forskellige operativsystemer, kan den faktiske ydelse variere betydeligt afhængigt af deres implementering. Det er muligt at begynde at studere disse forskelle med Dan Kegels velkendte artikel C10K-problemet.

komplette kodeeksempler er tilgængelige i GitHub-arkivet.

Skriv et svar

Din e-mailadresse vil ikke blive publiceret.