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
- I/O modellek
- blokkoló I/O modell
- nem blokkoló I / O modell
- I/O multiplexelési modell
- Jelvezérelt I/O modell
- aszinkron I/O modell
- Java I/O API-k
- Java NIO API
- Java NIO2 API
- Socket echo server
- blokkoló Io echo szerver
- NIO echo szerver blokkolása
- nem blokkoló NIO echo szerver
- NIO echo szerver Multiplexelése
- aszinkron NIO2 echo server
- következtetés
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.
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.
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.
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.
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.
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:
- a szerver egy regisztrált TCP porton lévő socket-et hallgat 7000
- a kliens egy dinamikus TCP porton lévő socket-ről csatlakozik a szerver socket-hez
- 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
- a szerver fogadja a bájtokat a socket-ből, majd visszaküldi őket az ügyfél socket-be
- az ügyfél megkapja a bájtokat a foglalatából, és írja a visszhangzott karakterláncot a konzolra
- ha az ügyfél ugyanannyi bájtot kap, mint amennyit küldött, akkor leválasztja a kiszolgálót
- 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.