Lekce 14 - Komunikace Klient/Server - Úprava serveru a klienta
V minulé lekci, Komunikace Klient/Server - Klient, jsme si naprogramovali našeho klienta a vyzkoušeli, jak komunikuje se serverem.
V dnešním Java tutoriálu o síťové komunikaci Klient/Server si server upravíme tak, aby se na něj mohlo připojit více klientů. Také upravíme klienta, aby mohl přijímat zprávy od serveru. Využijeme k tomu znalosti z kurzu o práci s vlákny Multithreading v Javě.
Úprava klienta
V klientovi nejprve vytvoříme nový objekt BufferedReader
s
použitím InputStreamReader
a vstupního proudu soketu
clientSocket
. Tímto způsobem se připraví čtečka, která bude
zpracovávat data obdržená ze serveru.
Dále vytvoříme nové vlákno s názvem receiveThread()
,
které se bude starat o vypsání zprávy obdržené ze
serveru. V cyklu while
zavoláme metodu readLine()
na
instanci serverReader
. Ta pokaždé načte jednu řádku textu ze
serveru. Přečtená zpráva se vypíše v konzoli. V případě, že dojde k
výjimce IOException
, vypíše se její stopa:
this.serverReader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); Thread receiveThread = new Thread(new Runnable() { public void run() { try { while (true) { String message = serverReader.readLine(); System.out.println("Zpráva od serveru: " + message); } } catch (IOException e) { e.printStackTrace(); } } }); receiveThread.start();
Konečný zdrojový klienta pak vypadá takto:
public class Client { private Socket clientSocket; private BufferedReader serverReader; public static void main(String[] args) { Client client = new Client(); } public Client() { try { this.clientSocket = new Socket("localhost", 8080); System.out.println("Spuštění klienta proběhlo úspěšně."); this.serverReader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); Thread receiveThread = new Thread(new Runnable() { public void run() { try { while (true) { String message = serverReader.readLine(); System.out.println("Zpráva od serveru: " + message); } } catch (IOException e) { e.printStackTrace(); } } }); receiveThread.start(); BufferedWriter out = new BufferedWriter(new OutputStreamWriter(this.clientSocket.getOutputStream())); Scanner in = new Scanner(System.in); while (true) { String message = in.nextLine(); if (message.equalsIgnoreCase("exit")) { break; } out.write(message + "\r\n"); out.flush(); System.out.println("Zpráva \"" + message + "\" byla odeslána."); } } catch (IOException e) { e.printStackTrace(); } } }
Úprava serveru
Po server nejdříve vytvoříme novou třídu
ClientHandler
.
Třída ClientHandler
Jedná se o jednoduchou informační třídu, která bude držet informace o
připojeném klientovi. Třída bude při inicializaci přijímat
Socket
a PrintWriter
a bude obsahovat metody:
getSocket()
pro získání soketu klienta,getWriter()
vrátí objektPrintWriter
pro odesílání zpráv klientovi,getAddress()
pro získání řetězce představujícího IP adresu klienta.
Celý kód třídy ClientHandler
bude vypadat takto:
private static class ClientHandler { private Socket socket; private PrintWriter writer; public ClientHandler(Socket socket, PrintWriter writer) { this.socket = socket; this.writer = writer; } public Socket getSocket() { return this.socket; } public PrintWriter getWriter() { return this.writer; } public String getAddress(){ return this.socket.getInetAddress().toString(); } }
Metoda broadcastMessage()
Do třídy Server nyní doplníme dvě privátní metody.
Metodu broadcastMessage()
využijeme pro odeslání zprávy
všem klientům připojeným na serveru.
Každý klient je reprezentován instancí třídy
ClientHandler
, která obsahuje odkaz na objekt
PrintWriter
pomocí kterého bude odeslána zpráva klientovi.
Nejdříve synchronizujeme kolekci klientů, poté vytvoříme
Iterator<ClientHandler>
, který bude procházet jednotlivé
prvky v kolekci clients
a následně bude v cyklu
while
odesílat zprávu jednotlivým klientům:
private void broadcastMessage(String message) { synchronized (clients) { Iterator<ClientHandler> iterator = clients.iterator(); while (iterator.hasNext()) { ClientHandler clientHandler = iterator.next(); clientHandler.getWriter().println(message); clientHandler.getWriter().flush(); } } }
Úprava třídy Server
Třídě Server
přidáme jeden privátní atribut
ArrayList<ClientHandler>
jménem clients
. Z
konstruktoru odstraníme vše kromě inicializace serverSocket
a
přidáme inicializaci clients
. Nakonec zavoláme naši metodu
clients()
, kterou vzápětí doplníme:
public Server() { try { this.serverSocket = new ServerSocket(8080); System.out.print("Spuštění serveru proběhlo úspěšně.\nČekám na připojení klienta...\n"); this.clients = new ArrayList<>(); clients(); } catch (IOException e) { e.printStackTrace(); } }
Metoda clients()
V této metodě nejprve vytvoříme a spustíme nové vlákno jménem
acceptThread
, kterému jako argument předáme rozhraní
Runnable
s metodou run()
:
private void clients() { Thread acceptThread = new Thread(new Runnable() { public void run() { while (true) { ... } } }); acceptThread.start(); }
V cyklu while
poté vytvoříme Socket
pro klienta,
jménem clientSocket
. Inicializujeme PrintWriter
,
který bude přijímat OutputStream
od clientSocket
.
Vytvoříme také instanci třídy ClientHandler
a předáme ji
odkaz na Socket
a PrintWriter
. Synchronizujeme kolekci
clients
typu ArrayList
a přidáme do ní instanci
třídy clientHandler
pro nově připojeného klienta. Pak
vypíšeme hlášku o připojení klienta. Celý kód nezapomeneme obalit blokem
try – catch
:
try { Socket clientSocket = serverSocket.accept(); PrintWriter writer = new PrintWriter(new OutputStreamWriter(clientSocket.getOutputStream()), true); ClientHandler clientHandler = new ClientHandler(clientSocket, writer); synchronized (clients) { clients.add(clientHandler); } System.out.println("Klient" + clientHandler.getAddress() + " se připojil."); } catch (IOException e) { e.printStackTrace(); }
V druhé části naší metody vytvoříme další nekonečný
while
cyklus a v něm sekci synchronized
, abychom
mohli z tohoto vlákna přistupovat do clients
a nedošlo k
race condition:
while(true) { synchronized(clients) { } }
Bez synchronizace by nastal problém, pokud by přistupovalo více vláken k seznamu ve stejnou chvíli. Více informací o synchronizaci vláken naleznete v sekci Vícevláknové aplikace v Javě.
Nyní už zbývá poslední věc. Vytvoříme
Iterator<ClientHandler>
a další while
cyklus.
Inicializujeme objekt BufferedReader
, který slouží ke čtení
zpráv ze vstupního proudu soketu (clientHandler
) klienta. Pokud
bude BufferedReader
nějakého klienta obsahovat text, tak si ho
vypíšeme.
Celý kód druhé části naší metody vypadá následovně:
while (true) { synchronized (clients) { Iterator<ClientHandler> iterator = clients.iterator(); while (iterator.hasNext()) { ClientHandler clientHandler = iterator.next(); try { BufferedReader reader = new BufferedReader(new InputStreamReader(clientHandler.getSocket().getInputStream())); if (reader.ready()) { String message = reader.readLine(); System.out.println("Přijata zpráva od klienta " + clientHandler.getAddress() + " : " + message); broadcastMessage(message); } } catch (IOException e) { e.printStackTrace(); iterator.remove(); // Safely remove the clientHandler from the list } } } }
Nyní můžeme aplikaci spustit a vyzkoušet. Archiv se zdrojovým kódem je ke stažení pod touto lekcí.
V následujícím kvízu, Kvíz - Komunikace Klient/Server v Javě, si vyzkoušíme nabyté zkušenosti z předchozích lekcí.
Měl jsi s čímkoli problém? Stáhni si vzorovou aplikaci níže a porovnej ji se svým projektem, chybu tak snadno najdeš.
Stáhnout
Stažením následujícího souboru souhlasíš s licenčními podmínkami
Staženo 165x (31.24 kB)
Aplikace je včetně zdrojových kódů v jazyce Java