Översikt

Hantering av in- och utdata är vanliga uppgifter för Java-programmerare. I den här handledningen tittar vi på de ursprungliga biblioteken java.io (IO) och de nyare biblioteken java.nio (NIO) och hur de skiljer sig åt när de kommunicerar över ett nätverk.

Nyckelfunktioner

Låt oss börja med att titta på de viktigaste funktionerna i de båda paketen.

2.1. IO – java.io

Paketet java.io introducerades i Java 1.0 och Reader i Java 1.1. Det tillhandahåller:

  • InputStream och OutputStream – som tillhandahåller data en byte i taget
  • Reader och Writer – bekvämlighetsförpackningar för strömmarna
  • blockeringsläge – för att vänta på ett fullständigt meddelande

2.2. NIO – java.nio

Paketet java.nio introducerades i Java 1.4 och uppdaterades i Java 1.7 (NIO.2) med förbättrade filoperationer och en ASynchronousSocketChannel. Det tillhandahåller:

  • Buffer – för att läsa datamängder åt gången
  • CharsetDecoder – för att mappa råa bytes till/från läsbara tecken
  • Channel – för att kommunicera med omvärlden
  • Selector – för att aktivera multiplexering på en SelectableChannel och ge åtkomst till alla Channels som är redo för I/O
  • non-blockerande läge – för att läsa allt som är redo

Nu ska vi ta en titt på hur vi använder vart och ett av dessa paket när vi skickar data till en server eller läser dess svar.

Konfigurera vår testserver

Här använder vi WireMock för att simulera en annan server så att vi kan köra våra tester oberoende av varandra.

Vi konfigurerar den så att den lyssnar på våra förfrågningar och skickar oss svar precis som en riktig webbserver gör. Vi kommer också att använda en dynamisk port så att vi inte hamnar i konflikt med några tjänster på vår lokala maskin.

Låt oss lägga till Maven-beroendet för WireMock med testomfång:

<dependency> <groupId>com.github.tomakehurst</groupId> <artifactId>wiremock-jre8</artifactId> <version>2.26.3</version> <scope>test</scope></dependency>

I en testklass ska vi definiera en JUnit @Rule för att starta WireMock på en fri port. Vi konfigurerar den sedan så att den returnerar ett HTTP 200-svar till oss när vi frågar efter en fördefinierad resurs, med meddelandekroppen som en text i JSON-format:

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

Nu när vi har konfigurerat vår mock-server är vi redo att köra några tester.

Blockering av IO – java.io

Låtsas vi titta på hur den ursprungliga blockerande IO-modellen fungerar genom att läsa lite data från en webbplats. Vi använder en java.net.Socket för att få tillgång till en av operativsystemets portar.

4.1. Skicka en begäran

I det här exemplet skapar vi en GET-förfrågan för att hämta våra resurser. Först skapar vi en socket för att komma åt den port som vår WireMock-server lyssnar på:

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

För normal HTTP- eller HTTPS-kommunikation är porten 80 eller 443. Men i det här fallet använder vi wireMockRule.port() för att komma åt den dynamiska port som vi ställde in tidigare.

Nu öppnar vi en OutputStream på sockeln, inlindad i en OutputStreamWriter och skickar den till en PrintWriter för att skriva vårt meddelande. Och låt oss se till att vi spolar bufferten så att vår begäran skickas:

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. Vänta på svaret

Låt oss öppna en InputStream på sockeln för att få tillgång till svaret, läsa strömmen med en BufferedReader och lagra den i en StringBuilder:

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

Låt oss använda reader.readLine() för att blockera, vänta på en komplett rad och sedan lägga till raden i vårt lager. Vi fortsätter att läsa tills vi får en null, vilket indikerar slutet på strömmen:

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

Non-Blocking IO – java.nio

Nu ska vi titta på hur nio-paketets non-blocking IO-modell fungerar med samma exempel.

Den här gången skapar vi en java.nio.channel.SocketChannel för att få tillgång till porten på vår server istället för en java.net.Socket, och ger den en InetSocketAddress.

5.1. Skicka en begäran

Först öppnar vi vår SocketChannel:

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

Och nu skaffar vi en standard UTF-8 Charset för att koda och skriva vårt meddelande:

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

5.2. Läs svaret

När vi har skickat begäran kan vi läsa svaret i icke-blockerande läge, med hjälp av råbuffertar.

Då vi kommer att behandla text behöver vi en ByteBuffer för de råa bytena och en CharBuffer för de konverterade tecknen (med hjälp av en CharsetDecoder):

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

Vår CharBuffer kommer att ha utrymme över om datan skickas med en teckenuppsättning med flera byte.

Notera att om vi behöver särskilt snabb prestanda kan vi skapa en MappedByteBuffer i inhemskt minne med hjälp av ByteBuffer.allocateDirect(). I vårt fall är det dock tillräckligt snabbt att använda allocate() från standardheap.

När vi hanterar buffertar måste vi veta hur stor bufferten är (kapaciteten), var vi befinner oss i bufferten (den aktuella positionen) och hur långt vi kan gå (gränsen).

Så, låt oss läsa från vår SocketChannel, och skicka över vår ByteBuffer till den för att lagra våra data. Vår läsning från SocketChannel kommer att avslutas med vår ByteBuffers aktuella position satt till nästa byte att skriva till (precis efter den senast skrivna byten), men med gränsen oförändrad:

socketChannel.read(byteBuffer)

Vår SocketChannel.read() returnerar antalet lästa bytes som kunde skrivas in i vår buffert. Detta blir -1 om sockeln har kopplats bort.

När vår buffert inte har något utrymme kvar eftersom vi inte har bearbetat all data ännu, kommer SocketChannel.read() att återge noll lästa bytes men vår buffer.position() kommer fortfarande att vara större än noll.

För att se till att vi börjar läsa från rätt plats i bufferten använder vi Buffer.flip() för att sätta vår ByteBuffers nuvarande position till noll och dess gräns till den sista byte som skrevs av SocketChannel. Vi sparar sedan buffertinnehållet med hjälp av vår metod storeBufferContents, som vi kommer att titta på senare. Slutligen använder vi buffer.compact() för att komprimera bufferten och ställa in den aktuella positionen för nästa läsning från SocketChannel.

Då våra data kan komma att anlända i delar, ska vi linda in vår kod för buffertläsning i en slinga med avslutningsvillkor för att kontrollera om vår socket fortfarande är ansluten eller om vi har blivit frånkopplade men fortfarande har data kvar i vår buffert:

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

Och vi får inte glömma att stänga() vår socket (såvida vi inte öppnade den i ett try-with-resources-block):

socketChannel.close();

5.3. Lagra data från vår buffert

Svaret från servern kommer att innehålla rubriker, vilket kan göra att datamängden överstiger storleken på vår buffert. Därför använder vi en StringBuilder för att bygga upp vårt kompletta meddelande när det anländer.

För att lagra vårt meddelande avkodar vi först de råa bytena till tecken i vår CharBuffer. Sedan vänder vi på pekarna så att vi kan läsa våra teckendata och lägga till dem i vår expanderbara StringBuilder. Slutligen rensar vi CharBuffer och gör den redo för nästa skriv- och läscykel.

Så nu ska vi implementera vår kompletta storeBufferContents()-metod genom att skicka in våra buffertar, CharsetDecoder och StringBuilder:

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

Conclusion

I den här artikeln har vi sett hur den ursprungliga java.io-modellen blockerar, väntar på en begäran och använder Streams för att manipulera de data som tas emot.

Däremot tillåter java.nio-biblioteken icke-blockerande kommunikation med hjälp av Buffers och Channels och kan ge direkt tillgång till minnet för snabbare prestanda. Med denna snabbhet följer dock ytterligare komplexitet i hanteringen av buffertar.

Som vanligt finns koden för den här artikeln tillgänglig på GitHub.

Kom igång med Spring 5 och Spring Boot 2 genom kursen Lär dig Spring:

>> CHECK OUT THE COURSE

.

Lämna ett svar

Din e-postadress kommer inte publiceras.