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.DatagramSocket a java.net.DatagramPacket. 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(byte[] buf, int offset, int length) – Vytvoří paket nad polem bajtů s nastavenou délkou a posunutím.
- DatagramPacket(byte[] 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(SocketAddress 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.SocketException: 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.channels.DatagramChannel.
Připojení
Další zajímavou metodou je connect(SocketAddress 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(DatagramPacket 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.DatagramSocket 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(SocketAddress 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.
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.