Java sockets I / O: blokkoló, nem blokkoló és aszinkron

fotó: Evi Radauscher on Unsplash

az I/O leírásakor a nem blokkoló és az aszinkron kifejezéseket gyakran felcserélhetően használják, de jelentős különbség van közöttük. Ebben a cikkben ismertetjük a Java nem blokkoló és aszinkron sockets I/O műveletei közötti elméleti és gyakorlati különbségeket.

a socketek a TCP és UDP protokollok kétirányú kommunikációjának végpontjai. A Java sockets API-k adapterek az operációs rendszerek megfelelő funkcionalitásához. Sockets kommunikáció POSIX-kompatibilis operációs rendszerekben (Unix, Linux, Mac OS X, BSD, Solaris, AIX stb.) végzi Berkeley sockets. A Sockets kommunikációt A Windows rendszerben a Winsock végzi, amely szintén a Berkeley socketeken alapul, további funkciókkal, hogy megfeleljen a Windows programozási modellnek.

a POSIX definíciók

ebben a cikkben a POSIX specifikáció egyszerűsített definícióit használják.

Blokkolt szál — olyan szál, amely valamilyen feltételre vár, mielőtt folytathatná a végrehajtást.

blokkolás — A socket olyan tulajdonsága, amely arra készteti a hívásokat, hogy a visszatérés előtt megvárják a kért műveletet.

nem blokkolás-A socket olyan tulajdonsága, amely a hívásokat késedelem nélkül visszatéríti, amikor azt észleli, hogy a kért művelet ismeretlen késedelem nélkül nem hajtható végre.

szinkron I/O művelet — olyan I/O művelet, amely a kérő szál blokkolását okozza az I/O művelet befejezéséig.

aszinkron I/O művelet — olyan I/O művelet, amely önmagában nem okozza a kérő szál blokkolását; ez azt jelenti, hogy a szál és az I/O művelet egyidejűleg futhat.

tehát a POSIX specifikáció szerint nyilvánvaló a különbség a nem blokkoló és az aszinkron kifejezések között:

  • nem blokkolás-A socket olyan tulajdonsága, amely a hívásokat késedelem nélkül visszaküldi
  • aszinkron I/O — egy olyan I/O művelet (olvasás vagy írás) tulajdonsága, amely egyidejűleg fut a kérő szálral

I/O modellek

a következő I/O modellek a leggyakoribbak a POSIX — kompatibilis operációs rendszerek:

  • blokkoló I/O modell
  • nem blokkoló I/O modell
  • I/O multiplexelő modell
  • Jelvezérelt I/O modell
  • aszinkron I/O modell

blokkoló I/O modell

a blokkoló I/O modellben az alkalmazás blokkoló rendszerhívást hajt végre, amíg az adatok a kernelhez nem érkeznek, és a rendszermagból a felhasználói térbe kerülnek.

I/O blokkoló modell

 I / O blokkoló modell

profik:

  • a legegyszerűbb I / O modell végrehajtása

Cons:

  • az alkalmazás blokkolva van

nem blokkoló I / O modell

a nem blokkoló I / O modellben az alkalmazás rendszerhívást kezdeményez, amely azonnal visszaadja a két válasz egyikét:

  • ha az I/O művelet azonnal befejezhető, akkor az adatok visszaadódnak
  • ha az I/O művelet nem hajtható végre azonnal, egy hibakód jelenik meg, amely jelzi, hogy az I/O művelet blokkolna, vagy az eszköz ideiglenesen nem érhető el

az I/O művelet befejezéséhez az alkalmazásnak foglalt várakoznia kell (ismétlődő rendszerhívásokat kezdeményezhet) a befejezésig.

nem blokkoló I / O modell

 nem blokkoló I/O modell

profik:

  • az alkalmazás nem blokkolt

Cons:

  • az alkalmazásnak elfoglaltnak kell lennie-várjon a befejezésig, ami sok felhasználó-kernel kontextus kapcsolót okozna
  • ez a modell bevezetheti az I/O késleltetést, mert rés lehet A kernelben elérhető adatok és az alkalmazás által leolvasott adatok között

I/O multiplexelési modell

az I/O multiplexelési modellben (más néven nem blokkoló I/O modell blokkoló értesítésekkel), az alkalmazás blokkoló select rendszerhívást kezdeményez, hogy sok leírón figyelemmel kísérje a tevékenységet. Minden leíróhoz kérhető értesítés arról, hogy készen áll-e bizonyos I/O műveletekre (kapcsolat, olvasás vagy írás, hiba előfordulása stb.). Amikor a select rendszerhívás visszaadja, hogy legalább egy leíró készen áll, az alkalmazás nem blokkoló hívást kezdeményez, és átmásolja az adatokat a kerneltérből a felhasználói területre.

I / O multiplexelési modell

 I / O multiplexelési modell

profik:

  • lehetséges, hogy végre I / O műveletek több leírók egy szál

Cons:

  • az alkalmazás továbbra is blokkolva van a kiválasztott rendszerhíváson
  • nem minden operációs rendszer támogatja hatékonyan ezt a modellt

Jelvezérelt I/O modell

a Jelvezérelt I/O modellben az alkalmazás nem blokkoló hívást kezdeményez, és regisztrál egy jelkezelőt. Amikor egy leíró készen áll egy I / O műveletre, jelet generál az alkalmazás számára. Ezután a jelkezelő átmásolja az adatokat a kernel térből a felhasználói térbe.

Jelvezérelt I / O modell

 Jelvezérelt I / O modell

profik:

  • az alkalmazás nincs blokkolva
  • a jelek jó teljesítményt nyújthatnak

hátrányok:

  • nem minden operációs rendszer támogatja a jeleket

aszinkron I/O modell

az aszinkron I/O modellben (más néven átfedéses I/O modell) az alkalmazás nem blokkoló hívást kezdeményez, és háttérműveletet indít a kernelben. Amikor a művelet befejeződött (az adatokat a rendszermag fogadja, és a rendszermag területéről a felhasználói területre másolja), egy befejezési visszahívás jön létre az I/O művelet befejezéséhez.

az aszinkron I/O modell és a Jelvezérelt I/O modell közötti különbség az, hogy a Jelvezérelt I/O modellnél a kernel megmondja az alkalmazásnak, hogy mikor indítható egy I/O művelet, de az aszinkron I/O modellnél a kernel megmondja az alkalmazásnak, amikor egy I/O művelet befejeződött.

aszinkron I / O modell

 aszinkron I / O modell

profik:

  • az alkalmazás nincs blokkolva
  • ez a modell a legjobb teljesítményt nyújtja

hátrányok:

  • a legbonyolultabb I / O modell a
  • nem minden operációs rendszer támogatja hatékonyan ezt a modellt

Java I/O API-k

a Java Io API olyan adatfolyamokon (InputStream, OutputStream) alapul, amelyek blokkoló, egyirányú adatfolyamot képviselnek.

Java NIO API

a Java NIO API a csatorna, puffer, választó osztályokon alapul, amelyek az operációs rendszerek alacsony szintű I/O műveleteinek adapterei.

a csatorna osztály egy olyan entitáshoz (hardvereszköz, fájl, socket, szoftverkomponens stb.) való kapcsolatot jelent, amely képes I/O műveletek végrehajtására (olvasás vagy írás).

az egyirányú adatfolyamokhoz képest a csatornák kétirányúak.

a Buffer osztály egy rögzített méretű adattároló, amely további módszereket tartalmaz az adatok olvasására és írására. Az összes csatorna adatot Pufferen keresztül kezelik, de soha nem közvetlenül:a csatornához küldött összes adat pufferbe kerül, a csatornától kapott összes adat egy pufferbe kerül.

az adatfolyamokhoz képest, amelyek bájtorientáltak, a csatornák blokkorientáltak. A Byte-orientált I / O egyszerűbb, de néhány I / O entitás meglehetősen lassú lehet. Blokk-orientált I / O lehet sokkal gyorsabb, de bonyolultabb.

a választó osztály lehetővé teszi a feliratkozást események sok regisztrált SelectableChannel objektumok egyetlen hívást. Amikor az események megérkeznek, egy Választóobjektum elküldi őket a megfelelő eseménykezelőknek.

Java NIO2 API

a Java NIO2 API aszinkron csatornákon alapul (AsynchronousServerSocketChannel, AsynchronousSocketChannel stb.), amelyek támogatják az aszinkron I/O műveleteket (csatlakozás, olvasás vagy írás, hibakezelés).

az aszinkron csatornák két mechanizmust biztosítanak az aszinkron I/O műveletek vezérlésére. Az első mechanizmus a java visszaküldése.util.egyidejű.Future object, amely egy függőben lévő műveletet modellez, és felhasználható az állapot lekérdezésére és az eredmény megszerzésére. A második mechanizmus azáltal, hogy a művelet egy java.nio.csatornák.CompletionHandler objektum, amely meghatározza a kezelő metódusokat, amelyek a művelet befejezése vagy sikertelen végrehajtása után kerülnek végrehajtásra. A megadott API mindkét mechanizmus egyenértékű.

az aszinkron csatornák szabványos módot biztosítanak az aszinkron műveletek platformfüggetlenül történő végrehajtására. Azonban az az összeg, amelyet a Java sockets API képes kihasználni az operációs rendszer natív aszinkron képességeit, az adott platform támogatásától függ.

Socket echo server

a fent említett I/O modellek többsége itt van implementálva echo szervereken és klienseken Java Sockets API-kkal. Az echo szerverek és kliensek a következő algoritmus szerint működnek:

  1. a szerver egy regisztrált TCP porton lévő socket-et hallgat 7000
  2. a kliens egy dinamikus TCP porton lévő socket-ről csatlakozik a szerver socket-hez
  3. az ügyfél beolvassa a bemeneti karakterláncot a konzolról, és elküldi a bájtokat a socket-ből a szerver socket-be
  4. a szerver fogadja a bájtokat a socket-ből, majd visszaküldi őket az ügyfél socket-be
  5. az ügyfél megkapja a bájtokat a foglalatából, és írja a visszhangzott karakterláncot a konzolra
  6. ha az ügyfél ugyanannyi bájtot kap, mint amennyit küldött, akkor leválasztja a kiszolgálót
  7. amikor a szerver egy speciális karakterláncot kap, leállítja a hallgatást

a karakterláncok és bájtok közötti konverzió itt kifejezetten UTF-8 kódolásban történik.

További csak egyszerűsített kódokat echo szerverek állnak rendelkezésre. Az echo szerverek és kliensek teljes kódjaira mutató hivatkozás a következtetésben található.

blokkoló Io echo szerver

a következő példában a blokkoló I/O modell egy echo szerveren van implementálva Java Io API-val.

A ServerSocket.fogadja módszer blokkok, amíg a kapcsolat elfogadásra kerül. Az InputStream.olvassa el a metódusblokkokat, amíg a bemeneti adatok rendelkezésre állnak, vagy az ügyfél megszakad. Az OutputStream.írja módszer blokkokat, amíg az összes kimeneti adat meg nem íródik.

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

NIO echo szerver blokkolása

a következő példában a blokkoló I/O modell egy echo szerveren van implementálva Java NIO API-val.

a ServerSocketChannel és SocketChannel objektumok alapértelmezés szerint blokkolási módban vannak konfigurálva. A ServerSocketChannel.az accept metódus blokkokat ad vissza, és egy socketchannel objektumot ad vissza, amikor egy kapcsolat elfogadásra kerül. A ServerSocket.olvassa el a metódusblokkokat, amíg a bemeneti adatok rendelkezésre állnak, vagy az ügyfél megszakad. A ServerSocket.írja módszer blokkokat, amíg az összes kimeneti adat meg nem íródik.

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

nem blokkoló NIO echo szerver

a következő példában a nem blokkoló I/O modell Java NIO API-val rendelkező echo szerveren van megvalósítva.

a ServerSocketChannel és SocketChannel objektumok explicit módon vannak konfigurálva nem blokkoló módban. A ServerSocketChannel.az accept metódus nem blokkol, és null értéket ad vissza, ha még nincs kapcsolat elfogadva,vagy egyébként egy SocketChannel objektum. A ServerSocket.a read nem blokkol, és 0 értéket ad vissza, ha nem állnak rendelkezésre adatok, vagy pozitív számú bájt olvasható egyébként. A ServerSocket.az írási módszer nem blokkolja, ha van szabad hely a socket kimeneti pufferében.

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

NIO echo szerver Multiplexelése

a következő példában a multiplex I/O modell egy echo szerver Java NIO API-ban van megvalósítva.

az inicializálás során több, nem blokkoló módban konfigurált ServerSocketChannel objektum regisztrálva van ugyanazon a választó objektumon a SelectionKey-vel.OP_ACCEPT argumentum annak megadására, hogy a kapcsolat elfogadásának eseménye érdekes.

a fő hurok, a választó.válassza a metódusblokkok lehetőséget, amíg legalább az egyik regisztrált esemény meg nem történik. Aztán a választó.a selectedKeys metódus a SelectionKey objektumok azon halmazát adja vissza, amelyeknél események történtek. A SelectionKey objektumokon keresztül iterálva meg lehet határozni, hogy milyen I / O esemény (csatlakozás, elfogadás, olvasás, írás) történt, és mely sockets objektumok (ServerSocketChannel, SocketChannel) társultak az eseményhez.

a választógomb jelzése, hogy a csatorna készen áll valamilyen műveletre, utalás, nem garancia.

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

ha egy SelectionKey objektum azt jelzi, hogy kapcsolat elfogadási esemény történt, akkor a ServerSocketChannel lett.fogadja a hívást (amely nem blokkolható) a kapcsolat elfogadásához. Ezt követően egy új SocketChannel objektum nem blokkoló módban van konfigurálva, és ugyanazon a választó objektumon van regisztrálva a SelectionKey-vel.OP_READ argumentum annak megadására, hogy most egy olvasási esemény érdekes.

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

ha egy SelectionKey objektum azt jelzi, hogy olvasási esemény történt, akkor a SocketChannel.read call (amely lehet egy nem blokkoló) olvasni az adatokat a SocketChannel objektum egy új ByteByffer objektumot. Ezt követően a SocketChannel objektum ugyanazon a választó objektumon van regisztrálva a SelectionKey-vel.OP_WRITE argumentum annak megadására, hogy most egy írási esemény érdekes. Ezenkívül ezt a ByteBuffer objektumot a regisztráció során mellékletként használják.

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

ha egy SelectionKeys objektum azt jelzi, hogy írási esemény történt, akkor a SocketChannel lett.hívás írása (amely lehet nem blokkoló), hogy adatokat írjon a SocketChannel objektumra a Bytebyffer objektumból, amelyet a SelectionKey-ből kivontak.csatolási módszer. Ezt követően a Socketcsatornát.a cloase hívás bezárja a kapcsolatot.

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

minden olvasás vagy írás után a SelectionKey objektum eltávolításra kerül a SelectionKey objektumok halmazából az újrafelhasználás megakadályozása érdekében. De a selectionkey objektum a kapcsolat elfogadásához nem távolítható el, hogy képes legyen a következő hasonló művelet elvégzésére.

aszinkron NIO2 echo server

a következő példában az aszinkron I/O modell egy echo szerveren van implementálva Java NIO2 API-val. Az itt található AsynchronousServerSocketChannel, AsynchronousSocketChannel osztályokat a completion handlers mechanizmussal együtt használjuk.

Az AsynchronousServerSocketChannel.az accept metódus aszinkron kapcsolat-elfogadási műveletet kezdeményez.

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

amikor egy kapcsolat elfogadásra kerül (vagy a művelet sikertelen), az AcceptCompletionHandler osztályt hívják meg, amelyet az Aszinkronsocketchannel.olvas (ByteBuffer cél, mellékletet, CompletionHandler< egész,? super a> handler) metódus aszinkron olvasási műveletet kezdeményez az AsynchronousSocketChannel objektumról egy új ByteBuffer objektumra.

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

amikor az olvasási művelet befejeződik (vagy nem sikerül), a ReadCompletionHandler osztályt hívják meg, amelyet az Aszinkronsocketchannel.ír(ByteBuffer forrás, melléklet, CompletionHandler< egész szám,? super a> handler) metódus aszinkron írási műveletet kezdeményez az AsynchronousSocketChannel objektumhoz a ByteBuffer objektumból.

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

amikor az írási művelet befejeződik (vagy sikertelen), a WriteCompletionHandler osztályt hívják meg, amelyet az Aszinkronsocketchannel.a bezárási módszer bezárja a kapcsolatot.

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

ebben a példában az aszinkron I/O műveleteket csatolás nélkül hajtják végre, mert az összes szükséges objektum (AsynchronousSocketChannel, ByteBuffer) konstruktor argumentumként kerül átadásra a megfelelő befejezési kezelők számára.

következtetés

az I/O modell kiválasztása az aljzatok kommunikációjához a forgalom paramétereitől függ. Ha az I/O kérések hosszúak és ritkák, az aszinkron I / O általában jó választás. Ha azonban az I/O kérések rövidek és gyorsak, a rendszermaghívások feldolgozásának költsége sokkal jobbá teheti a szinkron I/O-t.

annak ellenére, hogy a Java szabványos módot biztosít a sockets I/O végrehajtására a különböző operációs rendszerekben, a tényleges teljesítmény jelentősen eltérhet a megvalósításuktól függően. Meg lehet kezdeni ezeket a különbségeket Dan Kegel jól ismert cikkével, a C10K problémával.

teljes kód példák állnak rendelkezésre a GitHub repository.

Vélemény, hozzászólás?

Az e-mail-címet nem tesszük közzé.