Java sockets I / O: blokkeren, niet-blokkeren en asynchroon

foto door Evi Radauscher op Unsplash

bij het beschrijven van I / O worden de termen niet-blokkerend en asynchroon vaak door elkaar gebruikt, maar er is een significant verschil tussen beide. In dit artikel worden de theoretische en praktische verschillen beschreven tussen niet-blokkerende en asynchrone sockets I/O operaties in Java.

Sockets zijn eindpunten voor tweerichtingscommunicatie via TCP-en UDP-protocollen. Java sockets API ‘ s zijn adapters voor de bijbehorende functionaliteit van de besturingssystemen. Sockets communicatie in POSIX-Compatibele besturingssystemen (Unix, Linux, Mac OS X, BSD, Solaris, AIX, enz.) wordt uitgevoerd door Berkeley sockets. Sockets-communicatie in Windows wordt uitgevoerd door Winsock, dat ook is gebaseerd op Berkeley sockets met extra functionaliteit om te voldoen aan het Windows-programmeermodel.

de POSIX-definities

in dit artikel worden vereenvoudigde definities uit de POSIX-specificatie gebruikt.

geblokkeerde thread-een thread die wacht op een voorwaarde voordat het kan doorgaan met uitvoeren.

blokkeren-een eigenschap van een socket die ervoor zorgt dat aanroepen wachten tot de gevraagde actie wordt uitgevoerd alvorens terug te keren.

niet-blokkerend-een eigenschap van een socket die ervoor zorgt dat oproepen naar deze socket onmiddellijk terugkeren, wanneer wordt vastgesteld dat de gevraagde actie niet kan worden voltooid zonder een onbekende vertraging.

synchrone I/O-operatie-een I / O-operatie die ervoor zorgt dat de aanvragende thread wordt geblokkeerd totdat die I/O-operatie is voltooid.

asynchrone I / O-operatie-een I/O-operatie die op zichzelf niet tot gevolg heeft dat de aanvragende thread wordt geblokkeerd; Dit houdt in dat de thread en de I/O-operatie gelijktijdig kunnen worden uitgevoerd.

volgens de POSIX-specificatie is het verschil tussen de termen niet-blokkerend en asynchroon duidelijk:

  • non-blocking-een eigenschap van een socket die ervoor zorgt dat oproepen onmiddellijk terugkeren
  • asynchrone I / O – een eigenschap op een i / O-bewerking (lezen of schrijven) die gelijktijdig met de aanvragende thread wordt uitgevoerd

I / O-modellen

de volgende I / O-modellen zijn de meest voorkomende voor de POSIX-Compatibele besturingssystemen:

  • blocking I/O model
  • niet-blocking I/O model
  • I/O multiplexing model
  • signaalgestuurd I/O model
  • asynchrone I/O model

Blocking I/O model

in het blocking I/O model voert de applicatie een blocking-systeemaanroep uit totdat gegevens in de kernel worden ontvangen en worden gekopieerd van kernelruimte naar gebruikersruimte.

blocking I / O model

blocking I / O model

voors:

  • het eenvoudigste te implementeren I/O-model

Cons:

  • de toepassing is geblokkeerd

niet-blokkerend I / O-model

In het niet-blokkerende I / O-model voert de toepassing een systeemaanroep uit die onmiddellijk een van de twee reacties retourneert:

  • als de i / O-bewerking onmiddellijk kan worden voltooid, worden de gegevens geretourneerd
  • als de i/O-bewerking niet onmiddellijk kan worden voltooid, wordt een foutcode geretourneerd die aangeeft dat de i/O-bewerking zou blokkeren of dat het apparaat tijdelijk niet beschikbaar is

om de i / O-bewerking te voltooien, moet de toepassing bezig wachten (herhaal systeemaanroepen) tot de uitvoering.

niet-blokkerend I/O-model

niet-blokkerend I / O-model

voors:

  • de toepassing is niet geblokkeerd

Cons:

  • de toepassing moet bezig-wachten tot de voltooiing, dat zou leiden tot veel gebruiker-kernel context switches
  • dit model kan I/O latency introduceren omdat er een kloof kan zijn tussen de beschikbaarheid van gegevens in de kernel en het lezen van gegevens door de toepassing

I/O multiplexing model

in het I/O multiplexing model (ook bekend als het niet-blokkerende I/O model met blokkerende notificaties), maakt de toepassing een blokkerende selectie systeemaanroep om te beginnen met het monitoren van activiteit op veel descriptoren. Voor elke descriptor is het mogelijk om melding te vragen van zijn gereedheid voor bepaalde I / O-bewerkingen (verbinding, lezen of schrijven, voorkomen van fouten, enz.). Wanneer de systeemaanroep selecteert dat ten minste één descriptor klaar is, maakt de toepassing een niet-blokkerende aanroep en kopieert de gegevens van de kernelruimte naar de gebruikersruimte.

I / O-multiplexmodel

I / O-multiplexmodel

voors:

  • het is mogelijk om I/O-bewerkingen uit te voeren op meerdere descriptoren in één thread

Cons:

  • de toepassing is nog steeds geblokkeerd op de Select system call
  • niet alle besturingssystemen ondersteunen dit model efficiënt

Signaalgestuurde I/O model

In het signaalgestuurde I/O model maakt de toepassing een niet-blokkerende oproep en registreert een signaalafhandeling. Wanneer een descriptor klaar is voor een I/O-operatie, wordt een signaal gegenereerd voor de toepassing. Vervolgens kopieert de signal handler de gegevens van de kernelruimte naar de gebruikersruimte.

signaal-driven I/O-model

signaal-driven I/O-model

Voors:

  • De toepassing is niet geblokkeerd
  • – Signalen kan leveren goede prestaties

Nadelen:

  • Niet alle besturingssystemen ondersteunen signalen

Asynchrone I/O-model

In de asynchrone I/O-model (ook wel bekend als de overlappende I/O-model) de toepassing maakt het niet-oproep blokkeren en begint een werking op de achtergrond, in de kernel. Wanneer de operatie is voltooid (data worden ontvangen in de kernel en worden gekopieerd van de kernelruimte naar de gebruikersruimte), wordt een voltooiing callback gegenereerd om de I/O operatie te voltooien.

een verschil tussen het asynchrone I/O-model en het signaal-gestuurde I/O-model is dat met signaal-gestuurde I/O, de kernel de toepassing vertelt wanneer een I/O-operatie kan worden gestart, maar met het asynchrone I/O-model, de kernel de toepassing vertelt wanneer een I/O-operatie is voltooid.

asynchrone I/O-model

asynchrone I/O-model

Voors:

  • De toepassing is niet geblokkeerd
  • Dit model kan zorgen voor de beste prestaties

Nadelen:

  • De meest ingewikkelde I/O-model te implementeren
  • Niet alle besturingssystemen ondersteunen dit model efficiënt

Java-I/O Api ‘ s

Java-IO API is gebaseerd op stromen (xml-aanvragen verzenden, OutputStream) die staan te blokkeren, one-directionele data flow.

Java NIO API

Java NIO API is gebaseerd op de kanaal -, Buffer-en Selectorklassen, die adapters zijn voor low-level I/O-operaties van besturingssystemen.

de Kanaalklasse vertegenwoordigt een verbinding met een entiteit (hardwareapparaat, bestand, socket, softwarecomponent, enz.) die in staat is om I/O-bewerkingen uit te voeren (lezen of schrijven).

in vergelijking met Uni-directionele streams zijn kanalen bi-directioneel.

de Bufferklasse is een gegevenscontainer van vaste grootte met aanvullende methoden om gegevens te lezen en te schrijven. Alle kanaalgegevens worden verwerkt via Buffer maar nooit direct: alle gegevens die naar een kanaal worden verzonden worden in een Buffer geschreven, alle gegevens die van een kanaal worden ontvangen worden in een Buffer gelezen.

in vergelijking met streams, die byte-georiënteerd zijn, zijn kanalen blok-georiënteerd. Byte-georiënteerde I / O is eenvoudiger, maar voor sommige I / O entiteiten kunnen vrij traag zijn. Blok-georiënteerde I / O kan veel sneller zijn, maar is ingewikkelder.

met de Selectorklasse kunt u zich abonneren op gebeurtenissen van veel geregistreerde SelectableChannel-objecten in één aanroep. Wanneer gebeurtenissen aankomen, verzendt een Selector object ze naar de overeenkomstige event handlers.

Java NIO2 API

Java NIO2 API is gebaseerd op asynchrone kanalen (AsynchronousServerSocketChannel, AsynchronousSocketChannel, enz.) die asynchrone I/O-operaties ondersteunen (verbinden, lezen of schrijven, fouten verwerken).

de asynchrone kanalen bieden twee mechanismen om asynchrone I / O-operaties te controleren. Het eerste mechanisme is door het retourneren van een java.util.gelijktijdig.Future object, dat modelleert een hangende operatie en kan worden gebruikt om de status te bevragen en het resultaat te verkrijgen. Het tweede mechanisme is door het doorgeven aan de operatie een java.nio.kanaal.CompletionHandler object, dat handler methoden definieert die worden uitgevoerd nadat de bewerking is voltooid of mislukt. De verstrekte API voor beide mechanismen zijn gelijkwaardig.

asynchrone kanalen bieden een standaard manier om asynchrone operaties platformonafhankelijk uit te voeren. Echter, de hoeveelheid die Java sockets API inheemse asynchrone mogelijkheden van een besturingssysteem kan benutten, zal afhangen van de ondersteuning voor dat platform.

Socket echo server

de meeste van de hierboven genoemde I/O modellen worden hier geïmplementeerd in echo servers en clients met Java sockets API ‘ s. De echo servers en clients werken volgens het volgende algoritme:

  1. een server luistert naar een aansluiting op een geregistreerde TCP-poort 7000
  2. een client verbinding maakt van een aansluiting op een dynamische TCP-poort aan de server socket
  3. de klant leest een input-string van de console en stuurt de bytes van de aansluiting op de server socket
  4. de server ontvangt de bytes van de houder en stuurt ze terug naar de client socket
  5. de opdrachtgever ontvangt van de bytes van de houder en schrijft de teruggekaatste string op de console
  6. wanneer de opdrachtgever eenzelfde aantal bytes dat is verzonden, het verbreekt de verbinding met de server
  7. wanneer de server een speciale tekenreeks ontvangt, stopt het met luisteren

de conversie tussen tekenreeksen en bytes wordt hier expliciet uitgevoerd in UTF-8-codering.

verder worden alleen vereenvoudigde codes voor echo-servers verstrekt. De link naar de volledige codes voor echo-servers en-clients vindt u in de conclusie.

Blocking Io echo server

in het volgende voorbeeld wordt het blocking I / O model geà mplementeerd in een echo server met Java Io API.

De ServerSocket.accept methode blokkeert totdat een verbinding wordt geaccepteerd. De InputStream.leesmethode blokkeert totdat invoergegevens beschikbaar zijn of een client wordt losgekoppeld. De OutputStream.schrijf methode blokken totdat alle output gegevens zijn geschreven.

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

Blocking NIO echo server

in het volgende voorbeeld wordt het blocking I / O model geà mplementeerd in een echo server met Java NIO API.

de objecten ServerSocketChannel en SocketChannel worden standaard geconfigureerd in de blokkeringsmodus. Het Serversocketkanaal.accept method blokkeert en retourneert een SocketChannel object wanneer een verbinding wordt geaccepteerd. De ServerSocket.leesmethode blokkeert totdat invoergegevens beschikbaar zijn of een client wordt losgekoppeld. De ServerSocket.schrijf methode blokken totdat alle output gegevens zijn geschreven.

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

niet-blokkerende NIO echo server

in het volgende voorbeeld wordt het niet-blokkerende I / O model geà mplementeerd in een echo server met Java NIO API.

de objecten ServerSocketChannel en SocketChannel worden expliciet geconfigureerd in de niet-blokkerende modus. Het Serversocketkanaal.accept methode blokkeert niet en retourneert null Als er nog geen verbinding wordt geaccepteerd of een SocketChannel object anders. De ServerSocket.read blokkeert niet en geeft 0 terug als er geen gegevens beschikbaar zijn of als er een positief aantal bytes anders wordt gelezen. De ServerSocket.schrijfmethode blokkeert niet als er vrije ruimte is in de uitvoerbuffer van de socket.

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

in het volgende voorbeeld wordt het multiplexing I / O model geà mplementeerd in een echo server Java NIO API.

tijdens de initialisatie worden meerdere serversocketchannel-objecten, die zijn geconfigureerd in de niet-blokkerende modus, geregistreerd op hetzelfde Selector-object met de SelectionKey.OP_ACCEPT argument om aan te geven dat een gebeurtenis van verbinding acceptatie interessant is.

In de hoofdlus, de Selector.selecteer methode blokken totdat ten minste een van de geregistreerde gebeurtenissen optreedt. Dan de Selector.selectedKeys methode retourneert een set van de selectionkey objecten waarvoor gebeurtenissen hebben plaatsgevonden. Itererend door de selectionkey objecten, is het mogelijk om te bepalen welke I/O event (connect, accept, read, write) is gebeurd en welke sockets objecten (ServerSocketChannel, SocketChannel) zijn geassocieerd met die event.

het aangeven van een selectiesleutel dat een kanaal klaar is voor een bepaalde operatie is een hint, geen garantie.

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

wanneer een SelectionKey-object aangeeft dat er een verbindingsacceptatie-gebeurtenis heeft plaatsgevonden, wordt het ServerSocketChannel gemaakt.aanroep accepteren (wat een niet-blokkerende kan zijn) om de verbinding te accepteren. Daarna wordt een nieuw SocketChannel-object geconfigureerd in de niet-blokkerende modus en wordt geregistreerd op hetzelfde Selector-object met de SelectionKey.OP_READ argument om aan te geven dat nu een gebeurtenis van lezen interessant is.

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

wanneer een SelectionKey-object aangeeft dat een leesgebeurtenis heeft plaatsgevonden, wordt een Socketkanaal gemaakt.lees aanroep (wat een niet-blokkerende) om gegevens van het socketchannel object in een nieuw ByteByffer object te lezen. Daarna wordt het object SocketChannel geregistreerd op hetzelfde Selectorobject met de SelectionKey.Op_write argument om aan te geven dat nu een gebeurtenis van schrijven interessant is. Daarnaast wordt dit bytebuffer object gebruikt tijdens de registratie als bijlage.

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

wanneer een object SelectionKeys aangeeft dat er een schrijfgebeurtenis heeft plaatsgevonden, wordt het SocketChannel gemaakt.write call (wat een niet-blokkerende) om gegevens te schrijven naar het SocketChannel object van het bytebyffer object, geëxtraheerd uit de SelectionKey.bijlage methode. Daarna, het stopcontact kanaal.cloase gesprek sluit de verbinding.

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

na elk lezen of schrijven wordt het selectionkey-object uit de set van de selectionkey-objecten verwijderd om hergebruik te voorkomen. Maar de selectionkey object voor verbinding acceptatie wordt niet verwijderd om de mogelijkheid om de volgende soortgelijke operatie te maken.

asynchrone nio2 echo server

in het volgende voorbeeld wordt het asynchrone I / O model geà mplementeerd in een echo server met Java NIO2 API. De asynchronousserversocketchannel, AsynchronousSocketChannel klassen hier worden gebruikt met de voltooiing handlers mechanisme.

Het Asynchrone Serversocketkanaal.accept methode initieert een asynchrone verbinding acceptatie operatie.

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

wanneer een verbinding wordt geaccepteerd (of de operatie mislukt), wordt de klasse AcceptCompletionHandler aangeroepen, die door het asynchrone Socketchannel wordt aangeroepen.read (ByteBuffer destination, a attachment, CompletionHandler<Integer,? super a > handler) methode initieert een asynchrone leesbewerking van het asynchronoussocketchannel object naar een nieuw ByteBuffer object.

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

wanneer de leesbewerking is voltooid (of mislukt), wordt de readcompletionhandler-klasse aangeroepen, die door het asynchrone Socketchannel wordt aangeroepen.write (ByteBuffer source, a attachment, CompletionHandler<Integer,? super a > handler) methode initieert een asynchrone schrijfbewerking naar het asynchronoussocketchannel object vanuit het ByteBuffer object.

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

wanneer de schrijfbewerking is voltooid (of mislukt), wordt de klasse WriteCompletionHandler aangeroepen, die door het asynchrone Socketchannel wordt aangeroepen.sluit methode sluit de verbinding.

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

in dit voorbeeld worden asynchrone I/O-bewerkingen uitgevoerd zonder bijlage, omdat alle benodigde objecten (AsynchronousSocketChannel, ByteBuffer) worden doorgegeven als constructorargumenten voor de juiste handlers.

conclusie

de keuze van het I/O-model voor communicatie met sockets hangt af van de parameters van het verkeer. Als I/O-verzoeken lang en zeldzaam zijn, is asynchrone I/O over het algemeen een goede keuze. Als I / O-verzoeken echter kort en snel zijn, kan de overhead van het verwerken van kernelaanroepen synchrone I/O veel beter maken.

ondanks dat Java een standaard manier biedt om sockets I/O uit te voeren in de verschillende besturingssystemen, kunnen de werkelijke prestaties aanzienlijk variëren, afhankelijk van hun implementatie. Het is mogelijk om te beginnen met het bestuderen van deze verschillen met Dan Kegel ‘ s bekende artikel de c10k probleem.

volledige codevoorbeelden zijn beschikbaar in de GitHub repository.

Geef een antwoord

Het e-mailadres wordt niet gepubliceerd.