Java IO gegen NIO

1. Übersicht

Die Verarbeitung von Ein- und Ausgaben ist eine häufige Aufgabe für Java-Programmierer. In diesem Tutorial sehen wir uns die ursprünglichen java.io (IO) -Bibliotheken und die neueren java.nio (NIO) -Bibliotheken an und wie sie sich bei der Kommunikation über ein Netzwerk unterscheiden.

2. Hauptmerkmale

Beginnen wir mit den wichtigsten Funktionen beider Pakete.

2.1. IO - java.io.

Das Paket java.io wurde in Java 1.0 eingeführt , Reader in Java 1.1. Es bietet:

  • InputStream und OutputStream - liefern Daten byteweise
  • Reader and Writer - Convenience-Wrapper für die Streams
  • Blockierungsmodus - um auf eine vollständige Nachricht zu warten

2.2. NIO - java.nio

Das Paket java.nio wurde in Java 1.4 eingeführt und in Java 1.7 (NIO.2) mit erweiterten Dateivorgängen und einem ASynchronousSocketChannel aktualisiert . Es bietet:

  • Puffer - um Datenblöcke gleichzeitig zu lesen
  • CharsetDecoder - zum Zuordnen von Rohbytes zu / von lesbaren Zeichen
  • Kanal - für die Kommunikation mit der Außenwelt
  • Selector - Zum Aktivieren des Multiplexens auf einem SelectableChannel und zum Zugriff auf alle Kanäle , die für E / A bereit sind
  • Nicht blockierender Modus - um zu lesen, was bereit ist

Schauen wir uns nun an, wie wir jedes dieser Pakete verwenden, wenn wir Daten an einen Server senden oder dessen Antwort lesen.

3. Konfigurieren Sie unseren Testserver

Hier verwenden wir WireMock, um einen anderen Server zu simulieren, damit wir unsere Tests unabhängig ausführen können.

Wir werden es so konfigurieren, dass es auf unsere Anfragen wartet und uns Antworten sendet, wie es ein echter Webserver tun würde. Wir werden auch einen dynamischen Port verwenden, damit wir nicht mit Diensten auf unserem lokalen Computer in Konflikt geraten.

Lassen Sie uns die Maven Abhängigkeit für WireMock mit fügen Testumfang:

 com.github.tomakehurst wiremock-jre8 2.26.3 test 

Definieren wir in einer Testklasse eine JUnit @Rule , um WireMock an einem freien Port zu starten. Wir werden es dann so konfigurieren, dass es uns eine HTTP 200-Antwort zurückgibt, wenn wir nach einer vordefinierten Ressource mit dem Nachrichtentext als Text im JSON-Format fragen:

@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!\" }"))); }

Nachdem wir unseren Mock-Server eingerichtet haben, können wir einige Tests durchführen.

4. Blockieren von IO - java.io

Schauen wir uns an, wie das ursprüngliche blockierende E / A-Modell funktioniert, indem wir einige Daten von einer Website lesen. Wir werden einen java.net.Socket verwenden , um Zugriff auf einen der Ports des Betriebssystems zu erhalten.

4.1. Eine Anfrage senden

In diesem Beispiel erstellen wir eine GET-Anforderung zum Abrufen unserer Ressourcen. Erstellen wir zunächst einen Socket, um auf den Port zuzugreifen, den unser WireMock-Server überwacht:

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

Für die normale HTTP- oder HTTPS-Kommunikation wäre der Port 80 oder 443. In diesem Fall verwenden wir jedoch wireMockRule.port (), um auf den zuvor eingerichteten dynamischen Port zuzugreifen.

Öffnen wir nun einen OutputStream am Socket , der in einen OutputStreamWriter eingeschlossen ist, und übergeben ihn an einen PrintWriter , um unsere Nachricht zu schreiben. Und stellen wir sicher, dass wir den Puffer leeren, damit unsere Anfrage gesendet wird:

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. Warten Sie auf die Antwort

Lassen Sie sich öffnen Sie einen Inputstream auf dem Sockel die Antwort zuzugreifen, lesen Sie den Strom mit einem BufferedReader , und speichern sie in einem Stringbuilder :

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

Verwenden Sie reader.readLine () , um zu blockieren, auf eine vollständige Zeile zu warten und die Zeile dann an unseren Shop anzuhängen. Wir werden so lange lesen, bis wir eine Null erhalten, die das Ende des Streams anzeigt:

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

5. Nicht blockierende E / A - java.nio

Schauen wir uns nun an, wie das nicht blockierende E / A-Modell des nio- Pakets mit demselben Beispiel funktioniert.

Dieses Mal erstellen wir einen java.nio.channel . SocketChannel, um auf den Port auf unserem Server anstatt auf java.net.Socket zuzugreifen und ihm eine InetSocketAddress zu übergeben .

5.1. Eine Anfrage senden

Öffnen wir zunächst unseren SocketChannel :

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

Und jetzt erhalten wir einen Standard-UTF-8- Zeichensatz zum Codieren und Schreiben unserer Nachricht:

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

5.2. Lesen Sie die Antwort

Nachdem wir die Anfrage gesendet haben, können wir die Antwort im nicht blockierenden Modus mithilfe von Rohpuffern lesen.

Da wir Text verarbeiten, benötigen wir einen ByteBuffer für die Rohbytes und einen CharBuffer für die konvertierten Zeichen (unterstützt von einem CharsetDecoder ):

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

Unser CharBuffer hat noch Platz, wenn die Daten in einem Multi-Byte-Zeichensatz gesendet werden.

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.

Implementieren wir nun unsere vollständige storeBufferContents () -Methode, die in unseren Puffern CharsetDecoder und StringBuilder übergeben wird :

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

6. Fazit

In diesem Artikel haben wir gesehen, wie das ursprüngliche java.io- Modell blockiert , auf eine Anforderung wartet und Streams verwendet , um die empfangenen Daten zu bearbeiten .

Im Gegensatz dazu die java.nio erlauben Bibliotheken für nicht-blockierende Kommunikation mit Buffer s und Kanal s und einem direkten Speicherzugriff für schnellere Leistung bieten können. Mit dieser Geschwindigkeit geht jedoch die zusätzliche Komplexität der Handhabung von Puffern einher.

Wie üblich ist der Code für diesen Artikel auf GitHub verfügbar.