Síťování v Javě: UDP

1. 6. 2006
Doba čtení: 7 minut

Sdílet

Dnes se budeme věnovat dalšímu tématu z oblasti síťování - používání UDP protokolu v Javě. Vysvětlíme si práci s datagramy pomocí java.net API i s použitím New I/O. Na závěr vytvoříme Quote of the day server a klientskou aplikaci schopnou qotd přijímat.

Dosud jsme v tomto seriálu používali pouze TCP (Transmission Control Protocol) protokol. Třídy Socket, ServerSocket a oba kanály z NIO byly postaveny na TCP/IP modelu. Dnes se však budeme zabývat UDP (User Datagram Protocol) protokolem. Pojďme si tedy připomenout základní rozdíly mezi TCP a UDP:

  • TCP je spojovaný protokol, UDP nikoliv.
  • TCP je spolehlivý, UDP nespolehlivý.
  • UDP je rychlejší, což vyplývá z předchozích rozdílů.

Nemusíme tedy volat žádnou metodu accept(), abychom přijali klienta, jako tomu bylo u TCP. Jednoduše si počkáme na příchozí paket a zjistíme, odkud přišel. Na straně klienta nemusíme volat metodu connect(), pouze datagramu nastavíme adresu a port a odešleme ho do sítě. Takový jednoduchý přístup má ovšem i své stinné stránky. Nepoznáme odpojení klieta, jediným signálem může být dlouhá nečinnost. A protože UDP je nespolehlivý protokol, nemáme záruku, že data opravdu dorazí. Paket se může dokonce po své cestě změnit, přijít dvakrát. Více paketů může klidně dorazit v jiném pořadí, než jak byly odeslány.

UDP v java.net API

Nejprve si ukážeme, jak s datagramy pracovat pomocí java.net API. Práci s UDP protokolem zde mají na starosti tyto dvě třídy: java.net.Data­gramSocket a java.net.Data­gramPacket. Co dělají, je zcela zřejmé z jejich názvů – první je soket, druhá paket.

DatagramPacket

Tato třída představuje UDP paket. Poskytuje přístup k jeho nejdůležitějším částem – k IP adrese a portu, k délce dat a k datům samotným.

Vytvoření paketu

Třída DatagramPacket má šest konstruktorů. Uvedeme si pouze dva, které nám budou plně stačit:

  • DatagramPacket(by­te[] buf, int offset, int length) – Vytvoří paket nad polem bajtů s nastavenou délkou a posunutím.
  • DatagramPacket(by­te[] buf, int offset, int length, SocketAddress address) – Funguje stejně jako přechozí konstruktor, ale nastaví i cílovou IP adresu a port.

Ve zbylých čtyřech konstruktorech je vynechán parametr offset a/nebo address rozdělen na InetAddress a port.

Práce s paketem

Třída DatagramPacket má také mnoho metod, které umoňují práci s paketem a jeho daty. Všechny jsou typické gettery/settery pro tyto vlastnosti:

  • address – IP adresa paketu.
  • data – Pole bajtů s daty, které bylo zadáno v konstruktoru.
  • length – Délka dat v paketu.
  • offset – Posunutí dat v poli (index prvku, na kterém data paketu začínají).
  • port – Port.
  • socketAddress – Kombinace IP a portu.

Např. getAddress(), getLength(), setData(byte[] buf, int offset, int length), setPort(int port)…

DatagramSocket

DatagramSocket je druhá třída, kterou budeme používat pro práci s UDP protokolem. Umí odesílat datagramy, přijímat je a naslouchat na určitém UDP portu.

Vytvoření soketu a naslouchání

K vytvoření soketu slouží čtyři veřejné kontruktory třídy DatagramSocket:

  • DatagramSocket() – Vytvoří soket, který bude naslouchat na libovolném dostupném portu.
  • DatagramSocket(int port) – Vytvoří soket naslouchající na localhostu a zadaném portu.
  • DatagramSocket(int port, InetAddress laddr) – Vytvoří soket, který začne naslouchat na zadaném síťovém rozhraní a portu.
  • DatagramSocket(Soc­ketAddress bindaddr) – Stejný jako přechozí konstruktor.

Třída DatagramSocket obsahuje i metodu bind(SocketAddress addr). Pokud ji však zavoláte na soketu vytvořeném jedním ze čtyř předchozích konstruktorů, JVM vyhodí výjimku java.net.Socke­tException: already bound. Tato metoda byla totiž přidána ve verzi 1.4 společně s NIO a slouží výhradně pro práci s UDP pomocí NIO. Kdy ji používat si ukážeme, jakmile se dostaneme k třídě java.nio.chan­nels.Datagram­Channel.

Připojení

Další zajímavou metodou je connect(Socke­tAddress addr), která slouží k připojení ke vzdálené adrese. Zní to zvláštně, protože UDP je přece nespojovaný protokol. Tato metoda ale ve skutečnosti soket nikam nepřipojuje, slouží pouze k dosažení vyšší bezpečnosti. Po připojení budou UDP pakety filtrovány a odešlou se jen ty, které míří na adresu, kam je soket připojen.

Připojený soket lze odpojit pomocí metody disconnect().

Odesílání a přijímání paketů

K odesílání a přijímání dat slouží metody send(DatagramPacket p) a receive(Datagram­Packet p). Metoda send() odešle datagram, receive() zablokuje aktuální vlákno, dokud nějaký datagram nepřijde. Kam se má datagram odeslat, popř. odkud přišel, je uloženo ve vlastnosti address paketu.

Ukončení práce se soketem

Až nebudeme soket potřebovat, musíme zavolat metodu close(), aby přestal naslouchat a uzavřel se.

UDP pomocí New I/O

Už umíme používat UDP s java.net API. Nyní se naučíme pracovat s UDP pomocí New I/O.

Základem je třída DatagramChannel z balíku java.nio.channels. Dále budeme potřebovat NIO buffery a v případě nutnosti můžeme použít i ostatní součásti NIO, např. selektory. Ani java.net.Data­gramSocket nezůstane stranou, budeme volat její metodu bind().

DatagramChannel

DatagramChannel je kanál určený pro odesílání datagramů. Pojďme se podívat, jak ho používat:

Vytvoření kanálu

Kanál otevřeme pomocí statické tovární metody DatagramChannel­.open(). V tuto chvíli máme kanál připravený pro komunikaci ze strany klienta. Jestliže ho chceme použít na serverové straně, musíme ještě zavolat bind() na objektu

DatagramSocket, který vrátí metoda socket().

DatagramChannel ch = DatagramChannel.open();
ch.socket().bind(new InetSocketAddress("localhost", 10997)); 

Připojení

Připojení zde funguje úplně stejně jako s použitím třídy DatagramSocket. Pakety mířící na jinou adresu, než ke které je kanál připojen, budou zamítnuty.

Kanál připojíme pomocí metody connect(Socke­tAddress remote) a odpojíme metodou disconnect().

Odesílání a přijímání paketů

Stejně jako třída DatagramSocket i DatagramChannel má metody send() a receive(). Tentokrát se však liší. Metody z DatagramSocket jako parametr nepřebírají DatagramPacket, nýbrž bajtový buffer.

  • int send(ByteBuffer src, SocketAddress target) – Odešle paket s obsahem bajtového bufferu. Vrací počet odeslaných bajtů.
  • SocketAddress receive(ByteBuffer dst) – Přijme příchozí paket, jeho obsah zapíše do bufferu. Vrací adresu a port, odkud paket přišel.

DatagramChannel také obsahuje metody read() a write(), které již známe z třídy SocketChannel. Ty jdou použít jen v případě, kdy je soket připojen. Jinak by totiž nebylo jasné, kam data odeslat, popř. odkud data přišla.

Ukončení práce s kanálem

Nakonec nesmíme zapomenout kanál uzavřít metodou close().

Qoute of the day

Jako praktickou ukázku použití UDP protokolu v Javě jsem zvolil Quote of the day. Naprogramujeme pro něj si klientskou i serverovou aplikaci. Server v New I/O a klienta pomocí java.net.

Server

Tato aplikace je poměrně jednoduchá. Vytvoříme UDP kanál DatagramChannel a necháme ho naslouchat na všech síťových rozhraních a portu 17. Dále alokujeme buffer o velikosti 512 bajtů. Pokračujeme průchodem smyčky. Počkáme na přijetí UDP paketu, vymažeme jeho obsah z bufferu a naplníme buffer náhodně vybranou citací. Nakonec obsah bufferu odešleme jako UDP paket.

ict ve školství 24

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

public class Qotd {

    private DatagramChannel udpChannel;
    private ByteBuffer buffer;

    private static final String[] QUOTES = {
        "Jestliže se program osvědčí, je třeba jej změnit.",
        "Kdyby Bůh stvořil člověka k tomu, aby používal počítač, byl by mu dal 16 prstů.",
        "Jediný jazyk, který všichni programátoři ovládají dokonale, jsou nadávky.",
        "Chybové hlášení je nejpodlejší pomstou počítače.",
        "Počítače jsou nespolehlivé, lidé také. Avšak počítače jsou v tom mnohem důkladnější."
    };

    public void run() {
        try {
            udpChannel = DatagramChannel.open(); //otevřít UDP kanál
            udpChannel.socket().bind(new InetSocketAddress("0.0.0.0", 17)); //začít naslouchat
            buffer = ByteBuffer.allocateDirect(512); //alokovat buffer

            while(true) {
                SocketAddress client = udpChannel.receive(buffer); //přijmout UDP paket
                buffer.clear(); //vymazat obsah bufferu
                buffer.put(randomQuote().getBytes()); //vložit do bufferu náhodnou citaci
                buffer.flip();
                udpChannel.send(buffer, client); //odeslat zpět
            }
        }
        catch(IOException e) {
            e.printStackTrace(System.err);
        }
        finally {
            try {
                if(udpChannel != null) udpChannel.close();
            }
            catch(IOException e) {}
        }
    }

    public static String randomQuote() {
        return QUOTES[(int)Math.floor(Math.random() * QUOTES.length)];
    }

    public static void main(String[] args) {
        new Qotd().run();
    }
} 

Klient

Klientská aplikace je stejně jednoduchá jako ta na straně serveru. Nejprve vytvoříme novou instanci třídy DatagramSocket, pole 512 bajtů. a paket DatagramPacket. V dalším kroku odešleme prázdný paket na adresu zadanou jako první parametr konzole. Pak počkáme, dokud se nevrátí odpoveď s citátem, který vypíšeme na standardní výstupní proud.

import java.net.*;
import java.io.*;

public class QotdClient {

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

        DatagramSocket udpSocket = null;
        byte[] packetData = new byte[512];
        DatagramPacket packet = new DatagramPacket(packetData, 512); //vytvořit paket
        try {
            udpSocket = new DatagramSocket(); //vytvořit soket
            packet.setSocketAddress(new InetSocketAddress(args[0], 17)); //nastavit paketu adresu
            udpSocket.send(packet); //odeslat prázdný paket
            udpSocket.receive(packet); //počkat na přijetí paketu s citací
            System.out.println(new String(packetData, 0, packet.getLength())); //vytisknout obsah paketu
        }
        catch(IOException e) {
            e.printStackTrace(System.err);
        }
        finally {
            if(udpSocket != null) udpSocket.close();
        }
    }
} 

Závěr

Dnes jsme si ukázali, jak pracovat s UDP protokolem v Javě. A to jak pomocí New I/O, tak s použitím java.net API. Příště nás čeká závěr seriálu. Shrneme si všechny získané znalosti a ukážeme si několik tipů a triků, které se budou při síťování určitě hodit.

Autor článku