11. diel - Java server - Propagácia lokálnou sieťou (1. časť)
V minulej lekcii, Java server - Systém pluginov , sme sa venovali systému pluginov nášho Java servera. Dnes sa postaráme, aby bol server viditeľný v lokálnej sieti.
Tcp vs. UDP
Než začneme programovať, povieme si trochu teórie o TCP a UDP protokoloch.
Tcp
Skratka znamená Transmission Control Protocol. Jedná sa o protokol, ktorý je spoľahlivý a spojovaný. Spoľahlivý znamená, že dáta, ktoré odošle jeden používateľ, dorazí k cieľu v poriadku a v správnom poradí. Spojovaný znamená, že pred začiatkom komunikácia sa musí vytvoriť spojenie, ktoré sa drží po celú dobu. Používa sa hlavne tam, kde dávame prednosť spoľahlivosti pred rýchlosťou.
UDP
Skratka znamená Universal / User Datagram Protocol. UDP je presný opak TCP. Protokol je nespoľahlivý a spojované. Jednotlivé datagramy môžu prichádzať v rôznom poradí. Protokol nezaručuje, že sa dáta úspešne prenesú - môžu sa cestou stratiť. Používa sa tam, kde je potreba efektívne a rýchlo prenášať dáta, ako sú hry, videá ...
Multicast sender
V predchádzajúcich lekciách sme navrhli komunikačný protokol práve nad TCP, takže máme zaručené, že dáta vždy dorazí v poriadku. Teraz implementujeme zviditeľnenie servera pomocou UDP. Vytvoríme si novú triedu, ktorá bude v nekonečnej slučke v definovanom intervale rozosielať datagram všetkým strojom v lokálnej sieti. Stroj, ktorý nebude vedieť, ako správu spracovať, ju zahodí. My si napíšeme v klientovi obsluhu na príjem týchto datagramov.
V balíčku core
vytvoríme nový balík
multicaster
, v ktorom implementujeme vyššie zmienenú
funkcionalitu.
Návrh rozhrania
Vytvoríme si jednoduché značkovacie rozhranie
IMulticastSender
, ktoré nebude obsahovať žiadnu metódu:
public interface IMulticastSender extends IThreadControl {}
Ďalej rozhranie predstavujúce továreň pre tvorbu inštancií
IMulticastSenderFactory
s metódou
getMulticastSender()
:
public interface IMulticastSenderFactory { IMulticastSender getMulticastSender(ServerInfoProvider serverInfoProvider); }
V metóde getMulticastSender()
sme použili doteraz nie
vytvorené rozhranie ServerInfoProvider
, ktoré bude predstavovať
rozhranie na získanie informácie o aktuálnom stave servera (identifikátor,
obsadenosť, adresu, názov ...):
public interface ServerInfoProvider { IMessage getServerStatusMessage(); }
Rozhranie obsahuje jedinú metódu getServerStatusMessage()
,
ktorá bude vracať správu s informáciami o stave servera.
Úprava existujúcich rozhranie
Teraz pridáme nové metódy do už existujúcich rozhrania, ktoré v budeme
potrebovať pri implementácii. Rozhranie IParameterFactory
rozšírime o bezparametrickou metódu getParameters()
:
public interface IParameterFactory { IParameterProvider getParameters(); // Nově přidaná metoda IParameterProvider getParameters(String[] args); }
Do rozhrania IConnectionManager
pridáme metódu na získanie
počtu pripojených klientov getConnectedClientCount()
a metódu
getMaxClients()
, ktorá vráti maximálny počet pripojených
klientov:
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 }
Rozhranie IMessage
rozšírime o defaultný metódu
toByteArray()
, ktorá vytvorí z triedy serializovaný balík
dát:
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 metóde vytvárame inštanciu triedy ByteArrayOutputStream
,
ktorú odovzdáme ako parameter pri vytváraní inštancie
ObjectOutputStream
. Metódou writeObject()
serializujeme našej správu a dáta zapíšeme do streamu. Je nutné pridať
ešte nulový bajt, pretože inak by stream na druhej strane nerozpoznal, kde
dáta končí. Metódou toByteArray()
získame výsledný balík
dát. Pridal som ešte kontrolu, aby dáta nepresiahla dĺžku
1024
. Až budeme implementovať klienta, tak buffer, do ktorého
budeme čítať dáta, bude mať práve veľkosť 1024
bajtov.
Nakoniec upravíme rozhranie IServerThread
tak, aby dedilo ešte
z rozhrania ServerInfoProvider
:
public interface IServerThread extends IThreadControl, ServerInfoProvider {}
Implementácia rozhrania
Keď sme vytvorili a upravili potrebná rozhranie, poďme je
naimplementovať. Najskôr vytvoríme implementáciu rozhrania
IMulticastSender
pomocou triedy MulticastSender
:
class MulticastSender extends Thread implements IMulticastSender {}
Do triedy pridáme tri konštanty:
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 okamih by som sa zastavil pri predvolené multicastovej adresy. Adresa
224.0.2.50
spadá do multicast rozsahu 224.0.2.0
-
224.0.255.255
. Packety odosielané v tomto adresnom rozsahu budú
putovať naprieč celou lokálnou sieťou.
Inštančný konštanty budú celkom dve:
private final IParameterFactory parameterFactory; private final ServerInfoProvider serverInfoProvider;
Inštančný premenné budú štyri:
private DatagramSocket socket; private InetAddress broadcastAddress; private int port; private boolean interrupt = false;
DatagramSocket
reprezentuje socket, pomocou ktorého budeme odosielať datagramový packety.InetAddress
obsahuje broadcastovací adresu, na ktoré budeme naše packet odosielať.- Premenná
interrupt
má rovnaký význam, ako v predchádzajúcich kapitolách.
Konštruktor triedy bude neverejný, dostupný len v rámci svojho
balíčka a bude prijímať dva parametre typu IParameterFactory
a
ServerInfoProvider
:
MulticastSender(IParameterFactory parameterFactory, ServerInfoProvider serverInfoProvider) { super("MulticastSender"); this.parameterFactory = parameterFactory; this.serverInfoProvider = serverInfoProvider; }
V konstruktoru najskôr nastavíme názov vlákna, aby sme ho mohli v budúcnosti ľahko rozlíšiť. Potom sa inicializujú inštančný konštanty.
Ďalej vytvoríme privátne metódu, v ktorej budeme inicializovať adresu a
socket. Metódu 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); } }
Najskôr sa získa inštancie typu IParameterProvider
z továrne
pomocou bezparametrické metódy getParameters()
. Z parametrov
získame hodnotu broadcastovací adresy. Ak hodnota nebude k dispozícii,
použijeme predvolenú hodnotu. Do triedy CmdParser
si prosím
pridajte dva nové atribúty: 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";
Teraz budeme implementovať, prípadne prepisovať metódy, ktoré nám
definuje rozhranie IThreadControl
, prípadne trieda
Thread
. Prepíšeme metódu start()
, ktorú vybavíme
volaním metódy init()
:
@Override public synchronized void start() { init(); super.start(); }
Metóda shutdown()
bude mať rovnaké telo, ako v mnohých
iných prípadoch:
@Override public void shutdown() { interrupt = true; try { join(); } catch (InterruptedException ignored) { } }
Najdôležitejšie metódu run()
som si nechal nakoniec:
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čiatku metódy sa zistí, či ak prebehla inicializácia socketu a
adresy úspešne. Ak jedna z premenných bude mať hodnotu null
,
nastaví sa premenná interrupt
na hodnotu true
, čím
sa zabezpečí, že sa vlákno ukončí. V nekonečnej slučke sa získa správa
s informáciami o serveri a prevedie sa na balík dát. Tento balík dát sa
vloží do datagramu a socketom sa odošle do sveta. Nasleduje uspanie vlákna
na hodnotu konštanty SLEEP_TIME
. Touto nekonečnú slučkou
zaistíme, že sa náš server zviditeľní naprieč celou lokálnou
sieťou.
Sme takmer na konci lekcie, ale ešte stihneme vytvoriť továreň.
Vytvoríme teda triedu MulticastSenderFactory
, ktorá implementuje
rozhranie IMulticastSenderFactory
. Rozhranie vyžaduje, aby trieda
obsahovala jedinú metódu 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); } }
Nakoniec zaregistrujeme továreň v module ServerModule
:
bind(IMulticastSenderFactory.class).to(MulticastSenderFactory.class);
To by bolo z prvej časti dnešnej lekcie všetko. V druhej časti, Java server - Propagácia lokálnou sieťou (2. časť) , naimplementujeme zvyšok funkcionality na serverovej časti.