Lekce 11 - Java server - Propagace lokální sítí (1. část)
V předchozím kvízu, Kvíz - Komunikační protokol, Event bus a pluginy v Javě, jsme si ověřili nabyté zkušenosti z předchozích lekcí.
Dnes se postaráme, aby byl server viditelný v lokální síti.
TCP vs. UDP
Než začneme programovat, povíme si trochu teorie o TCP a UDP protokolech.
TCP
Zkratka znamená Transmission Control Protocol. Jedná se o protokol, který je spolehlivý a spojovaný. Spolehlivý znamená, že data, která odešle jeden uživatel, dorazí k cíli v pořádku a ve správném pořadí. Spojovaný znamená, že před začátkem komunikace se musí vytvořit spojení, které se drží po celou dobu. Používá se hlavně tam, kde dáváme přednost spolehlivosti před rychlostí.
UDP
Zkratka znamená Universal/User Datagram Protocol. UDP je přesný opak TCP. Protokol je nespolehlivý a nespojovaný. Jednotlivé datagramy mohou přicházet v různém pořadí. Protokol nezaručuje, že se data úspěšně přenesou - mohou se cestou ztratit. Používá se tam, kde je potřeba efektivně a rychle přenášet data, jako jsou hry, videa...
Multicast sender
V předchozích lekcích jsme navrhli komunikační protokol právě nad TCP, takže máme zaručeno, že data vždy dorazí v pořádku. Nyní implementujeme zviditelnění serveru pomocí UDP. Vytvoříme si novou třídu, která bude v nekonečné smyčce v definovaném intervalu rozesílat datagram všem strojům v lokální síti. Stroj, který nebude vědět, jak zprávu zpracovat, ji zahodí. My si napíšeme v klientovi obsluhu na příjem těchto datagramů.
V balíčku core
vytvoříme nový balíček
multicaster
, ve kterém implementujeme výše zmíněnou
funkcionalitu.
Návrh rozhraní
Vytvoříme si jednoduché značkovací rozhraní
IMulticastSender
, které nebude obsahovat žádnou metodu:
public interface IMulticastSender extends IThreadControl {}
Dále rozhraní představující továrnu pro tvorbu instancí
IMulticastSenderFactory
s metodou
getMulticastSender()
:
public interface IMulticastSenderFactory { IMulticastSender getMulticastSender(ServerInfoProvider serverInfoProvider); }
V metodě getMulticastSender()
jsme použili dosud nevytvořené
rozhraní ServerInfoProvider
, které bude představovat rozhraní
na získání informace o aktuálním stavu serveru (identifikátor, obsazenost,
adresu, název...). Pojďme jej přidat:
public interface ServerInfoProvider { IMessage getServerStatusMessage(); }
Rozhraní obsahuje jedinou metodu getServerStatusMessage()
,
která bude vracet zprávu s informacemi o stavu serveru.
Úprava stávajících rozhraní
Nyní přidáme nové metody do již existujících rozhraní, které v
budeme potřebovat při implementaci. Rozhraní IParameterFactory
rozšíříme o bezparametrickou metodu getParameters()
:
public interface IParameterFactory { IParameterProvider getParameters(); // Nově přidaná metoda IParameterProvider getParameters(String[] args); }
Do rozhraní IConnectionManager
přidáme metodu na získání
počtu připojených klientů getConnectedClientCount()
a metodu
getMaxClients()
, která vrátí maximální počet připojených
klientů:
public interface IConnectionManager { void addClient(Socket socket) throws IOException; void onServerStart(); void onServerStop(); int getConnectedClientCount(); // Nově přidaná metoda int getMaxClients(); // Nově přidaná metoda }
Rozhraní IMessage
rozšíříme o defaultní metodu
toByteArray()
, která vytvoří ze třídy serializovaný balík
dat:
default byte[] toByteArray() throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(this); oos.writeByte(0); final byte[] bytes = baos.toByteArray(); assert bytes.length < 1024; return bytes; }
V metodě vytváříme instanci třídy ByteArrayOutputStream
,
kterou předáme jako parametr při vytváření instance
ObjectOutputStream
. Metodou writeObject()
serializujeme naší zprávu a data zapíšeme do streamu. Je nutné přidat
ještě nulový byte, protože jinak by stream na druhé straně nerozpoznal,
kde data končí. Metodou toByteArray()
získáme výsledný balík
dat. Přidal jsem ještě kontrolu, aby data nepřesáhla délku
1024
. Až budeme implementovat klienta, tak buffer, do kterého
budeme číst data, bude mít právě velikost 1024
bajtů.
Nakonec upravíme rozhraní IServerThread
tak, aby dědilo
ještě z rozhraní ServerInfoProvider
:
public interface IServerThread extends IThreadControl, ServerInfoProvider {}
Implementace rozhraní
Když jsme vytvořili a upravili potřebná rozhraní, pojďme je
naimplementovat. Nejdříve vytvoříme implementaci rozhraní
IMulticastSender
pomocí třídy MulticastSender
:
class MulticastSender extends Thread implements IMulticastSender { }
Do třídy přidáme tři konstanty:
private static final long SLEEP_TIME = 2000L; private static final String DEFAULT_MULTICAST_ADDRESS = "224.0.2.50"; private static final int DEFAULT_MULTICAST_PORT = 56489;
Na okamžik bych se zastavil u výchozí multicastové adresy. Adresa
224.0.2.50
spadá do multicast rozsahu 224.0.2.0
-
224.0.255.255
. Packety odesílané v tomto adresním rozsahu budou
putovat napříč celou lokální sítí.
Instanční konstanty budou celkem dvě:
private final IParameterFactory parameterFactory; private final ServerInfoProvider serverInfoProvider;
Instanční proměnné budou čtyři:
private DatagramSocket socket; private InetAddress broadcastAddress; private int port; private boolean interrupt = false;
DatagramSocket
reprezentuje socket, pomocí kterého budeme odesílat datagramové packety.InetAddress
obsahuje broadcastovací adresu, na které budeme naše packet odesílat.- Proměnná
interrupt
má stejný význam, jako v předchozích kapitolách.
Konstruktor třídy bude neveřejný, dostupný pouze v rámci
svého balíčku a bude přijímat dva parametry typu
IParameterFactory
a ServerInfoProvider
:
MulticastSender(IParameterFactory parameterFactory, ServerInfoProvider serverInfoProvider) { super("MulticastSender"); this.parameterFactory = parameterFactory; this.serverInfoProvider = serverInfoProvider; }
V konstruktoru nejdříve nastavíme název vlákna, abychom ho mohli v budoucnu snadno rozlišit. Pak se inicializují instanční konstanty.
Dále vytvoříme privátní metodu, ve které budeme inicializovat adresu a
socket. Metodu nazveme init()
:
private void init() { final IParameterProvider parameterProvider = parameterFactory.getParameters(); try { this.broadcastAddress = InetAddress.getByName(parameterProvider .getString(CmdParser.MULTICAST_ADDRESS, DEFAULT_MULTICAST_ADDRESS)); this.port = parameterProvider.getInteger(CmdParser.MULTICAST_PORT, DEFAULT_MULTICAST_PORT); this.socket = new DatagramSocket(); } catch (IOException e) { throw new RuntimeException(e); } }
Nejdříve se získá instance typu IParameterProvider
z
továrny pomocí bezparametrické metody getParameters()
. Z
parametrů získáme hodnotu broadcastovací adresy. Pokud hodnota nebude k
dispozici, použijeme výchozí hodnotu. Do třídy CmdParser
si
prosím přidejte dva nové atributy: MULTICAST_ADDRESS
a
MULTICAST_PORT
s vlastními hodnotami:
// Adresa, na které se budou vysílat multicastové packety public static final String MULTICAST_ADDRESS = "multicast_address"; // Port, na kterém se budou vysílat multicastové packety public static final String MULTICAST_PORT = "multicast_port";
Nyní budeme implementovat, případně přepisovat metody, které nám
definuje rozhraní IThreadControl
, případně třída
Thread
. Přepíšeme metodu start()
, kterou vybavíme
voláním metody init()
:
@Override public synchronized void start() { init(); super.start(); }
Metoda shutdown()
bude mít stejné tělo, jako v mnoha
ostatních případech:
@Override public void shutdown() { interrupt = true; try { join(); } catch (InterruptedException ignored) { } }
Nejdůležitější metodu run()
jsem si nechal nakonec:
public void run() { if (socket == null || broadcastAddress == null) { interrupt = true; } while(!interrupt) { try { final IMessage serverStatusMessage = serverInfoProvider .getServerStatusMessage(); final byte[] data = serverStatusMessage.toByteArray(); final DatagramPacket datagramPacket = new DatagramPacket( data, data.length, broadcastAddress, port); this.socket.send(datagramPacket); } catch (IOException e) { e.printStackTrace(); break; } try { Thread.sleep(SLEEP_TIME); } catch (InterruptedException ignored) {} } }
Na začátku metody se zjistí, zda-li proběhla inicializace socketu a
adresy úspěšně. Pokud jedna z proměnných bude mít hodnotu
null
, nastaví se proměnná interrupt
na hodnotu
true
, čímž se zajistí, že se vlákno ukončí. V nekonečné
smyčce se získá zpráva s informacemi o serveru a převede se na balík dat.
Tento balík dat se vloží do datagramu a socketem se odešle do světa.
Následuje uspání vlákna na hodnotu konstanty SLEEP_TIME
. Touto
nekonečnou smyčkou zajistíme, že se náš server zviditelní napříč celou
lokální sítí.
Jsme téměř na konci lekce, ale ještě stihneme vytvořit továrnu.
Vytvoříme tedy třídu MulticastSenderFactory
, která
implementuje rozhraní IMulticastSenderFactory
. Rozhraní
vyžaduje, aby třída obsahovala jedinou metodu
getMulticastSender()
:
@Singleton public class MulticastSenderFactory implements IMulticastSenderFactory { private final IParameterFactory parameterFactory; @Inject public MulticastSenderFactory(IParameterFactory parameterFactory) { this.parameterFactory = parameterFactory; } @Override public IMulticastSender getMulticastSender(ServerInfoProvider serverInfoProvider) { return new MulticastSender(parameterFactory, serverInfoProvider); } }
Nakonec zaregistrujeme továrnu v modulu ServerModule
:
bind(IMulticastSenderFactory.class).to(MulticastSenderFactory.class);
To by bylo z první části dnešní lekce vše.
V druhé části, Java server - Propagace lokální sítí (2. část), naimplementujeme zbytek funkcionality na serverové části.