Java sockets I / O: blocare, non-blocare și asincron

fotografie de Evi Radauscher pe Unsplash

la descrierea I/O, termenii non-blocanți și asincroni sunt adesea folosiți interschimbabil, dar există o diferență semnificativă între ei. În acest articol sunt descrise diferențele teoretice și practice dintre non-blocare și asincron prize i/o operațiuni în Java.

soclurile sunt puncte finale pentru a efectua comunicații bidirecționale prin protocoalele TCP și UDP. API-urile Java sockets sunt adaptoare pentru funcționalitatea corespunzătoare a sistemelor de operare. Comunicare Sockets în sisteme de operare compatibile POSIX (Unix, Linux, Mac OS X, BSD, Solaris, AIX etc.) este realizată de Berkeley sockets. Comunicarea Sockets în Windows este realizată de Winsock, care se bazează și pe sockets Berkeley cu funcționalități suplimentare pentru a se conforma modelului de programare Windows.

definițiile POSIX

din acest articol sunt utilizate definiții simplificate din specificația POSIX.

fir blocat — un fir care așteaptă o anumită condiție înainte de a putea continua execuția.

blocare — o proprietate a unui soclu care determină apelurile către acesta să aștepte efectuarea acțiunii solicitate înainte de a reveni.

non-blocare — o proprietate a unui soclu care determină apelurile către acesta să se întoarcă fără întârziere, atunci când se detectează că acțiunea solicitată nu poate fi finalizată fără o întârziere necunoscută.

funcționare I/O sincronă — o operație I/O care determină blocarea firului solicitant până la finalizarea operației I/O.

operație i/o asincronă — o operație I/O care nu determină blocarea firului solicitant; aceasta implică faptul că firul și operația I/o pot rula simultan.

deci, conform specificației POSIX, diferența dintre termenii care nu blochează și asincron este evidentă:

  • non-blocare — o proprietate a unui soclu care face ca apelurile către acesta să se întoarcă fără întârziere
  • proprietate i/o asincronă pe o operație I/o (citire sau scriere) care rulează concomitent cu firul solicitant

modele I/O

următoarele modele I/O sunt cele mai frecvente pentru sisteme:

  • blocare I/o model
  • non-blocare I/o model
  • i/o multiplexare model
  • semnal-driven I/o model
  • asincron I/o model

blocare i/o model

în modelul i/o de blocare, aplicația efectuează un apel de sistem de blocare până când datele sunt primite la kernel și sunt copiate din spațiul kernel-ului în spațiul utilizatorului.

blocarea modelului I/o

blocarea modelului I / O

Pro:

  • cel mai simplu model I / O de implementat

contra:

  • cererea este blocat

non-blocare I/O model

în non-blocare I/o Model aplicația face un apel de sistem care returnează imediat unul din cele două răspunsuri:

  • dacă operația I/O poate fi finalizată imediat, datele sunt returnate
  • dacă operația I/o nu poate fi finalizată imediat, este returnat un cod de eroare care indică faptul că operația I/o s-ar bloca sau dispozitivul este temporar indisponibil

pentru a finaliza operația I/O, aplicația trebuie să aștepte ocupat (efectuați apeluri de sistem repetate) până la finalizare.

modelul I / O care nu blochează

 modelul I/O care nu blochează

Pro:

  • cererea nu este blocat

contra:

  • cererea ar trebui să ocupat-așteptați până la finalizare, care ar provoca multe switch-uri context utilizator-kernel
  • acest model poate introduce i/o latență, deoarece poate exista un decalaj între disponibilitatea datelor în kernel și citirea datelor de către aplicația

i/o multiplexare model

în I/O multiplexare model (de asemenea, cunoscut sub numele non-blocare i/o Model cu notificări de blocare), aplicația face un apel de blocare selectați Sistem pentru a începe să monitorizeze activitatea pe mai multe descriptori. Pentru fiecare descriptor, este posibil să solicitați Notificarea disponibilității sale pentru anumite operații I/O (conexiune, citire sau scriere, apariția erorilor etc.). Când apelul select system returnează că cel puțin un descriptor este gata, aplicația efectuează un apel care nu blochează și copiază datele din spațiul kernel-ului în spațiul utilizatorului.

model de multiplexare I/O

 model de multiplexare I/O

Pro:

  • este posibil să efectuați operații I/O pe mai mulți descriptori într-un fir

contra:

  • aplicația este încă blocată la selectarea apelului de sistem
  • nu toate sistemele de operare acceptă eficient acest model

model I/O condus de semnal

în modelul i/o condus de semnal, aplicația efectuează un apel fără blocare și înregistrează un handler de semnal. Când un descriptor este gata pentru o operație I/O, este generat un semnal pentru aplicație. Apoi, handlerul de semnal copiază datele din spațiul kernel-ului în spațiul utilizatorului.

semnal-driven I/o model

semnal-driven I/o model

Pro:

  • cererea nu este blocat
  • semnalele pot oferi performanțe bune

contra:

  • nu toate sistemele de operare acceptă semnale

asincron I/o model

în modelul asincron i/o (cunoscut și sub numele de modelul i/o suprapus) aplicația face apelul fără blocare și începe o operație de fundal în kernel. Când operația este finalizată (datele sunt primite la kernel și sunt copiate din spațiul kernel-ului în spațiul utilizatorului), este generat un apel invers de finalizare pentru a termina operația I/O.

o diferență între modelul i/o asincron și modelul i/o condus de semnal este că, cu I/O condus de semnal, nucleul spune aplicației când poate fi inițiată o operație I/o, dar cu modelul i/o asincron, nucleul spune aplicației când o operație I/o este finalizată.

model I/O asincron

model I/O asincron

Pro:

  • cererea nu este blocat
  • acest model poate oferi cea mai bună performanță

contra:

  • cel mai complicat model I/O de implementat
  • nu toate sistemele de operare acceptă acest model eficient

API-uri Java i/o

api java IO se bazează pe fluxuri (InputStream, OutputStream) care reprezintă blocarea fluxului de date unidirecțional.

Java NIO API

Java NIO API se bazează pe clasele de canale, tampon, Selector, care sunt adaptoare la operațiunile I/O de nivel scăzut ale sistemelor de operare.

clasa de canale reprezintă o conexiune la o entitate (dispozitiv hardware, fișier, soclu, componentă software etc.) care este capabilă să efectueze operații I/O (citire sau scriere).

în comparație cu fluxurile uni-direcționale, canalele sunt bidirecționale.

clasa tampon este un container de date cu dimensiuni fixe, cu metode suplimentare de citire și scriere a datelor. Toate datele canalului sunt gestionate prin Buffer, dar niciodată direct: toate datele trimise către un canal sunt scrise într-un Buffer, toate datele primite de la un canal sunt citite într-un Buffer.

în comparație cu fluxurile, care sunt orientate pe octeți, canalele sunt orientate pe blocuri. I / o orientat pe octeți este mai simplu, dar pentru unele entități I/O poate fi destul de lent. I / o orientat spre bloc poate fi mult mai rapid, dar este mai complicat.

clasa Selector permite abonarea la evenimente din mai multe obiecte SelectableChannel înregistrate într-un singur apel. Când sosesc evenimente, un obiect Selector le expediază către gestionarii de evenimente corespunzători.

Java NIO2 API

Java NIO2 API se bazează pe canale asincrone (asynchronousserversocketchannel, AsynchronousSocketChannel, etc) care acceptă operațiuni I/O asincrone (conectare, citire sau scriere, manipularea erorilor).

canalele asincrone oferă două mecanisme pentru a controla operațiile I/O asincrone. Primul mecanism este prin returnarea unui java.util.concurentă.Obiectul viitor, care modelează o operație în așteptare și poate fi folosit pentru a interoga starea și a obține rezultatul. Al doilea mecanism este prin trecerea la operație a java.nio.canale.Obiect CompletionHandler, care definește metodele de manipulare care sunt executate după ce operațiunea a finalizat sau nu a reușit. API-ul furnizat pentru ambele mecanisme este echivalent.

canalele asincrone oferă o modalitate standard de a efectua operațiuni asincrone independent de platformă. Cu toate acestea, suma pe care java sockets API poate exploata capacitățile asincrone native ale unui sistem de operare, va depinde de suportul pentru acea platformă.

socket echo server

majoritatea modelelor i/O menționate mai sus sunt implementate aici în serverele echo și clienții cu API-uri Java sockets. Serverele și clienții echo funcționează după următorul algoritm:

  1. un server ascultă un soclu pe un port TCP înregistrat 7000
  2. un client se conectează de la un soclu pe un port TCP dinamic la soclul serverului
  3. clientul citește un șir de intrare din consolă și trimite octeții din soclul său la soclul serverului
  4. serverul primește octeții din soclul său și îi trimite înapoi la soclul clientului
  5. clientul primește octeții din socket-ul său și scrie șirul ecou pe consola
  6. când Clientul primește același număr de octeți pe care l-a trimis, se deconectează de la server
  7. când serverul primește un șir special, acesta nu mai ascultă

conversia dintre șiruri și octeți aici este efectuată Explicit în codificarea UTF-8.

sunt furnizate numai coduri simplificate pentru serverele echo. Legătura cu codurile complete pentru serverele și clienții echo este furnizată în concluzie.

blocarea serverului IO echo

în exemplul următor, modelul i/o de blocare este implementat într-un server echo cu API Java IO.

ServerSocket.acceptați blocuri de metode până când este acceptată o conexiune. Fluxul De Intrare.citiți blocurile de metode până când datele de intrare sunt disponibile sau un client este deconectat. Fluxul De Ieșire.scrieți blocuri de metode până când toate datele de ieșire sunt scrise.

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

blocarea serverului NIO echo

în exemplul următor, modelul i/o de blocare este implementat într-un server echo cu API Java Nio.

obiectele ServerSocketChannel și SocketChannel sunt configurate implicit în modul de blocare. ServerSocketChannel.acceptați blocuri de metodă și returnează un obiect SocketChannel atunci când este acceptată o conexiune. ServerSocket.citiți blocurile de metode până când datele de intrare sunt disponibile sau un client este deconectat. ServerSocket.scrieți blocuri de metode până când toate datele de ieșire sunt scrise.

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

non-blocare NIO echo server

în exemplul următor, modelul i/o non-blocare este implementat într-un server echo cu Java NIO API.

obiectele ServerSocketChannel și SocketChannel sunt configurate în mod explicit în modul non-blocare. ServerSocketChannel.metoda accept nu blochează și returnează null dacă nu este acceptată încă nicio conexiune sau un obiect SocketChannel altfel. ServerSocket.read nu blochează și returnează 0 dacă nu sunt disponibile date sau un număr pozitiv de octeți citiți altfel. ServerSocket.metoda de scriere nu se blochează dacă există spațiu liber în tamponul de ieșire al soclului.

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

multiplexare NIO echo server

în exemplul următor, modelul i/o multiplexare este implementat într-un server Echo Java NIO API.

în timpul inițializării, mai multe obiecte ServerSocketChannel, care sunt configurate în modul non-blocare, sunt înregistrate pe același obiect Selector cu SelectionKey.Op_accept argument pentru a specifica faptul că un eveniment de acceptare a conexiunii este interesant.

în bucla principală, selectorul.selectați blocuri de metode până când apare cel puțin unul dintre evenimentele înregistrate. Apoi selectorul.metoda selectedKeys returnează un set de obiecte SelectionKey pentru care au avut loc evenimente. Iterând prin obiectele SelectionKey, este posibil să se determine ce eveniment I/O (conectare, acceptare, citire, scriere) s-a întâmplat și ce obiecte sockets (ServerSocketChannel, SocketChannel) au fost asociate cu acel eveniment.

indicarea unei taste de selecție că un canal este pregătit pentru o anumită operație este un indiciu, nu o garanție.

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

când un obiect SelectionKey indică faptul că un eveniment de acceptare a conexiunii sa întâmplat, se face ServerSocketChannel.accepta apel (care poate fi un non-blocare) pentru a accepta conexiunea. După aceea, un nou obiect SocketChannel este configurat în modul non-blocare și este înregistrat pe același obiect Selector cu tasta de selecție.Op_read argument pentru a specifica faptul că acum un eveniment de lectură este interesant.

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

atunci când un obiect SelectionKey indică faptul că un eveniment de lectură sa întâmplat, este făcut un SocketChannel.citește apel (care poate fi un non-blocare) pentru a citi datele din obiectul SocketChannel într-un nou obiect bytebyffer. După aceea, obiectul SocketChannel este înregistrat pe același obiect Selector cu tasta de selecție.Op_write argument pentru a specifica faptul că acum un eveniment de scriere este interesant. În plus, acest obiect ByteBuffer este utilizat în timpul înregistrării ca atașament.

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

când un obiect SelectionKeys indică faptul că un eveniment de scriere sa întâmplat, este făcut SocketChannel.scrie apel (care poate fi un non-blocare) pentru a scrie date la obiectul SocketChannel din obiectul ByteByffer, extras din SelectionKey.metoda de atașare. După aceea, Prizacanal.close apel închide conexiunea.

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

după fiecare citire sau scriere obiectul SelectionKey este eliminat din setul de obiecte SelectionKey pentru a preveni reutilizarea acestuia. Dar obiectul SelectionKey pentru acceptarea conexiunii nu este eliminat pentru a avea capacitatea de a efectua următoarea operație similară.

asincron nio2 echo server

în exemplul următor, modelul i/O asincron este implementat într-un server echo cu API Java Nio2. Clasele AsynchronousServerSocketChannel, asynchronoussocketchannel sunt utilizate aici cu mecanismul de manipulare a finalizării.

AsynchronousServerSocketChannel.metoda accept inițiază o operație de acceptare a conexiunii asincrone.

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

când este acceptată o conexiune (sau operația eșuează), se numește clasa AcceptCompletionHandler, care prin AsynchronousSocketChannel.citit(destinație ByteBuffer, un atașament,CompletionHandler <întreg,? super a > handler) metoda inițiază o operație de citire asincronă de la obiectul Asincronsocketchannel la un nou obiect 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
}
}

când operația de citire se finalizează (sau eșuează), se numește clasa ReadCompletionHandler, care prin AsynchronousSocketChannel.scrie (sursa ByteBuffer, un atașament,CompletionHandler <întreg,? super a > handler) metoda inițiază o operație de scriere asincronă la obiectul Asincronsocketchannel din obiectul 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
}
}

când operația de scriere se finalizează (sau eșuează), se numește clasa WriteCompletionHandler, care prin AsynchronousSocketChannel.metoda de închidere închide conexiunea.

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

în acest exemplu, operațiile I/O asincrone sunt efectuate fără atașare, deoarece toate obiectele necesare (AsynchronousSocketChannel, ByteBuffer) sunt transmise ca argumente constructor pentru manipulatorii de finalizare corespunzători.

concluzie

alegerea modelului I/O pentru comunicarea prizelor depinde de parametrii traficului. Dacă solicitările I/O sunt lungi și rare, i / o asincron este, în general, o alegere bună. Cu toate acestea, dacă cererile de I/O sunt scurte și rapide, cheltuielile generale ale procesării apelurilor de kernel pot îmbunătăți I/o sincron.

în ciuda faptului că Java oferă un mod standard de a efectua prize I/O în diferite sisteme de operare, performanța reală poate varia semnificativ în funcție de punerea lor în aplicare. Este posibil să începeți să studiați aceste diferențe cu binecunoscutul articol al lui Dan Kegel problema C10K.

exemple complete de cod sunt disponibile în depozitul GitHub.

Lasă un răspuns

Adresa ta de email nu va fi publicată.