Yleiskatsaus

Syötteen ja tulosteen käsittely on Java-ohjelmoijille yleinen tehtävä. Tässä opetusohjelmassa tarkastelemme alkuperäisiä java.io (IO) -kirjastoja ja uudempia java.nio (NIO) -kirjastoja sekä sitä, miten ne eroavat toisistaan verkon yli tapahtuvassa kommunikoinnissa.

Keskeiset ominaisuudet

Aloitetaan tarkastelemalla molempien pakettien keskeisimpiä ominaisuuksia.

2.1. JOAVA.io (IO) -kirjastot. IO – java.io

Javapaketti java.io otettiin käyttöön Java 1.0:ssa, ja Reader otettiin käyttöön Java 1.1:ssä. Se tarjoaa:

  • InputStream ja OutputStream – jotka toimittavat dataa tavu kerrallaan
  • Reader ja Writer – käteviä kääreitä streameille
  • blocking mode – odottamaan täydellistä viestiä

2.2. Viestintätekniikka. NIO – java.nio

Java.nio-paketti otettiin käyttöön Java 1.4:ssä ja päivitettiin Java 1.7:ssä (NIO.2) parannetuilla tiedosto-operaatioilla ja ASynchronousSocketChannelilla. Se tarjoaa:

  • Buffer – lukemaan tietopaketteja kerrallaan
  • CharsetDecoder – käsittelemättömien tavujen kuvaamiseen luettaviksi merkeiksi/luettavista merkeistä
  • Channel – kommunikaatioon ulkomaailman kanssa
  • Selector – mahdollistamaan multipleksointi SelectableChannel-kanavalla ja tarjoamaan pääsyn mihin tahansa kanavaan, joka on valmis I/O:ksi
  • non-blocking mode – lukea mitä tahansa, mikä on valmis

Katsotaan nyt, miten käytämme kutakin näistä paketeista, kun lähetämme dataa palvelimelle tai luemme sen vastauksen.

Testipalvelimemme konfigurointi

Tässä käytämme WireMockia simuloidaksemme toista palvelinta, jotta voimme suorittaa testejämme itsenäisesti.

Konfiguroimme sen kuuntelemaan pyyntöjämme ja lähettämään meille vastauksia aivan kuten oikea web-palvelin. Käytämme myös dynaamista porttia, jotta emme ole ristiriidassa minkään paikallisen koneemme palveluiden kanssa.

Lisätään WireMockin Maven-riippuvuus testin soveltamisalalla:

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

Testiluokassa määritetään JUnit @Rule käynnistämään WireMock vapaassa portissa. Määritämme sen sitten palauttamaan meille HTTP 200 -vastauksen, kun pyydämme ennalta määriteltyä resurssia, jossa viestin runko on jotain tekstiä JSON-muodossa:

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

Nyt kun olemme saaneet mock-palvelimemme perustettua, olemme valmiita suorittamaan joitakin testejä.

Blokkaava IO – java.io

Katsotaanpa, miten alkuperäinen blokkaava IO-malli toimii lukemalla dataa verkkosivulta. Käytämme java.net.Socketia saadaksemme pääsyn yhteen käyttöjärjestelmän portista.

4.1. Pyynnön lähettäminen

Tässä esimerkissä luomme GET-pyynnön, jolla haemme resurssimme. Luodaan ensin Socket, jolla päästään käsiksi porttiin, jota WireMock-palvelimemme kuuntelee:

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

Normaalissa HTTP- tai HTTPS-viestinnässä portti olisi 80 tai 443. Tässä tapauksessa käytämme kuitenkin wireMockRule.port() -toimintoa päästäksemme käsiksi aiemmin määrittelemäämme dynaamiseen porttiin.

Avataan nyt Socketiin OutputStream, joka kääritään OutputStreamWriteriin ja siirretään se PrintWriteriin viestimme kirjoittamista varten. Ja varmistetaan, että huuhtelemme puskurin, jotta pyyntömme lähetetää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. Odotetaan vastausta

Avataan InputStream socketissa vastauksen saamiseksi, luetaan stream BufferedReaderilla ja tallennetaan se StringBuilderiin:

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

Käyttäkäämme reader.readLine():

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

Käyttäkäämme reader.readLine()-toimintoa blokkaamaan odottaessamme kokonaista riviä ja liittäkäämme sitten rivi varastoon. Jatkamme lukemista, kunnes saamme nollan, joka tarkoittaa virran loppua:

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

Blokkaamaton IO – java.nio

Katsotaan nyt, miten nio-paketin blokkaamaton IO-malli toimii saman esimerkin avulla.

Tällä kertaa luomme java.nio.channel.SocketChannel java.net.Socketin sijaan java.nio.channel.SocketChannelin, jolla pääsemme käsiksi palvelimemme porttiin, ja annamme sille InetSocketAddressin.

5.1. Lähetä pyyntö

Avataan ensin SocketChannel-kanavamme:

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

Ja nyt hankitaan standardi UTF-8 Charset viestimme koodausta ja kirjoittamista varten:

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

5.2. Vastauksen lukeminen

Kun lähetämme pyynnön, voimme lukea vastauksen ei-blokkaavassa tilassa käyttäen raakapuskureita.

Koska käsittelemme tekstiä, tarvitsemme ByteBufferin raaoille tavuille ja CharBufferin muunnetuille merkeille (apunamme CharsetDecoder):

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

CharBufferissamme on ylimääräistä tilaa, jos data lähetetään usean tavuisen merkin merkkijoukossa.

Huomaa, että jos tarvitsemme erityisen nopeaa suorituskykyä, voimme luoda MappedByteBufferin natiivimuistiin käyttämällä ByteBuffer.allocateDirect(). Meidän tapauksessamme allocate():n käyttäminen tavallisesta kasasta on kuitenkin riittävän nopeaa.

Käsitellessämme puskureita meidän on tiedettävä, kuinka suuri puskuri on (kapasiteetti), missä olemme puskurissa (nykyinen sijainti) ja kuinka pitkälle voimme mennä (raja).

Luetaan siis SocketChannel-kanavastamme ja välitämme sille ByteBuffer-puskurimme tietojemme tallennusta varten. Lukemamme SocketChannelista päättyy siten, että ByteBufferimme nykyinen sijainti on asetettu seuraavaan tavuun, johon voimme kirjoittaa (heti viimeisen kirjoitetun tavun jälkeen), mutta sen raja on ennallaan:

socketChannel.read(byteBuffer)

SocketChannel.read() palauttaa lukemiemme tavujen lukumäärän, joka voitaisiin kirjoittaa puskuriin. Tämä on -1, jos socketista katkaistiin yhteys.

Kun puskurissamme ei ole enää tilaa, koska emme ole vielä käsitelleet kaikkea sen dataa, SocketChannel.read() palauttaa nolla luettua tavua, mutta buffer.position() on silti suurempi kuin nolla.

Voidaksemme varmistaa, että aloitamme lukemisen oikeasta kohdasta puskurissa, käytämme Buffer.flip():tä asettaaksemme ByteBufferimme nykyisen position nollaan ja sen rajan SocketChannelin viimeksi kirjoittamaan tavuun. Tallennamme sitten puskurin sisällön käyttämällä storeBufferContents-metodia, jota tarkastelemme myöhemmin. Lopuksi käytämme buffer.compact()-menetelmää puskurin tiivistämiseen ja nykyisen sijainnin asettamiseen valmiiksi seuraavaa SocketChannel-lukua varten.

Koska datamme saattaa saapua osissa, kiedotaan puskuria lukeva koodimme silmukkaan, jossa on lopetusehdot, joilla tarkistetaan, onko socketimme yhä yhteydessä tai onko yhteys katkaistu, mutta puskurissamme on vielä dataa jäljellä:

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

Eikä unohdeta sulkea() socketiamme (paitsi jos avasimme sen try-with-resources-lohkossa):

socketChannel.close();

5.3. Puskuria lukeva koodi. Tietojen tallentaminen puskuristamme

Palvelimelta tuleva vastaus sisältää otsikoita, joiden vuoksi tietomäärä saattaa ylittää puskurimme koon. Niinpä käytämme StringBuilderia rakentaaksemme täydellisen viestimme sitä mukaa, kun se saapuu.

Tallentaaksemme viestimme, dekoodaamme ensin raa’at tavut merkeiksi CharBufferiimme. Sitten käännämme osoittimet niin, että voimme lukea merkkidatamme ja liittää sen laajennettavaan StringBuilderiin. Lopuksi tyhjennämme CharBufferin valmiiksi seuraavaa kirjoitus-/lukusykliä varten.

Toteutetaan nyt siis täydellinen storeBufferContents()-metodimme välittämällä puskurimme, CharsetDecoderimme ja StringBuilderimme:

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

Conclusion

Tässä artikkelissa näimme, miten alkuperäinen java.io-malli blokkaa, odottaa pyyntöä ja käyttää Streamsia saadun datan käsittelyyn.

Java.nio-kirjastot sen sijaan mahdollistavat lukkiutumattoman kommunikoinnin käyttämällä Buffers- ja Channels-kanavia ja voivat tarjota suoran muistin käytön nopeamman suorituskyvyn saavuttamiseksi. Tämän nopeuden myötä tulee kuitenkin lisää monimutkaisuutta puskureiden käsittelyyn.

Tuttuun tapaan tämän artikkelin koodi on saatavilla GitHubissa.

Aloita Spring 5:n ja Spring Boot 2:n käyttö Opi Spring -kurssin avulla:

>> KATSO KURSSI

Vastaa

Sähköpostiosoitettasi ei julkaista.