Java IO vs NIO

1. Prezentare generală

Gestionarea intrărilor și ieșirilor sunt sarcini obișnuite pentru programatorii Java. În acest tutorial, vom analiza bibliotecile originale java.io (IO) și cele mai noi biblioteci java.nio (NIO) și modul în care acestea diferă atunci când comunică într-o rețea.

2. Caracteristici cheie

Să începem prin a ne uita la caracteristicile cheie ale ambelor pachete.

2.1. IO - java.io

Java.io Pachetul a fost introdus în Java 1.0 , cu Reader introdus în Java 1.1. Oferă:

  • InputStream și OutputStream - care furnizează date câte un octet pe rând
  • Reader și Writer - ambalaje convenționale pentru fluxuri
  • modul de blocare - pentru a aștepta un mesaj complet

2.2. NIO - java.nio

Java.nio Pachetul a fost introdus în Java 1.4 și actualizate în Java 1.7 (NIO.2) cu operațiunile de fișiere îmbunătățite și un ASynchronousSocketChannel . Oferă:

  • Buffer - pentru a citi bucăți de date la un moment dat
  • CharsetDecoder - pentru maparea octeților bruti la / de la caractere lizibile
  • Canal - pentru comunicarea cu lumea exterioară
  • Selector - pentru a activa multiplexarea pe un SelectableChannel și pentru a oferi acces la orice canal care este pregătit pentru I / O
  • modul fără blocare - pentru a citi orice este gata

Acum să aruncăm o privire la modul în care folosim fiecare dintre aceste pachete atunci când trimitem date către un server sau citim răspunsul acestuia.

3. Configurați serverul nostru de testare

Aici vom folosi WireMock pentru a simula un alt server, astfel încât să putem rula testele noastre în mod independent.

Îl vom configura pentru a ne asculta cererile și pentru a ne trimite răspunsuri la fel ca un server web real. De asemenea, vom folosi un port dinamic, astfel încât să nu intrăm în conflict cu niciun serviciu de pe mașina noastră locală.

Să adăugăm dependența Maven pentru WireMock cu domeniul de testare :

 com.github.tomakehurst wiremock-jre8 2.26.3 test 

Într-o clasă de testare, să definim un JUnit @Rule pentru a porni WireMock up pe un port liber. Îl vom configura apoi pentru a ne întoarce un răspuns HTTP 200 când cerem o resursă predefinită, cu corpul mesajului ca text în format JSON:

@Rule public WireMockRule wireMockRule = new WireMockRule(wireMockConfig().dynamicPort()); private String REQUESTED_RESOURCE = "/test.json"; @Before public void setup() { stubFor(get(urlEqualTo(REQUESTED_RESOURCE)) .willReturn(aResponse() .withStatus(200) .withBody("{ \"response\" : \"It worked!\" }"))); }

Acum, că am configurat serverul nostru fals, suntem gata să executăm câteva teste.

4. Blocarea IO - java.io

Să vedem cum funcționează modelul IO de blocare original citind unele date de pe un site web. Vom folosi un java.net.Socket pentru a avea acces la unul dintre porturile sistemului de operare.

4.1. Trimite o cerere

În acest exemplu, vom crea o solicitare GET pentru a ne recupera resursele. În primul rând, să creăm un soclu pentru a accesa portul pe care ascultă serverul nostru WireMock:

Socket socket = new Socket("localhost", wireMockRule.port())

Pentru comunicațiile HTTP sau HTTPS normale, portul ar fi 80 sau 443. Cu toate acestea, în acest caz, folosim wireMockRule.port () pentru a accesa portul dinamic pe care l-am configurat mai devreme.

Acum să deschidem un OutputStream pe soclu , înfășurat într-un OutputStreamWriter și să-l transmitem unui PrintWriter pentru a scrie mesajul nostru. Și să ne asigurăm că spălăm tamponul astfel încât solicitarea noastră să fie trimisă:

OutputStream clientOutput = socket.getOutputStream(); PrintWriter writer = new PrintWriter(new OutputStreamWriter(clientOutput)); writer.print("GET " + TEST_JSON + " HTTP/1.0\r\n\r\n"); writer.flush();

4.2. Așteptați răspunsul

deschidem un InputStream pe soclu pentru a accesa răspunsul, să citim fluxul cu un BufferedReader și să-l stocăm într-un StringBuilder :

InputStream serverInput = socket.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(serverInput)); StringBuilder ourStore = new StringBuilder();

Să folosim reader.readLine () pentru a bloca, așteptând o linie completă, apoi adăugăm linia la magazinul nostru. Vom continua să citim până când vom obține un nul, care indică sfârșitul fluxului:

for (String line; (line = reader.readLine()) != null;) { ourStore.append(line); ourStore.append(System.lineSeparator()); }

5. IO non-blocant - java.nio

Acum, să vedem cum funcționează modelul IO non-blocant al pachetului nio cu același exemplu.

De data aceasta, vom crea un java.nio.channel . SocketChannel pentru a accesa portul de pe serverul nostru în locul unui java.net.Socket și pentru a-i transmite un InetSocketAddress .

5.1. Trimite o cerere

Mai întâi, să deschidem SocketChannel :

InetSocketAddress address = new InetSocketAddress("localhost", wireMockRule.port()); SocketChannel socketChannel = SocketChannel.open(address);

Și acum, să obținem un set de caractere UTF-8 standard pentru a codifica și a scrie mesajul nostru:

Charset charset = StandardCharsets.UTF_8; socket.write(charset.encode(CharBuffer.wrap("GET " + REQUESTED_RESOURCE + " HTTP/1.0\r\n\r\n")));

5.2. Citiți răspunsul

După ce trimitem solicitarea, putem citi răspunsul în modul fără blocare, folosind buffere brute.

Deoarece vom prelucra text, vom avea nevoie de un ByteBuffer pentru octeții bruti și un CharBuffer pentru caracterele convertite (ajutat de un CharsetDecoder ):

ByteBuffer byteBuffer = ByteBuffer.allocate(8192); CharsetDecoder charsetDecoder = charset.newDecoder(); CharBuffer charBuffer = CharBuffer.allocate(8192);

CharBuffer-ul nostru va avea spațiu rămas dacă datele sunt trimise într-un set de caractere multi-octet.

Note that if we need especially fast performance, we can create a MappedByteBuffer in native memory using ByteBuffer.allocateDirect(). However, in our case, using allocate() from the standard heap is fast enough.

When dealing with buffers, we need to know how big the buffer is (the capacity), where we are in the buffer (the current position), and how far we can go (the limit).

So, let's read from our SocketChannel, passing it our ByteBuffer to store our data. Our read from the SocketChannel will finish with our ByteBuffer‘s current position set to the next byte to write to (just after the last byte written), but with its limit unchanged:

socketChannel.read(byteBuffer)

Our SocketChannel.read() returns the number of bytes read that could be written into our buffer. This will be -1 if the socket was disconnected.

When our buffer doesn't have any space left because we haven't processed all its data yet, then SocketChannel.read() will return zero bytes read but our buffer.position() will still be greater than zero.

To make sure that we start reading from the right place in the buffer, we'll use Buffer.flip() to set our ByteBuffer‘s current position to zero and its limit to the last byte that was written by the SocketChannel. We'll then save the buffer contents using our storeBufferContents method, which we'll look at later. Lastly, we'll use buffer.compact() to compact the buffer and set the current position ready for our next read from the SocketChannel.

Since our data may arrive in parts, let's wrap our buffer-reading code in a loop with termination conditions to check if our socket is still connected or if we've been disconnected but still have data left in our buffer:

while (socketChannel.read(byteBuffer) != -1 || byteBuffer.position() > 0) { byteBuffer.flip(); storeBufferContents(byteBuffer, charBuffer, charsetDecoder, ourStore); byteBuffer.compact(); }

And let's not forget to close() our socket (unless we opened it in a try-with-resources block):

socketChannel.close();

5.3. Storing Data From Our Buffer

The response from the server will contain headers, which may make the amount of data exceed the size of our buffer. So, we'll use a StringBuilder to build our complete message as it arrives.

To store our message, we first decode the raw bytes into characters in our CharBuffer. Then we'll flip the pointers so that we can read our character data, and append it to our expandable StringBuilder. Lastly, we'll clear the CharBuffer ready for the next write/read cycle.

Deci, acum, să implementăm metoda noastră completă storeBufferContents () trecând în tampoanele noastre, CharsetDecoder și StringBuilder :

void storeBufferContents(ByteBuffer byteBuffer, CharBuffer charBuffer, CharsetDecoder charsetDecoder, StringBuilder ourStore) { charsetDecoder.decode(byteBuffer, charBuffer, true); charBuffer.flip(); ourStore.append(charBuffer); charBuffer.clear(); }

6. Concluzie

În acest articol, am văzut cum se blochează modelul original java.io , așteaptă o cerere și folosește Stream s pentru a manipula datele primite.

În contrast, cele java.nio bibliotecile permit non-blocare de comunicare folosind tampon S și Canalul s și poate oferi acces direct la memorie pentru performanțe mai rapide. Cu toate acestea, cu această viteză vine complexitatea suplimentară a manipulării bufferelor.

Ca de obicei, codul pentru acest articol este disponibil pe GitHub.