- Přehled
- Klíčové vlastnosti
- 2.1. Jaké jsou klíčové vlastnosti obou balíků? IO – java.io
- 2.2. NIO – java.nio
- Konfigurace našeho testovacího serveru
- Blokující IO – java.io
- 4.1. Odeslání požadavku
- 4.2. Čekání na odpověď
- Neblokující IO – java.nio
- 5.1. Odeslání požadavku
- 5.2. Čtení odpovědi
- 5.3. Jaké jsou výsledky? Ukládání dat z naší vyrovnávací paměti
- Závěr
- Začněte se Spring 5 a Spring Boot 2 prostřednictvím kurzu Learn Spring:
Přehled
Zpracování vstupů a výstupů je pro programátory v Javě běžnou úlohou. V tomto kurzu se podíváme na původní knihovny java.io (IO) a novější knihovny java.nio (NIO) a na to, jak se liší při komunikaci po síti.
Klíčové vlastnosti
Začneme tím, že se podíváme na klíčové vlastnosti obou balíků.
2.1. Jaké jsou klíčové vlastnosti obou balíků? IO – java.io
Balík java.io byl zaveden v Javě 1.0, přičemž Reader byl představen v Javě 1.1. Poskytuje:
- InputStream a OutputStream – které poskytují data po jednom bajtu
- Reader a Writer – komfortní obaly pro proudy
- blokovací režim – čekání na kompletní zprávu
2.2. NIO – java.nio
Balíček java.nio byl zaveden v Javě 1.4 a aktualizován v Javě 1.7 (NIO.2) o rozšířené souborové operace a ASynchronousSocketChannel. Poskytuje:
- Buffer – pro čtení kusů dat najednou
- CharsetDecoder – pro mapování surových bajtů na/z čitelných znaků
- Channel – pro komunikaci s vnějším světem
- Selector – pro umožnění multiplexování na SelectableChannel a zajištění přístupu ke všem Channelům, které jsou připraveny pro I/O
- ne-blocking mode – pro čtení všeho, co je připraveno
Nyní se podíváme na to, jak používáme každý z těchto balíčků, když posíláme data na server nebo čteme jeho odpověď.
Konfigurace našeho testovacího serveru
Pomocí WireMocku budeme simulovat jiný server, abychom mohli nezávisle provádět naše testy.
Nakonfigurujeme jej tak, aby naslouchal našim požadavkům a posílal nám odpovědi stejně jako skutečný webový server. Použijeme také dynamický port, abychom se nedostali do konfliktu s žádnou službou na našem lokálním počítači.
Přidáme závislost Maven pro WireMock s rozsahem testů:
<dependency> <groupId>com.github.tomakehurst</groupId> <artifactId>wiremock-jre8</artifactId> <version>2.26.3</version> <scope>test</scope></dependency>
V testovací třídě definujme pravidlo JUnit @Rule, které spustí WireMock na volném portu. Poté jej nakonfigurujeme tak, aby nám vrátil odpověď HTTP 200, když se zeptáme na předem definovaný zdroj, přičemž tělem zprávy bude nějaký text ve formátu JSON:
@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!\" }")));}
Teď, když máme nastavený náš mock server, jsme připraveni spustit nějaké testy.
Blokující IO – java.io
Podívejme se, jak funguje původní model blokujícího IO čtením nějakých dat z webové stránky. Pro získání přístupu k jednomu z portů operačního systému použijeme java.net.Socket.
4.1. Odeslání požadavku
V tomto příkladu vytvoříme požadavek GET pro získání našich zdrojů. Nejprve vytvoříme soket pro přístup k portu, na kterém naslouchá náš server WireMock:
Socket socket = new Socket("localhost", wireMockRule.port())
Pro běžnou komunikaci HTTP nebo HTTPS by tento port byl 80 nebo 443. V případě, že by server WireMock naslouchal, vytvořil bychom soket. V tomto případě však použijeme funkci wireMockRule.port() pro přístup k dynamickému portu, který jsme nastavili dříve.
Nyní otevřeme na socketu OutputStream zabalený do OutputStreamWriter a předáme jej PrintWriter pro zápis naší zprávy. A ujistěme se, že jsme propláchli vyrovnávací paměť, aby byl náš požadavek odeslán:
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. Čekání na odpověď
Otevřeme na socketu InputStream pro přístup k odpovědi, přečteme proud pomocí BufferedReader a uložíme jej do StringBuilder:
InputStream serverInput = socket.getInputStream();BufferedReader reader = new BufferedReader(new InputStreamReader(serverInput));StringBuilder ourStore = new StringBuilder();
Pomocí reader.readLine() zablokujeme a počkáme na celý řádek, který pak připojíme do našeho úložiště. Budeme číst, dokud nedostaneme nulu, která označuje konec proudu:
for (String line; (line = reader.readLine()) != null;) { ourStore.append(line); ourStore.append(System.lineSeparator());}
Neblokující IO – java.nio
Nyní se na stejném příkladu podíváme, jak funguje neblokující model IO balíku nio.
Tentokrát vytvoříme pro přístup k portu na našem serveru místo java.net.Socket kanál java.nio.channel.SocketChannel a předáme mu adresu InetSocketAddress.
5.1. Odeslání požadavku
Nejprve otevřeme náš SocketChannel:
InetSocketAddress address = new InetSocketAddress("localhost", wireMockRule.port());SocketChannel socketChannel = SocketChannel.open(address);
A nyní získáme standardní UTF-8 Charset pro kódování a zápis naší zprávy:
Charset charset = StandardCharsets.UTF_8;socket.write(charset.encode(CharBuffer.wrap("GET " + REQUESTED_RESOURCE + " HTTP/1.0\r\n\r\n")));
5.2. Čtení odpovědi
Po odeslání požadavku můžeme číst odpověď v neblokujícím režimu pomocí surových bufferů.
Protože budeme zpracovávat text, budeme potřebovat ByteBuffer pro surové bajty a CharBuffer pro převedené znaky (s pomocí dekodéru CharsetDecoder):
ByteBuffer byteBuffer = ByteBuffer.allocate(8192);CharsetDecoder charsetDecoder = charset.newDecoder();CharBuffer charBuffer = CharBuffer.allocate(8192);
Náš CharBuffer bude mít volné místo, pokud jsou data odeslána ve vícebajtové znakové sadě.
Poznamenejme, že pokud potřebujeme obzvlášť rychlý výkon, můžeme vytvořit MappedByteBuffer v nativní paměti pomocí funkce ByteBuffer.allocateDirect(). V našem případě je však použití funkce allocate() ze standardní haldy dostatečně rychlé.
Při práci s buffery potřebujeme vědět, jak je buffer velký (kapacita), kde se v bufferu nacházíme (aktuální pozice) a jak daleko můžeme jít (limit).
Přečteme tedy z našeho SocketChannelu a předáme mu náš ByteBuffer pro uložení našich dat. Naše čtení ze SocketChannelu skončí s aktuální pozicí našeho ByteBufferu nastavenou na další bajt k zápisu (hned za posledním zapsaným bajtem), ale s jeho limitem beze změny:
socketChannel.read(byteBuffer)
Náš SocketChannel.read() vrátí počet přečtených bajtů, které bylo možné zapsat do našeho bufferu. Bude to -1, pokud byl socket odpojen.
Když v našem bufferu nezbylo žádné místo, protože jsme ještě nezpracovali všechna jeho data, pak SocketChannel.read() vrátí nula přečtených bajtů, ale naše buffer.position() bude stále větší než nula.
Abychom měli jistotu, že začneme číst ze správného místa v bufferu, použijeme Buffer.flip(), abychom nastavili aktuální pozici našeho ByteBufferu na nulu a jeho limit na poslední bajt, který byl zapsán SocketChannel. Poté uložíme obsah bufferu pomocí metody storeBufferContents, kterou se budeme zabývat později. Nakonec pomocí buffer.compact() buffer zkomprimujeme a nastavíme aktuální pozici připravenou pro další čtení z kanálu SocketChannel.
Protože naše data mohou přicházet po částech, zabalíme náš kód pro čtení z bufferu do smyčky s ukončovacími podmínkami pro kontrolu, zda je náš socket stále připojen, nebo zda jsme byli odpojeni, ale v našem bufferu stále zbývají data:
while (socketChannel.read(byteBuffer) != -1 || byteBuffer.position() > 0) { byteBuffer.flip(); storeBufferContents(byteBuffer, charBuffer, charsetDecoder, ourStore); byteBuffer.compact();}
A nezapomeňme zavřít() náš socket (pokud jsme ho neotevřeli v bloku try-with-resources):
socketChannel.close();
Odpověď ze serveru bude obsahovat hlavičky, kvůli kterým může množství dat přesáhnout velikost naší vyrovnávací paměti. Proto použijeme StringBuilder pro sestavení naší kompletní zprávy, jakmile dorazí.
Pro uložení naší zprávy nejprve dekódujeme surové bajty na znaky v našem CharBuffer. Pak převrátíme ukazatele tak, abychom mohli číst naše znaková data, a připojíme je do našeho expandovatelného StringBuilderu. Nakonec vymažeme CharBuffer připravený na další cyklus zápisu/čtení.
Nyní tedy implementujme naši kompletní metodu storeBufferContents() předávající naše buffery, CharsetDecoder a StringBuilder:
void storeBufferContents(ByteBuffer byteBuffer, CharBuffer charBuffer, CharsetDecoder charsetDecoder, StringBuilder ourStore) { charsetDecoder.decode(byteBuffer, charBuffer, true); charBuffer.flip(); ourStore.append(charBuffer); charBuffer.clear();}
Závěr
V tomto článku jsme viděli, jak původní metoda java.io model blokuje, čeká na požadavek a používá Streamy k manipulaci s přijatými daty.
Naproti tomu knihovny java.nio umožňují neblokující komunikaci pomocí Buffers a Channels a mohou poskytnout přímý přístup do paměti pro vyšší výkon. S touto rychlostí však přichází další složitost manipulace s buffery.
Kód k tomuto článku je jako obvykle k dispozici na GitHubu.
Začněte se Spring 5 a Spring Boot 2 prostřednictvím kurzu Learn Spring:
>> VYHLEDEJTE SI KURZ
.