Java sockets I / O: blockering, icke-blockering och asynkron

foto av Evi Radauscher på Unsplash

när man beskriver i / O används termerna icke-blockerande och asynkrona ofta omväxlande, men det finns en signifikant skillnad mellan dem. I den här artikeln beskrivs de teoretiska och praktiska skillnaderna mellan icke-blockerande och asynkrona sockets I/O-operationer i Java.

Sockets är slutpunkter för att utföra tvåvägskommunikation med TCP-och UDP-protokoll. Java sockets API: er är adaptrar för motsvarande funktionalitet i operativsystemen. Sockets kommunikation i POSIX-Kompatibla operativsystem (Unix, Linux, Mac OS X, BSD, Solaris, AIX, etc.) utförs av Berkeley sockets. Sockets kommunikation i Windows utförs av Winsock som också är baserad på Berkeley sockets med ytterligare funktionalitet för att följa Windows-programmeringsmodellen.

POSIX-definitionerna

i den här artikeln används förenklade definitioner från POSIX-specifikationen.

blockerad tråd-en tråd som väntar på något villkor innan den kan fortsätta exekveringen.

blockering — en egenskap hos ett uttag som gör att samtal till det väntar på att den begärda åtgärden ska utföras innan den returneras.

icke-blockerande — en egenskap hos ett uttag som får samtal till det att återvända utan dröjsmål, när det upptäcks att den begärda åtgärden inte kan slutföras utan en okänd fördröjning.

synkron I/O — operation-en I/O-operation som gör att den begärande tråden blockeras tills den i/O-operationen är klar.

asynkron I/O — operation-en I/O-operation som inte i sig orsakar att den begärande tråden blockeras; detta innebär att tråden och I/O-operationen kan köras samtidigt.

så enligt POSIX-specifikationen är skillnaden mellan termerna icke-blockerande och asynkrona uppenbara:

  • icke-blockerande — en egenskap hos ett uttag som får samtal till det att återvända utan dröjsmål
  • asynkron I/O — en egenskap på en I/O-operation (läsning eller skrivning) som körs samtidigt med den begärande tråden

I/O-modeller

följande I/O-modeller är de vanligaste för POSIX-Kompatibla operativsystem:

  • blockerande I/O-modell
  • icke-blockerande I/O-modell
  • I/O-multiplexeringsmodell
  • signaldriven I/O-modell
  • asynkron I/O-modell

blockerande I/O-modell

i den blockerande I/O-modellen gör applikationen ett Blockeringssystemanrop tills data tas emot i kärnan och kopieras från kärnutrymme till användarutrymme.

blockering av I / O-modell

 blockering av I / O-modell

Pros:

  • den enklaste I / O-modellen att implementera

nackdelar:

  • applikationen är blockerad

icke-blockerande I / O-modell

i den icke-blockerande I/O-modellen gör applikationen ett systemanrop som omedelbart returnerar ett av två svar:

  • om i/O-operationen kan slutföras omedelbart, returneras data
  • om i/o-operationen inte kan slutföras omedelbart, returneras en felkod som indikerar att I/O-operationen skulle blockera eller enheten är tillfälligt otillgänglig

för att slutföra I/O-operationen bör applikationen upptagen-vänta (gör upprepade systemanrop) tills den är klar.

icke-blockerande I / O-modell

 icke-blockerande I / O-modell

Pros:

  • applikationen är inte blockerad

nackdelar:

  • ansökan bör upptagen-vänta tills färdigställandet, som skulle orsaka många användare-kernel sammanhang växlar
  • denna modell kan införa i/o-latens eftersom det kan finnas ett gap mellan datatillgänglighet i kärnan och data läsning av programmet

I/O multiplexering modell

i I/O multiplexering modell (även känd som den icke – – blockering av i/O-modell med blockeringsmeddelanden), gör applikationen ett blockerande Select-systemanrop för att börja övervaka aktivitet på många beskrivare. För varje deskriptor är det möjligt att begära anmälan om dess beredskap för vissa i/O-operationer (anslutning, läsning eller skrivning, fel förekomst etc.). När select-systemanropet returnerar att minst en deskriptor är klar, gör programmet ett icke-blockerande samtal och kopierar data från kärnutrymme till användarutrymme.

i / O-multiplexeringsmodell

 I / O-multiplexeringsmodell

Pros:

  • det är möjligt att utföra I / O-operationer på flera deskriptorer i en tråd

nackdelar:

  • applikationen är fortfarande blockerad på det valda systemanropet
  • inte alla operativsystem stöder denna modell effektivt

Signaldriven I/O-modell

i den signaldrivna I/O-modellen gör applikationen ett icke-blockerande samtal och registrerar en signalhanterare. När en deskriptor är redo för en I / O-operation genereras en signal för applikationen. Sedan kopierar signalhanteraren data från kärnutrymme till användarutrymme.

signaldriven I / O-modell

 signaldriven I / O-modell

Pros:

  • applikationen är inte blockerad
  • signaler kan ge bra prestanda

nackdelar:

  • inte alla operativsystem stöder signaler

asynkron I / O-modell

i den asynkrona I/O-modellen (även känd som den överlappade I/O-modellen) gör applikationen det icke-blockerande samtalet och startar en bakgrundsoperation i kärnan. När operationen är klar (data tas emot i kärnan och kopieras från kärnutrymme till användarutrymme) genereras en slutförande återuppringning för att avsluta i/O-operationen.

en skillnad mellan den asynkrona I/O-modellen och den signaldrivna I/O-modellen är att med signaldriven i/O berättar kärnan applikationen när en I/O-operation kan initieras, men med den asynkrona I/O-modellen berättar kärnan applikationen när en I/O-operation är klar.

asynkron I / O-modell

 asynkron I / O-modell

Pros:

  • applikationen är inte blockerad
  • denna modell kan ge bästa prestanda

nackdelar:

  • den mest komplicerade I / O-modellen att implementera
  • inte alla operativsystem stöder denna modell effektivt

Java I/O API: er

Java Io API är baserat på strömmar (InputStream, OutputStream) som representerar blockering, ettriktat dataflöde.

Java NIO API

Java NIO API är baserat på Kanal, buffert, Väljarklasser, som är adaptrar till lågnivå I/O-operationer av operativsystem.

kanalklassen representerar en anslutning till en enhet (maskinvaruenhet, fil, uttag, programvarukomponent etc.) som kan utföra I/O-operationer (läsning eller skrivning).

i jämförelse med enkelriktade strömmar är kanalerna Dubbelriktade.

Buffertklassen är en databehållare med fast storlek med ytterligare metoder för att läsa och skriva data. Alla kanaldata hanteras via buffert men aldrig direkt: Alla data som skickas till en kanal skrivs in i en buffert, Alla data som tas emot från en kanal läses in i en buffert.

i jämförelse med strömmar, som är byteorienterade, är kanaler blockorienterade. Byte-orienterad I / O är enklare men för vissa i/O-enheter kan det vara ganska långsamt. Blockorienterad I / O kan vara mycket snabbare men är mer komplicerad.

Väljarklassen tillåter att prenumerera på händelser från många registrerade SelectableChannel-objekt i ett enda samtal. När händelser anländer skickar ett Väljarobjekt dem till motsvarande händelsehanterare.

Java NIO2 API

Java NIO2 API är baserat på asynkrona kanaler (AsynchronousServerSocketChannel, asynchronoussocketchannel, etc) som stöder asynkrona I/O-operationer (anslutning, läsning eller skrivning, felhantering).

de asynkrona kanalerna tillhandahåller två mekanismer för att styra asynkrona I/O-operationer. Den första mekanismen är genom att returnera en java.util.samtidig.Framtida objekt, som modellerar en väntande operation och kan användas för att fråga tillståndet och få resultatet. Den andra mekanismen är genom att skicka till operationen en java.nio.kanaler.CompletionHandler-objekt, som definierar hanteringsmetoder som körs efter att åtgärden har slutförts eller misslyckats. Det tillhandahållna API för båda mekanismerna är likvärdiga.

asynkrona kanaler ger ett standardiserat sätt att utföra asynkrona operationer plattform-oberoende. Mängden som Java sockets API kan utnyttja inbyggda asynkrona funktioner i ett operativsystem beror dock på stödet för den plattformen.

Socket echo server

de flesta av de ovan nämnda I/O-modellerna implementeras här i echo-servrar och klienter med Java sockets API: er. Echo-servrarna och klienterna arbetar med följande algoritm:

  1. en server lyssnar på ett uttag på en registrerad TCP-port 7000
  2. en klient ansluter från ett uttag på en dynamisk TCP-port till serveruttaget
  3. klienten läser en inmatningssträng från konsolen och skickar byte från sitt uttag till serveruttaget
  4. servern tar emot byte från sitt uttag och skickar dem tillbaka till klientuttaget
  5. klienten tar emot byte från sitt uttag och skriver den ekade strängen på konsolen
  6. när klienten får samma antal byte som den har skickat kopplar den från servern
  7. när servern tar emot en speciell sträng slutar den att lyssna

konverteringen mellan strängar och byte här utförs uttryckligen i UTF-8-kodning.

vidare tillhandahålls endast förenklade koder för echo-servrar. Länken till de fullständiga koderna för echo-servrar och klienter finns i slutsatsen.

blockering av IO echo server

i följande exempel implementeras blockerings-I/O-modellen i en echo-server med Java Io API.

Den ServerSocket.Acceptera metodblock tills en anslutning accepteras. InputStream.läs metodblock tills indata är tillgängliga, eller en klient är frånkopplad. Utmatningsströmmen.skriv metod block tills alla utdata skrivs.

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

blockering NIO echo server

i följande exempel implementeras den blockerande I/O-modellen i en echo-server med Java NIO API.

ServerSocketChannel-och SocketChannel-objekten är som standard konfigurerade i blockeringsläget. Den ServerSocketChannel.accept method blockerar och returnerar ett SocketChannel-objekt när en anslutning accepteras. Den ServerSocket.läs metodblock tills indata är tillgängliga, eller en klient är frånkopplad. Den ServerSocket.skriv metod block tills alla utdata skrivs.

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

icke-blockerande NIO echo server

i följande exempel implementeras den icke-blockerande I/O-modellen i en echo-server med Java NIO API.

ServerSocketChannel-och SocketChannel-objekten konfigureras uttryckligen i icke-blockerande läge. Den ServerSocketChannel.accept-metoden blockerar inte och returnerar null om ingen anslutning accepteras ännu eller ett SocketChannel-objekt annars. Den ServerSocket.read blockerar inte och returnerar 0 om inga data finns tillgängliga eller ett positivt antal byte läses annars. Den ServerSocket.skrivmetoden blockerar inte om det finns ledigt utrymme i uttagets utgångsbuffert.

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

multiplexering NIO echo server

i följande exempel implementeras multiplexering I/O-modellen i en echo server Java NIO API.

under initieringen registreras flera ServerSocketChannel-objekt, som är konfigurerade i icke-blockerande läge, på samma Väljarobjekt med SelectionKey.Op_accept argument för att ange att en händelse av anslutning acceptans är intressant.

väljaren i huvudslingan.välj metodblock tills minst en av de registrerade händelserna inträffar. Sedan Väljaren.selectedKeys-metoden returnerar en uppsättning av SelectionKey-objekt för vilka händelser har inträffat. Iterating genom SelectionKey-objekten är det möjligt att bestämma vilken I/O-händelse (Anslut, Acceptera, läs, skriv) som har hänt och vilka sockets-objekt (ServerSocketChannel, SocketChannel) har associerats med den händelsen.

indikering av en valknapp att en kanal är redo för någon operation är en ledtråd, inte 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 ett SelectionKey-objekt indikerar att en anslutningsacceptanshändelse har hänt, görs den till ServerSocketChannel.Acceptera samtal (vilket kan vara en icke-blockerande) för att acceptera anslutningen. Därefter konfigureras ett nytt SocketChannel-objekt i det icke-blockerande läget och registreras på samma Väljarobjekt med SelectionKey.Op_read argument för att ange att nu en händelse av läsning är intressant.

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 ett SelectionKey-objekt indikerar att en läshändelse har hänt, är den gjord till SocketChannel.läs samtal (som kan vara en icke-blockerande) för att läsa data från SocketChannel objektet i en ny ByteByffer objekt. Därefter registreras SocketChannel-objektet på samma Väljarobjekt med SelectionKey.Op_write argument för att ange att nu är en skrivhändelse intressant. Dessutom används detta ByteBuffer-objekt under registreringen som en bilaga.

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 ett SelectionKeys-objekt indikerar att en skrivhändelse har hänt, har den gjort SocketChannel.skriv samtal (som kan vara en icke-blockerande) för att skriva data till SocketChannel-objektet från bytebyffer-objektet, extraherat från SelectionKey.fästmetod. Efter det, Socketkanal.cloase-samtal stänger anslutningen.

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 varje läsning eller skrivning tas SelectionKey-objektet bort från uppsättningen av SelectionKey-objekten för att förhindra återanvändning. Men SelectionKey-objektet för anslutning acceptans tas inte bort för att ha möjlighet att göra nästa liknande operation.

asynkron nio2 echo server

i följande exempel implementeras den asynkrona I/O-modellen i en echo-server med Java NIO2 API. De asynchronousserversocketchannel, asynchronoussocketchannel klasser här används med färdigställandehanterarmekanismen.

AsynchronousServerSocketChannel.accept-metoden initierar en asynkron anslutningsacceptansoperation.

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 anslutning accepteras (eller operationen misslyckas) kallas AcceptCompletionHandler-klassen, vilken av AsynchronousSocketChannel.läsa(ByteBuffer destination, en bilaga, CompletionHandler< heltal,? super a > handler) – metoden initierar en asynkron läsoperation från asynchronoussocketchannel-objektet till ett 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 läsoperationen slutförs (eller misslyckas) kallas readcompletionhandler-klassen, vilken av AsynchronousSocketChannel.skriva(ByteBuffer källa, en bilaga, CompletionHandler< heltal,? super a > handler) – metoden initierar en asynkron skrivoperation till asynchronoussocketchannel-objektet från 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 skrivoperationen slutförs (eller misslyckas) kallas writecompletionhandler-klassen, vilken av AsynchronousSocketChannel.stäng metod stänger anslutningen.

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 det här exemplet utförs asynkrona I/O-operationer utan bilaga, eftersom alla nödvändiga objekt (asynchronoussocketchannel, ByteBuffer) skickas som konstruktörsargument för lämpliga kompletteringshanterare.

slutsats

valet av I/O-modellen för uttagskommunikation beror på trafikparametrarna. Om i/O-förfrågningar är långa och sällsynta är asynkron I / O i allmänhet ett bra val. Men om i/O-förfrågningar är korta och snabba, kan kostnaderna för bearbetning av kärnanrop göra synkron i / O mycket bättre.

trots att Java tillhandahåller ett vanligt sätt att utföra sockets I/O i de olika operativsystemen kan den faktiska prestandan variera avsevärt beroende på deras implementering. Det är möjligt att börja studera dessa skillnader med Dan Kegels välkända artikel c10k-problemet.

kompletta kodexempel finns i GitHub-arkivet.

Lämna ett svar

Din e-postadress kommer inte publiceras.