Síťování v Javě: New I/O

11. 5. 2006
Doba čtení: 7 minut

Sdílet

V dnešním díle nás čeká úvod do New I/O API. Probereme důležité třídy, podrobněji se zaměříme na práci s buffery. Na závěr vytvoříme jednoduchý příklad NIO klientské aplikace, na kterém si ukážeme nejen síťování, ale také práci se soubory pomocí NIO.

Balíky

New I/O knihovna se skládá z pěti balíků: java.nio, java.nio.channels, java.nio.charset, java.nio.chan­nels.spi a java.nio.char­set.spi. Nás budou zajímat pouze první tři, další dva nebudeme vůbec využívat. Pokud máte zájem dozvědět se o nich něco, nahlédněte do dokumentace k Java API.

V java.nio jsou umístěny buffery, které si ještě dnes popíšeme. Síťové kanály a selektory jsou obsaženy v balíku java.nio.channels. Nakonec java.nio.charset poskytuje API pro práci se znakovými sadami.

Ani již známé knihovny java.net a java.io nezůstanou stranou. NIO je nad nimi vystavěno, tudíž i je si budeme muset naimportovat.

Buffery

Když jsme potřebovali přenést nějaká data do soketu nebo naopak z něj, vystačili jsme si s vyrovnávací pamětí implemetovanou jako pole bajtů. NIO však přišlo s chytřejším a především rychlejším řešením než javová pole. Pro každý primitivní datový typ kromě boolean nabízí třídu bufferu. Ta umožňuje takové operace, jakými jsou například přetáčení, nastavení značky atd. Navíc bajtový buffer lze alokovat jako nativní datovou strukturu.

Třídy

Jak již bylo řečeno, buffery se nacházejí v balíčku java.nio a existují pro všechny primitivní typy kromě boolean. Tedy jen krátce, jsou to třídy ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer.

Zajímavostí je třída MappedByteBuffer vhodná pro práci s velkými soubory. Ta však do seriálu o síťování nepatří, takže se jí nebudeme zabývat.

Všechny buffery sjednocuje společná abstraktní třída Buffer.

Alokace bufferů

Bajtové buffery je možné alokovat jako přímé nebo nepřímé. Přímé nabízejí vyšší výkon, protože data se ukládají do nativních datových struktur. Hodí se zejména pro práci se soubory a sokety, budeme je tedy hojně využívat. Přímý bajtový buffer vytvoříme metodou allocateDirect(int kapacita). A jedno důležité upozornění: bajtový buffer neznamená, že se do něj dají ukládat pouze bajty. Je možné ho použít pro jakýkoliv primitivní datový typ kromě boolean.

Pokud chceme použít jiný buffer než bajtový, jedinou možností je alokovat buffer nepřímo. K tomu slouží metoda allocate(int kapacita). Nepřímé buffery jsou vhodné pro práci s daty pouze v rámci JVM, nebo musíme-li je často alokovat a mazat.

Práce s buffery

Každý buffer má čtyři důležité hodnoty: kapacitu, pozici, limit a značku. Práci s těmito hodnotami umožňují metody z abstraktní třídy Buffer.

Kapacita, pozice, limit a značka

Kapacita čili velikost vyrovnávací paměti je konstantní hodnota, kterou jsme zadali jako parametr metody allocate() nebo allocateDirect(). Zjistíme ji metodou capacity().

Limit je značka určená pro čtení a zápis do bufferu. Označuje index prvního prvku, který ještě nelze přečíst, nebo pozici, na kterou ještě nelze zapisovat (místo není volné). Aktuální limit vrací metoda limit(), změníme ho pomocí stejnojmenné metody s jedním celočíselným parametrem – limit(int novýLimit).

Pozice určuje index aktuálního elementu, na kterém bude probíhat čtení nebo zápis. Zjišťuje a nastavuje se obdobně jako limit, tentokrát však přetíženou metodou position().

Značka je index prvku, který si můžeme označit metodou mark() a vrátit se na jeho pozici voláním reset().

clear()

Tato metoda „vymaže“ obsah bufferu nastavením nulové pozice a limitu na kapacitu. Zároveň zruší značku. Buffer je pak připraven k novému použití.

flip()

Tuto metodu použijeme, když je buffer plný (nebo obsahuje požadované množství dat). Nastaví totiž limit na aktuální pozici, kterou následně resetuje na nulu. Tím máme zaručeno, že se nepřečte více prvků než chceme.

rewind()

Přetočí buffer. Vynuluje pozici a zruší značku, takže je možné z bufferu opět přečíst stejná data.

Kanály

Kanály reprezentují různé síťové prostředky, soubory, roury a pro nás nejdůležitější – síťové sokety. Společně s buffery nabízejí stejnou funkčnost jako třídy InputStream, OutputStream, Writer a Reader. Všechny NIO kanály se nacházejí v balíku java.nio.channels.

Kanály jsou odvozeny z mnoha různých rozhraní a abstraktních tříd. Od Channel se dvěmi metodami isOpen() a close() až po třídu SelectableChannel, jenž umožňuje poměrně pokročilé operace, jakou je třeba zaregistrování selektoru.

Balík java.nio.channels obsahuje dva kanály pro rouru – Pipe.SourceChannel a Pipe.SinkChannel, FileChannel pro práci se soubory a tři kanály pro práci se sítí – DatagramChannel pro UDP, SocketChannel a ServerSocketChannel pro TCP/IP přenos.

Dnes se naučíme používat třídu SocketChannel. V příkladu také využijeme schopnosti třídy FileChannel, která se chová téměř stejně.

SocketChannel

SocketChannel má stejné využití jako třída java.net.Socket. Reprezentuje soket klienta připojeného k serverové aplikaci protokolem TCP/IP. Pojďme se podívat na metody této důležité třídy:

Důležité metody z java.nio.chan­nels.SocketChan­nel
static SocketChannel open(SocketAddress remote) Statická tovární metoda třídy SocketChannel. Otevře kanál soketu, připojí ho k zadané adrese a vrátí na něj odkaz. Pokud bychom použili metodu open() bez parametru, museli bychom soket dodatečně připojit voláním metody connect(Socke­tAddress remote).
void close() Uzavře kanál. Pokus o jakoukoliv další I/O operaci vyhodí chybu java.nio.chan­nels.ClosedChan­nelException.
int read(ByteBuffer dst) Načte data do bajtového bufferu. Protože tato metoda nemusí vždy zaplnit celý buffer (resp. zbytek bufferu), vrací počet přečtených bajtů. Pokud vrátí hodnotu –1, kanál byl uzavřen.
int write(ByteBuffer src) Pokusí se zapsat do všechny kanálu bajty od aktuální pozice bufferu až do limitu. Protože nemusí všechna data odeslat, vrátí počet zapsaných bajtů.

Toto jsou nejdůležitější metody třídy SocketChannel, které budeme neustále používat. Podobné metody mají i ostatní kanály (kromě ServerSocketChan­nel), implementují totiž stejná rozhraní jako SocketChannel.

HTTP Download

Dnes jsem jako ukázkový příklad zvolil HTTP „stahovač“. Jedná se o velice primitivní obdobu známého wgetu, která téměř nic neumí :-). Je však dobrou ukázkou typické práce s NIO na straně klienta.

Jako parametr programu zadáme URL souboru, které chceme stáhnout. Program pomocí java.net.URL z URL získá adresu serveru, port a jméno souboru. Z toho plyne jeden problém. Třída java.net.URL totiž očekává URL ve tvaru protokol://ser­ver:port/soubor a pokud nějakou část vynecháme, vyhodí výjimku. Tudíž i v takovémto tvaru musíme URL zadat na vstup. Tento příklad ale nemá ukázat, jak správně parsovat URL, takže se touto drobnou chybou nebudeme zabývat.

Další nepříjemnost vzniká při ukládání dat do souboru. Všechna data bez jakékoliv změny nebo kontroly přepošleme do souborového kanálu, a to včetně HTTP hlaviček. Znovu však platí, že příklad má ukázat NIO, nikoliv dokonalou HTTP komunikaci a odstraňování hlaviček.

Návrh aplikace

Jako první načteme z parametrů port. Dále vytvoříme adresu serveru, připojíme soket a vytvoříme soubor, do kterého budeme zapisovat. Se souborem budeme pracovat také pomocí NIO. Kanál pro zápis získáme voláním metody FileOutputStre­am.getChannel(). Dále vytvoříme přímý bajtový buffer metodou ByteBuffer.allo­cateDirect(). Posledním krokem před začátkem síťové komunikace bude sestavení HTTP požadavku.

Ještě před zápisem převedeme řetězec požadavku. Následně v cyklu zapíšeme do bufferu 256 bajtů (nebo méně pokud jich tolik nezbylo). Obsah bufferu se budeme do soketu pokoušet zapsat tak dlouho, dokud se neodešle úplně celý. Po dokončení přejdeme na další průchod cyklu, abychom mohli poslat další data.

Čtení ze soketu probíhá podobně. Načteme ze soketu nějaké bajty a všechny je pak zapíšeme do souboru.

bitcoin_skoleni

Program končí přechodem do finally bloku, kde uzavřeme všechny systémové prostředky, tedy soubor, jeho kanál a síťový soket.

Zdrojový kód

import java.io.*;
import java.net.*;
import java.nio.*;
import java.nio.channels.*;

/** Hlavní třída HTTP stahovače. */
public class HttpDownload {

    public static void main(String[] args) {
        if(args.length < 1) {
            System.out.println("Použití: java HttpDownload URL");
            System.exit(-1);
        }

        URL url = null;
        try {
            url = new URL(args[0]); //načíst URL
        }
        catch(MalformedURLException e) {
            System.out.println("Neplatné URL.");
            System.exit(-1);
        }
        //vytvořit adresu pro připojení soketu
        SocketAddress addr = new InetSocketAddress(url.getHost(), url.getPort());

        SocketChannel socket = null;
        FileOutputStream fos = null;
        FileChannel file = null;

        try {
            //vytvořit a otevřít soket
            socket = SocketChannel.open(addr);

            //vytvořit soubor a souborový kanál
            String filename = url.getFile().substring(url.getFile().lastIndexOf('/') + 1);
            File f = new File(filename);
            f.createNewFile(); //vytvořit nový soubor
            fos = new FileOutputStream(f); //vytvořit výstupní proud
            file = fos.getChannel(); //ze vstupního proudu získat kanál pro zápis

            //vytvořit buffer
            ByteBuffer buffer = ByteBuffer.allocateDirect(256);

            //sestavit HTTP požadavek
            String http = "GET " + url.getFile() + " HTTP/1.0\n"
                             + "Host: " + url.getHost() + "\n\n";

            //zapsat do kanálu HTTP požadavek
            int nbytes = 0;
            byte[] bytes = http.getBytes(); //převést HTTP požadavek na pole bajtů
            while(nbytes < bytes.length) { //opakovat cyklus, dokud je co zapisovat
                int len = Math.min(buffer.capacity(), bytes.length - nbytes);
                buffer.put(bytes, nbytes, len); //vložit bajty do bufferu
                buffer.flip();
                nbytes += len;

                int n = 0;
                while(n < len) { //zapsat všechny bajty do soketu
                    n += socket.write(buffer);
                }
            }

            while(true) { //pořád číst data ze soketu
                int read = socket.read(buffer);
                if(read == -1) break; //signál uzavření soketu
                buffer.flip();

                int n = 0;
                while(n < read) { //zapsat všechny bajty do souboru
                    n += file.write(buffer);
                }
            }
        }
        catch(IOException e) {
            e.printStackTrace(System.err);
        }
        finally {
            try {
                if(socket != null) socket.close();
                if(fos != null) fos.close();
                if(file != null) file.close();
            }
            catch(IOException e) {}
        }
    }
} 

Závěr

Dnes jsme si popsali první část New I/O API – buffery a síťové sokety. Poprvé jsme použili třídu SocketChannel a dokonce jsme si zkusili zápis do souboru pomocí NIO. Příště si ukážeme práci se selektory, o kterých jsem se už několikrát znímil, a podíváme se, jak v NIO vytvářet serverové aplikace.

Autor článku