Áttekintés

A Java programozók számára a be- és kimenetek kezelése gyakori feladat. Ebben a bemutatóban az eredeti java.io (IO) könyvtárakat és az újabb java.nio (NIO) könyvtárakat nézzük meg, és azt, hogy miben különböznek egymástól a hálózaton keresztüli kommunikáció során.

Főbb jellemzők

Kezdésként nézzük meg mindkét csomag legfontosabb jellemzőit.

2.1. A Java.io (IO) és az újabb java.nio (NIO) könyvtárak. IO – java.io

A java.io csomag a Java 1.0-ban, a Reader pedig a Java 1.1-ben került bevezetésre. Biztosítja:

  • InputStream és OutputStream – amelyek bájtonként adnak át adatokat
  • Reader és Writer – kényelmi burkolatok a streamek számára
  • blokkoló üzemmód – a teljes üzenetre való várakozáshoz

2.2. Az io2.2. NIO – java.nio

A java.nio csomagot a Java 1.4-ben vezették be, és a Java 1.7-ben (NIO.2) továbbfejlesztett fájlműveletekkel és ASynchronousSocketChannel-nel frissítették. Biztosítja a következőket:

  • Buffer – az adatdarabok egyszerre történő olvasásához
  • CharsetDecoder – a nyers bájtok olvasható karakterekké/ből való leképezéséhez
  • Channel – a külvilággal való kommunikációhoz
  • Selector – a SelectableChannel-en történő multiplexelés engedélyezéséhez és az I/O-ra kész csatornákhoz való hozzáférés biztosításához
  • non-blocking mode – olvasni, ami készen áll

Most nézzük meg, hogyan használjuk az egyes csomagokat, amikor adatokat küldünk egy kiszolgálónak, vagy olvassuk a válaszát.

Tesztkiszolgálónk konfigurálása

Itt a WireMockot fogjuk használni egy másik kiszolgáló szimulálására, hogy tesztjeinket függetlenül futtathassuk.

Konfiguráljuk úgy, hogy figyeljen a kéréseinkre, és küldjön nekünk válaszokat, ahogy egy valódi webkiszolgáló tenné. Emellett dinamikus portot fogunk használni, hogy ne kerüljünk konfliktusba a helyi gépünkön lévő szolgáltatásokkal.

Adjuk hozzá a WireMock Maven függőségét tesztelési hatókörrel:

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

Egy tesztosztályban definiáljunk egy JUnit @Rule-t, hogy a WireMockot egy szabad porton indítsuk el. Ezután konfiguráljuk úgy, hogy egy HTTP 200 választ küldjön vissza nekünk, amikor egy előre definiált erőforrást kérünk, az üzenet teste pedig valamilyen JSON formátumú szöveg legyen:

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

Most, hogy felállítottuk a mock szerverünket, készen állunk néhány teszt futtatására.

Blokkoló IO – java.io

Nézzük meg, hogyan működik az eredeti blokkoló IO modell, néhány adat beolvasásával egy weboldalról. Egy java.net.Socketet fogunk használni, hogy hozzáférjünk az operációs rendszer egyik portjához.

4.1. Kérés küldése

Ebben a példában egy GET-kérést hozunk létre az erőforrásaink lekérdezéséhez. Először hozzunk létre egy Socketet, hogy elérjük azt a portot, amelyen a WireMock szerverünk hallgat:

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

Normál HTTP vagy HTTPS kommunikáció esetén a port 80 vagy 443 lenne. Ebben az esetben azonban a wireMockRule.port() segítségével érjük el a korábban beállított dinamikus portot.

Most nyissunk egy OutputStream-et a socket-en, egy OutputStreamWriter-be csomagolva, és adjuk át egy PrintWriter-nek az üzenetünk megírásához. És győződjünk meg róla, hogy a puffert kiürítjük, hogy a kérésünk elküldésre kerüljö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. Várjuk meg a választ

Nyissunk egy InputStream-et a socket-en a válasz eléréséhez, olvassuk be a streamet egy BufferedReaderrel, és tároljuk egy StringBuilderben:

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

A reader.readLine() segítségével blokkoljuk, várjuk meg a teljes sort, majd csatoljuk a sort a tárolónkhoz. Addig olvassuk, amíg egy nullát nem kapunk, ami a folyam végét jelzi:

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

Non-Blocking IO – java.nio

Most nézzük meg, hogyan működik a nio csomag nem blokkoló IO modellje ugyanezzel a példával.

Ezúttal egy java.nio.channel.SocketChannel-t hozunk létre a szerverünk portjának eléréséhez egy java.net.Socket helyett, és átadunk neki egy InetSocketAddresset.

5.1. Küldjünk egy kérést

Először nyissuk meg a SocketChannelünket:

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

És most szerezzünk egy szabványos UTF-8 Charset-et az üzenetünk kódolásához és megírásához:

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

5.2. Írjuk meg az üzenetünket. A válasz olvasása

A kérés elküldése után a választ nem blokkoló üzemmódban, nyers pufferek segítségével olvashatjuk.

Mivel szöveget fogunk feldolgozni, szükségünk lesz egy ByteBufferre a nyers bájtokhoz és egy CharBufferre az átalakított karakterekhez (egy CharsetDecoder segítségével):

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

A CharBufferünkben marad még hely, ha az adatokat több bájtos karakterkészletben küldjük.

Megjegyezzük, hogy ha különösen gyors teljesítményre van szükségünk, létrehozhatunk egy MappedByteBuffer-t a natív memóriában a ByteBuffer.allocateDirect() segítségével. A mi esetünkben azonban az allocate() használata a szabványos heapből elég gyors.

A pufferekkel való bánásmód során tudnunk kell, hogy mekkora a puffer (a kapacitás), hol vagyunk a pufferben (az aktuális pozíció), és meddig mehetünk (a határ).

Azért olvassunk a SocketChannelünkből, átadva neki a ByteBufferünket az adataink tárolására. A SocketChannelből történő olvasásunk úgy fejeződik be, hogy a ByteBufferünk aktuális pozíciója a következő bájtra lesz állítva, ahová írhatunk (közvetlenül az utoljára írt bájt után), de a limitje változatlan marad:

socketChannel.read(byteBuffer)

A SocketChannel.read() visszaadja, hogy hány bájtot olvastunk be, amit a pufferünkbe írhattunk. Ez -1 lesz, ha a socket kapcsolat megszakadt.

Ha a pufferünknek már nincs helye, mert még nem dolgoztuk fel az összes adatát, akkor a SocketChannel.read() nulla beolvasott bájtot ad vissza, de a buffer.position() értéke még mindig nagyobb lesz nullánál.

Azért, hogy a puffer megfelelő helyéről kezdjük az olvasást, a Buffer.flip() segítségével a ByteBufferünk aktuális pozícióját nullára, a határát pedig a SocketChannel által írt utolsó bájtra állítjuk. Ezután elmentjük a puffer tartalmát a storeBufferContents metódusunkkal, amelyet később megnézünk. Végül a buffer.compact() segítségével tömörítjük a puffert, és beállítjuk az aktuális pozíciót, hogy készen álljon a SocketChannelből történő következő olvasásra.

Mivel az adataink részekben érkezhetnek, csomagoljuk be a pufferolvasó kódunkat egy ciklusba, amely befejezési feltételekkel ellenőrzi, hogy a socketünk még mindig kapcsolatban van-e, vagy ha már megszakadt a kapcsolat, de még mindig van adat a pufferünkben:

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

És ne felejtsük el lezárni() a socketünket (kivéve, ha egy try-with-resources blokkban nyitottuk meg):

socketChannel.close();

5.3. A pufferolvasó kódot a pufferolvasás után a Socket-csatornánkon kell elvégezni. Adatok tárolása a pufferünkből

A szerver válasza fejléceket fog tartalmazni, ami miatt az adatmennyiség meghaladhatja a pufferünk méretét. Ezért egy StringBuildert fogunk használni a teljes üzenetünk felépítéséhez, amint az megérkezik.

Az üzenetünk tárolásához először dekódoljuk a nyers bájtokat karakterekké a CharBufferünkben. Ezután megfordítjuk a mutatókat, hogy be tudjuk olvasni a karakteradatainkat, és hozzácsatoljuk a bővíthető StringBuilderünkhöz. Végül töröljük a CharBuffer-t, készen állva a következő írási/olvasási ciklusra.

Most tehát implementáljuk a teljes storeBufferContents() metódusunkat, átadva a pufferünket, a CharsetDecoder-t és a StringBuilder-t:

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

Következtetés

Ebben a cikkben láttuk, hogy az eredeti java.io modell blokkol, vár a kérésre, és Streams segítségével manipulálja a kapott adatokat.

Ezzel szemben a java.nio könyvtárak lehetővé teszik a nem blokkoló kommunikációt Buffers és Channels segítségével, és a gyorsabb teljesítmény érdekében közvetlen memóriaelérést biztosítanak. Ezzel a sebességgel azonban együtt jár a pufferek kezelésének további bonyolultsága is.

A cikk kódja szokás szerint elérhető a GitHubon.

Kezdje el a Spring 5 és a Spring Boot 2 használatát a Tanulj Spring tanfolyamon keresztül:

>> KERESZE MEG A TANFOLYAMOT

Vélemény, hozzászólás?

Az e-mail-címet nem tesszük közzé.