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í objektPrintWriterpro 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 167x (31.24 kB)
Aplikace je včetně zdrojových kódů v jazyce Java
