Java sockets I / O: blokování, neblokování a asynchronní

Foto Evi Radauscher na Unsplash

při popisu I / O se pojmy neblokující a asynchronní často používají zaměnitelně, ale mezi nimi existuje významný rozdíl. V tomto článku jsou popsány teoretické a praktické rozdíly mezi neblokovacími a asynchronními soketovými i / o operacemi v Javě.

sokety jsou koncové body pro obousměrnou komunikaci pomocí protokolů TCP a UDP. Java sockets API jsou adaptéry pro odpovídající funkčnost operačních systémů. Komunikace soketů v operačních systémech kompatibilních s POSIX (Unix, Linux, Mac OS X, BSD, Solaris, AIX atd.).) provádí Berkeley sockets. Komunikace soketů ve Windows je prováděna společností Winsock, která je také založena na Soketech Berkeley s dalšími funkcemi, které odpovídají programovacímu modelu Windows.

definice POSIX

v tomto článku se používají zjednodušené definice ze specifikace POSIX.

Blokované vlákno — vlákno, které čeká na nějakou podmínku, než bude moci pokračovat v provádění.

blokování-vlastnost soketu, která způsobí, že volání na něj čeká na provedení požadované akce před návratem.

Neblokování-vlastnost soketu, která způsobí, že se volání k němu vrátí bez prodlení, když je zjištěno, že požadovanou akci nelze dokončit bez neznámého zpoždění.

synchronní operace I — O-operace I / O, která způsobí zablokování žádajícího vlákna, dokud se operace I / O nedokončí.

asynchronní operace I / O-operace I/O, která sama o sobě nezpůsobuje zablokování žádajícího vlákna; to znamená, že vlákno a operace I/O mohou být spuštěny souběžně.

takže podle specifikace POSIX je rozdíl mezi pojmy neblokující a asynchronní zřejmý:

  • neblokování-vlastnost soketu, který způsobí, že volání na něj se vrátí bez prodlení
  • asynchronní I / O-vlastnost na I / o operaci (čtení nebo zápis), která běží souběžně s požadovaným podprocesem

I/o modely

následující I / o modely jsou nejběžnější pro operační systémy kompatibilní s POSIX:

  • blokování I/o modelu
  • neblokující I/o model
  • i/o multiplexní model
  • signálově řízený I/o model
  • asynchronní I/o model

blokování I/o modelu

v blokačním I/o modelu aplikace provádí blokovací systémové volání, dokud nejsou přijímána data v jádře a nejsou zkopírováno z prostoru jádra do uživatelského prostoru.

blokování I / o modelu

blokování I / o modelu

Pros:

  • nejjednodušší I / o model pro implementaci

nevýhody:

  • aplikace je blokována

neblokující I / o model

v neblokujícím I / o modelu aplikace provede systémové volání, které okamžitě vrátí jednu ze dvou odpovědí:

  • pokud lze I / o operaci dokončit okamžitě, data jsou vrácena
  • pokud I / o operaci nelze dokončit okamžitě, vrátí se chybový kód, který indikuje, že I/o operace by se zablokovala nebo zařízení je dočasně nedostupné

pro dokončení I/O operace by aplikace měla být zaneprázdněna čekáním (opakováním systémových volání) až do dokončení.

neblokující I / o model

neblokující I / o model

Pros:

  • aplikace není blokována

nevýhody:

  • aplikace by měla být zaneprázdněna-počkat až do dokončení, což by způsobilo mnoho kontextových přepínačů uživatelského jádra
  • tento model může zavést i/o latenci, protože může existovat mezera mezi dostupností dat v jádře a čtením dat aplikací

i/o multiplexní model

v I/O multiplexním modelu (také známý jako neblokovací I/o model s blokovacími oznámeními), aplikace provede blokovací systém, který umožňuje vyberte systémové volání a začněte sledovat aktivitu na mnoha deskriptorech. U každého deskriptoru je možné požádat o oznámení o jeho připravenosti na určité I/O operace (připojení, čtení nebo zápis, výskyt chyb atd.).). Když select system call vrátí, že alespoň jeden deskriptor je připraven, aplikace provede neblokovací hovor a zkopíruje data z prostoru jádra do uživatelského prostoru.

I / o multiplexní model

I / O multiplexní model

Pros:

  • je možné provádět I / o operace na více deskriptorech v jednom vlákně

nevýhody:

  • aplikace je stále blokována na select system call
  • ne všechny operační systémy podporují tento model efektivně

Signal-driven I / o model

v signal-driven I / o modelu aplikace dělá neblokující volání a zaregistruje obsluhu signálu. Když je deskriptor připraven pro I/o operaci, vygeneruje se pro aplikaci signál. Poté obsluha signálu zkopíruje data z prostoru jádra do uživatelského prostoru.

signal-driven I / o model

signal-driven I / o model

Pros:

  • aplikace není blokována
  • signály mohou poskytnout dobrý výkon

nevýhody:

  • ne všechny operační systémy podporují signály

asynchronní I / o model

v asynchronním I / o modelu (také známém jako překrývající se I / o model) aplikace provede neblokovací volání a spustí operaci na pozadí v jádře. Po dokončení operace (data jsou přijímána v jádře a jsou zkopírována z prostoru jádra do uživatelského prostoru) je vygenerováno zpětné volání dokončení pro dokončení I/O operace.

rozdíl mezi asynchronním I / o modelem a signálem řízeným I / o modelem spočívá v tom, že s I / O řízeným signálem jádro řekne aplikaci, kdy lze zahájit I/o operaci, ale s asynchronním I / o modelem jádro řekne aplikaci, když je I / o operace dokončena.

asynchronní I / o model

asynchronní I / o model

Pros:

  • aplikace není blokována
  • tento model může poskytnout nejlepší výkon

nevýhody:

  • nejsložitější I / o model implementovat
  • ne všechny operační systémy podporují tento model efektivně

Java I / O API

Java IO API je založen na proudech (InputStream, OutputStream), které představují blokování, jednosměrný tok dat.

Java NIO API

Java NIO API je založeno na třídách kanálů, vyrovnávací paměti, selektorů, které jsou adaptéry pro nízkoúrovňové I / O operace operačních systémů.

třída kanálů představuje připojení k entitě (hardwarové zařízení, soubor, socket, softwarová součást atd.), Která je schopna provádět I / o operace(čtení nebo zápis).

ve srovnání s jednosměrnými proudy jsou kanály obousměrné.

Třída vyrovnávací paměti je datový kontejner s pevnou velikostí s dalšími metodami pro čtení a zápis dat. Všechna data kanálu jsou zpracovávána prostřednictvím vyrovnávací paměti, ale nikdy přímo: všechna data, která jsou odesílána do kanálu, jsou zapsána do vyrovnávací paměti, všechna data, která jsou přijímána z kanálu, jsou načtena do vyrovnávací paměti.

ve srovnání s proudy, které jsou bajtově orientované, jsou kanály blokově orientované. Byte orientované I / O je jednodušší, ale pro některé I / o entity může být poměrně pomalé. Blokově orientovaný I / O může být mnohem rychlejší, ale je složitější.

Třída Selector umožňuje přihlášení k odběru událostí z mnoha registrovaných objektů SelectableChannel V jednom hovoru. Když události dorazí, objekt selektoru je odešle příslušným manipulátorům událostí.

Java NIO2 API

Java NIO2 API je založeno na asynchronních kanálech (Asynchronníserversocketchannel, Asynchronníssocketchannel atd.), které podporují asynchronní I/O operace (připojení, čtení nebo zápis, zpracování chyb).

asynchronní kanály poskytují dva mechanismy pro řízení asynchronních I / O operací. Prvním mechanismem je vrácení java.util.souběžný.Budoucí objekt, který modeluje čekající operaci a může být použit k dotazu na stav a získání výsledku. Druhým mechanismem je předání do operace java.nio.program.CompletionHandler objekt, který definuje handler metody, které jsou provedeny po dokončení nebo selhání operace. Poskytnuté API pro oba mechanismy jsou rovnocenné.

asynchronní kanály poskytují standardní způsob provádění asynchronních operací nezávisle na platformě. Množství, které Java sockets API může využívat nativní asynchronní schopnosti operačního systému, však bude záviset na podpoře této platformy.

Socket echo server

většina výše uvedených I/O modelů je zde implementována v Echo serverech a klientech s Java sockets API. Echo servery a klienti pracují podle následujícího algoritmu:

  1. server poslouchá soket na registrovaném TCP portu 7000
  2. klient se připojí ze soketu na dynamickém TCP portu do soketu serveru
  3. klient přečte vstupní řetězec z konzoly a odešle bajty ze svého soketu do soketu serveru
  4. server přijme bajty ze svého soketu a odešle je zpět do soketu klienta
  5. klient obdrží bajty ze svého soketu a zapíše ozvěnu řetězce na konzoli
  6. když klient obdrží stejný počet bajtů, které odeslal, odpojí se od serveru
  7. když server obdrží speciální řetězec, přestane poslouchat

konverze mezi řetězci a bajty se zde provádí explicitně v kódování UTF-8.

dále jsou poskytovány pouze zjednodušené kódy pro servery echo. V závěru je uveden odkaz na kompletní kódy pro servery a klienty echo.

blokování IO echo serveru

v následujícím příkladu je blokovací I / o model implementován v echo serveru s Java IO API.

ServerSocket.přijměte bloky metod, dokud nebude přijato připojení. Vstupní Proud.čtení bloků metod, dokud nejsou k dispozici vstupní data nebo dokud není klient odpojen. Výstupní Proud.zapisujte bloky metod, dokud nebudou zapsána všechna výstupní data.

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

blokování NIO echo serveru

v následujícím příkladu je blokovací I / o model implementován v echo serveru s Java NIO API.

objekty ServerSocketChannel a SocketChannel jsou ve výchozím nastavení konfigurovány v režimu blokování. ServerSocketChannel.přijmout bloky metody a vrátí objekt SocketChannel, když je přijato připojení. ServerSocket.čtení bloků metod, dokud nejsou k dispozici vstupní data nebo dokud není klient odpojen. ServerSocket.zapisujte bloky metod, dokud nebudou zapsána všechna výstupní data.

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

neblokující NIO echo server

v následujícím příkladu je neblokovací I / o model implementován v echo serveru s Java NIO API.

objekty ServerSocketChannel a SocketChannel jsou explicitně konfigurovány v neblokovacím režimu. ServerSocketChannel.metoda accept neblokuje a vrací null, pokud ještě není přijato žádné připojení nebo objekt SocketChannel jinak. ServerSocket.čtení neblokuje a vrací 0, pokud nejsou k dispozici žádná data nebo kladný počet bajtů přečtených jinak. ServerSocket.metoda zápisu neblokuje, pokud je ve výstupní vyrovnávací paměti soketu volné místo.

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

multiplexní NIO echo server

v následujícím příkladu je multiplexní I / o model implementován v echo serveru Java NIO API.

během inicializace je více objektů ServerSocketChannel, které jsou konfigurovány v neblokovacím režimu, registrováno na stejném objektu selektoru pomocí SelectionKey.Op_accept argument určit, že událost přijetí připojení je zajímavé.

v hlavní smyčce volič.vyberte bloky metod, dokud nenastane alespoň jedna z registrovaných událostí. Pak volič.metoda selectedKeys vrací sadu objektů SelectionKey, pro které došlo k událostem. Iterací pomocí objektů SelectionKey je možné určit, jaká událost I / O (connect, accept, read, write) se stala a které objekty soketů (ServerSocketChannel, SocketChannel) byly s touto událostí spojeny.

označení výběrového klíče, že je kanál připraven na nějakou operaci, je náznak, nikoli záruka.

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

když objekt SelectionKey indikuje, že došlo k události přijetí připojení, provede se ServerSocketChannel.přijmout hovor (což může být neblokování) přijmout připojení. Poté je nový objekt SocketChannel nakonfigurován v neblokovacím režimu a je registrován na stejném objektu selektoru pomocí SelectionKey.Op_read argument určit, že nyní událost čtení je zajímavé.

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

když objekt SelectionKey indikuje, že došlo ke čtení události, je to udělal SocketChannel.read call (což může být neblokování) pro čtení dat z objektu SocketChannel do nového objektu ByteByffer. Poté je objekt SocketChannel registrován na stejném Selektorovém objektu s SelectionKey.Op_write argument určit, že nyní událost zápisu je zajímavé. Tento objekt ByteBuffer se navíc používá při registraci jako příloha.

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

když objekt SelectionKeys označuje, že došlo k události zápisu, je vytvořen SocketChannel.write call (což může být neblokující) pro zápis dat do objektu SocketChannel z objektu ByteByffer, extrahovaného z SelectionKey.Způsob připojení. Poté Soketkanál.cloase call uzavře spojení.

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

po každém čtení nebo zápisu je objekt SelectionKey odstraněn ze sady objektů SelectionKey, aby se zabránilo jeho opětovnému použití. Objekt SelectionKey pro přijetí připojení však není odstraněn, aby byl schopen provést další podobnou operaci.

asynchronní nio2 echo server

v následujícím příkladu je asynchronní I / o model implementován v echo serveru s Java NIO2 API. Třídy AsynchronousServerSocketChannel, AsynchronousSocketChannel se zde používají s mechanismem dokončovacích manipulátorů.

Asynchronní Serversocketchannel.metoda accept zahajuje asynchronní operaci přijetí připojení.

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

když je spojení přijato (nebo operace selže), je volána Třída AcceptCompletionHandler, která asynchronním kanálem.číst(ByteBuffer cíl, příloha, CompletionHandler<celé číslo,? metoda Super a> handler) iniciuje asynchronní operaci čtení z objektu AsynchronousSocketChannel na nový objekt ByteBuffer.

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

když operace čtení dokončí (nebo selže), je volána Třída ReadCompletionHandler, která asynchronním kanálem.zápis(ByteBuffer zdroj, příloha, CompletionHandler<celé číslo,? metoda Super a> handler) iniciuje asynchronní operaci zápisu do objektu AsynchronousSocketChannel z objektu ByteBuffer.

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

když operace zápisu dokončí (nebo selže), je volána třída WriteCompletionHandler, která asynchronním kanálem.zavřít metoda zavře připojení.

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

v tomto příkladu jsou asynchronní I / O operace prováděny bez připojení, protože všechny potřebné objekty (asynchronní Socketchannel, ByteBuffer) jsou předány jako argumenty konstruktoru pro příslušné obslužné rutiny dokončení.

závěr

volba I / o modelu pro komunikaci soketů závisí na parametrech provozu. Pokud jsou I / o požadavky dlouhé a vzácné, asynchronní I/O je obecně dobrá volba. Pokud jsou však I / o požadavky krátké a rychlé, režie zpracování volání jádra může synchronní I / O mnohem zlepšit.

přestože Java poskytuje standardní způsob provádění soketů I / O v různých operačních systémech, skutečný výkon se může výrazně lišit v závislosti na jejich implementaci. Je možné začít studovat tyto rozdíly se známým článkem Dana Kegela the c10k problem.

kompletní příklady kódu jsou k dispozici v repozitáři GitHub.

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna.