See demoprojekt on hoitud võimalikult lihtsana, et põhifookus hoida mitte mängu loogikal endal, vaid kliendi ja serveri ülespanekul ning selle korralikul ja õige arhitektuuri loomisel. Siinloodud serveri transformeerime hiljem üheks rasvaseks jar-failiks ja paneme selle ülikooli masinasse kliente serveerima.
2. Klient & server: chat demo¶
Paneme püsti oma esimese “mängu” anonchat, kus kasutajad saavad üksteisega väga salajaselt sõnumineerida 🥷

Õpetus eeldab, et kasutame IntelliJ IDEA’d ja et meil juba olemas mingi esialgne projekt loodud libGDXi abiga. Hoiame serveri ja kliendi ühes projetkis, mis teeb koodi jagamise ja ühiste seadistuste kasutamise lihtsamaks.
2.1. Üldine setup¶
Loome juuritasandil uued moodulid
sharedjaserverParemklõps juurkataloogil>New>Module...
Sõltuvalt sellest, kuidas te oma projekti libGDX kaudu seadistasite, võivad teie projekti arhitektuuris olla mõned erinevused, kuid peamine on, et need kaks moodulit oleksid loodud.
Lisame
shared,corejaservermodulitesbuild.gradlefaili KryoNet-i dependency:// ... dependencies { // ... implementation 'com.esotericsoftware:kryonet:2.22.0-RC1' } // ...
- Vajutame ilmunud “Load Gradle Changes” ikoonile

NB!
Kui ikooni ei ilmu, võib kontrollida ekraani allservast, kas background taskid jooksevad veel
- Vajutame ilmunud “Load Gradle Changes” ikoonile

2.3. Klient¶
2.3.1. Setup¶
Loome uue paki (
core/src/main/javakaustale), ntee.example.clientLoome paki sisse uut faili
ChatClient.javaLoome selle paki sisse veel kaks uut paki, nt
listenerjamanagerLoome vastavatesse pakkidesse failid
ClientListener.javajaClientManager.javaSee peaks tulema nii välja:

Kuna me kasutame objekte kaustast
shared, siis tuleb lisada sõltuvus ka meie failibuild.gradlekaustascore// ... dependencies { // ... implementation 'com.esotericsoftware:kryonet:2.22.0-RC1' implementation project(':shared') } // ...
2.3.2. Kood¶
Alustame ChatClient.java:
package ee.example.client;
import com.esotericsoftware.kryonet.Client;
import com.esotericsoftware.minlog.Log;
import ee.example.client.listener.ClientListener;
import messages.TextMsg;
import network.register.Register;
import java.io.IOException;
import java.util.Scanner;
public class ChatClient {
private static final String SERVER_IP = "127.0.0.1";
private static final int TCP_PORT = 8080;
private final Client client;
private final ClientListener clientListener;
public ChatClient() throws IOException {
Log.set(Log.LEVEL_INFO); // Log.LEVEL_INFO to reduce verbosity in logs
client = new Client(); // Creating a KryoNet client
client.start(); // Start the client's networking threads
clientListener = new ClientListener(this);
// Register all classes that will be sent over the network
// That's why we created a separate Register class in shared,
// so that we could write object registration in a single line
Register.register(client.getKryo());
bindServer(); // Connect to the server
client.addListener(clientListener); // Add client event listener
}
private void bindServer() throws IOException {
try {
// Connect to server with a 15-second timeout
client.connect(15000, SERVER_IP, TCP_PORT);
System.out.println("Connected to server: " + SERVER_IP + ":" + TCP_PORT);
} catch (IOException e) {
e.printStackTrace();
System.err.println("Error starting the client: " + e.getMessage());
System.err.println("Make sure the server is running, reachable, and that the correct IP address and port are set.");
System.exit(1);
throw e;
}
}
public Client getClient() {
return client;
}
public void sendMessage(String text) {
// Create a message object with text
TextMsg msg = new TextMsg(text, client.getID());
client.sendTCP(msg); // Send a message via TCP
}
public static void main(String[] args) throws IOException {
ChatClient client = new ChatClient();
// Reading messages from the console and sending them to the server
Scanner scanner = new Scanner(System.in);
while (true) {
String input = scanner.nextLine();
client.sendMessage(input);
}
}
}
Järgmiseks ClientListener.java:
package ee.example.client.listener;
import com.esotericsoftware.kryonet.Connection;
import com.esotericsoftware.kryonet.FrameworkMessage;
import com.esotericsoftware.kryonet.Listener;
import com.esotericsoftware.minlog.Log;
import ee.example.client.ChatClient;
import ee.example.client.manager.ClientManager;
import messages.PeopleCountMsg;
import messages.TextMsg;
public class ClientListener extends Listener {
private final ClientManager clientManager;
public ClientListener(ChatClient chatClient) {
// Initialize the manager responsible for handling client-side logic
clientManager = new ClientManager(chatClient);
}
public void connected(Connection c) {
// Called when the client successfully connects to the server
clientManager.playerConnected(c);
}
@Override
public void received(Connection c, Object object) {
// Called when a message or object is received from the server
switch (object) {
case TextMsg message ->
// Handle incoming chat message from the server
clientManager.playerSentText(message);
case PeopleCountMsg message ->
// Handle message containing the number of connected players
clientManager.numberOfPeople(message);
case FrameworkMessage.KeepAlive ignored ->
// Handle KryoNet's KeepAlive heartbeat message
// (used internally to keep the TCP connection alive)
Log.debug("Received KeepAlive message");
default -> {
// Can be ignored
}
}
}
}
Lõpuks ClientManager.java:
package ee.example.client.manager;
import com.esotericsoftware.kryonet.Connection;
import ee.example.client.ChatClient;
import messages.PeopleCountMsg;
import messages.TextMsg;
public class ClientManager {
private final ChatClient chatClient;
public ClientManager(ChatClient chatClient) {
// Store reference to the main ChatClient to send messages back to server
this.chatClient = chatClient;
}
public void playerConnected(Connection c) {
// Called when connection to the server is established
// Print out the unique connection ID assigned by the server
System.out.printf("Connected to the server: your ID is %d%n", c.getID());
// Send a PeopleCountMsg to the server to request current number of connected clients
PeopleCountMsg msg = new PeopleCountMsg();
chatClient.getClient().sendTCP(msg);
}
public void playerSentText(TextMsg message) {
// Handle incoming chat message
if (message.isSentByServer()) {
// If message was not sent by any player, just print the text
System.out.println(message.getText());
} else {
// Otherwise, print message with sender's ID for identification
System.out.println("#" + message.getSenderId() + ": " + message.getText());
}
}
public void numberOfPeople(PeopleCountMsg message) {
// Handle message containing number of connected people
// Print a welcome message showing how many other users are online
String text = "Welcome to anonchat. " + message.getNumber() + " other users online.";
System.out.println(text);
}
}
Mis siin toimub?
Klient käivitub ja ühendub serveriga.
Ühendamiseks peab klient teadma serveri IP-aadressi ja porti (meie puhul
127.0.0.1ja port8080).Klient kuulab klaviatuurilt sisestust ja saadab sisestatud teksti serverile.
Klient võtab serverilt sõnumeid vastu ja kuvab need konsoolis.
Ühendudes saab klient unikaalse ID, mille määrab server, ning teab, kui palju inimesi on hetkel võrku ühendatud.
Selleks, et klient saaks serverile sõnumi saata, tuleb kasutada käsku
client.sendTCP()(võiclient.sendUDP()). Just sellepärast edastame meChatClientobjektiClientListenerkaudu edasiClientManagerklassi — et sealt saaksime vajadusel kliendi objekti kätte (getClient()meetodiga) ja sõnumi saata.See on hea näide sellest, kuidas objektid OOP-s saavad üksteisele edasi anda viiteid ja andmeid, et teha koostööd ilma kogu loogikat ühte kohta koondamata.
Sissetulevate sõnumite töötlemiseks on kliendil eraldi klass
ClientListener.ClientListener'itähtsamad sündmused on:connected— kui klient on serveriga ühendatud,disconnected— kui ühendus katkeb,received— kui serverilt tuleb sõnumeid või andmeid.
Just
receivedmeetod võtab vastu kõik serveri sõnumid ja andmed.Võiks kirjutada töötlemise otse listeneri lisamisel,
/// ... client.addListener(new Listener() { // message processing here }); // ...
kuid koodi kasvades muutub see ebamugavaks. Seetõttu eraldasime loogika eraldi klassi, et kood oleks puhtam ja lihtsam hooldada.
Tegemist on klassiga
ClientManager, kuhu pannakse sõnumite töötlemise loogika — mida teha saadud andmetega.Me oleksime võinud kirjutada kogu loogika, kuidas serverilt saadud sõnumitega töötada, otse
ClientListenerklassi, kuid siis muutuks kood liiga suureks ja keeruliseks. Seetõttu eraldasime selle loogika eraldi klassiClientManager, mis teeb koodi puhtamaks ja lihtsamaks hallata.Tulevikus saab luua mitu erinevat managerit, olenevalt ülesannetest või meeskonna oskustest, näiteks
LobbyManagervõiPlayerManager. See aitab paremini organiseerida koodi ja jagada vastutust erinevate funktsioonide vahel.ClientListenerainult võtab sõnumeid vastu, agaClientManagerotsustab, kuidas nendele reageerida.
Kliendiga meil on kõik valmis, liigume serverile!
2.4. Server¶
2.4.1. Setup¶
Kordame kõike nagu kliendi puhul: loome uue paki, nimeks näiteks
ee.example.server, mille sees on veel kaks paki listener ja manager. Loome vastavatesse
pakkidesse failid ChatServer.java ja ServerListener.java ja ServerManager.java.
Lisame jälle sõltuvus ka meie faili build.gradle
// ...
dependencies {
// ...
implementation 'com.esotericsoftware:kryonet:2.22.0-RC1'
implementation project(':shared')
}
// ...
2.4.2. Kood¶
Alustame ChatServer.java:
package ee.example.server;
import com.esotericsoftware.kryonet.Server;
import com.esotericsoftware.minlog.Log;
import ee.example.server.listener.ServerListener;
import network.register.Register;
import java.io.IOException;
public class ChatServer {
private static final int TCP_PORT = 8080;
private final Server server;
private final ServerListener serverListener;
public ChatServer() throws IOException {
Log.set(Log.LEVEL_INFO);
server = new Server();
serverListener = new ServerListener(this);
Register.register(server.getKryo());
bindServer();
server.addListener(serverListener);
server.start();
}
private void bindServer() throws IOException {
try {
server.bind(TCP_PORT);
System.out.println("Server started on port: " + TCP_PORT);
} catch (IOException e) {
System.err.println("Error starting the server: " + e.getMessage());
System.err.println("Failed to bind server!");
throw e;
}
}
public Server getServer() {
return server;
}
public static void main(String[] args) throws IOException {
ChatServer server = new ChatServer();
}
}
Järgmiseks ServerListener.java:
package ee.example.server.listener;
import com.esotericsoftware.kryonet.Connection;
import com.esotericsoftware.kryonet.FrameworkMessage;
import com.esotericsoftware.kryonet.Listener;
import com.esotericsoftware.minlog.Log;
import ee.example.server.ChatServer;
import ee.example.server.manager.ServerManager;
import messages.PeopleCountMsg;
import messages.TextMsg;
public class ServerListener extends Listener {
private final ServerManager serverManager;
public ServerListener(ChatServer chatServer) {
serverManager = new ServerManager(chatServer);
}
public void connected(Connection c) {
serverManager.playerConnected(c);
}
public void disconnected(Connection c) {
serverManager.playerDisconnected(c);
}
@Override
public void received(Connection c, Object object) {
switch (object) {
case TextMsg message -> serverManager.playerSentText(c, message);
case PeopleCountMsg message -> serverManager.numberOfPeopleRequest(c, message);
case FrameworkMessage.KeepAlive ignored ->
Log.debug("Received KeepAlive message"); // Handles KryoNet's heartbeat packet to keep the connection alive
default -> {
// Can be ignored
}
}
}
}
Lõpuks ServerManager.java
package ee.example.server.manager;
import com.esotericsoftware.kryonet.Connection;
import ee.example.server.ChatServer;
import messages.PeopleCountMsg;
import messages.TextMsg;
import java.util.ArrayList;
import java.util.List;
public class ServerManager {
private final ChatServer chatServer;
private final List<Integer> players = new ArrayList<>();
public ServerManager(ChatServer chatServer) {
this.chatServer = chatServer;
}
public void playerConnected(Connection c) {
int clientId = c.getID();
players.add(clientId);
String text = "User #%d has joined the chat.".formatted(clientId);
sendMessageToAllExcept(clientId, text);
}
public void playerDisconnected(Connection c) {
int clientId = c.getID();
players.removeIf(id -> id == clientId);
String text = "User #%d has left the chat.".formatted(clientId);
sendMessageToAllExcept(clientId, text);
}
public void playerSentText(Connection c, TextMsg message) {
String text = "Received message: '%s' from client with ID='%d'"
.formatted(message.getText(), c.getID());
System.out.println(text);
sendMessageToAll(message.getText(), c.getID());
}
public void numberOfPeopleRequest(Connection c, PeopleCountMsg message) {
message.setNumber(players.size() - 1);
chatServer.getServer().sendToTCP(c.getID(), message);
}
private void sendMessageToAll(String text, int id) {
TextMsg msg = new TextMsg(text, id);
chatServer.getServer().sendToAllTCP(msg);
}
private void sendMessageToAllExcept(int clientId, String text) {
TextMsg msg = new TextMsg(text, -1);
msg.setSentByServer();
chatServer.getServer().sendToAllExceptTCP(clientId, msg);
}
}
Üldine struktuur on kliendipoolega sarnane. Need klassid (
ChatServer,ServerListener,ServerManager) on arhitektuurilt väga sarnased kliendipoolsetele (ChatClient,ClientListener,ClientManager). Rollid on jaotatud samamoodi, aga töötavad kahes erinevas otsas.Samamoodi registreerime kõik sõnumiklassid serveri poolel ning loogikat jagama eraldi klassidesse.
ServerListeneriskasutatakse samu sündmusi.Server saab klientidele sõnumeid saata järgmiste käskudega:
server.sendToTCP(id, object)— saadab sõnumi konkreetsele kliendile.server.sendToAllTCP(object)— saadab sõnumi kõigile klientidele.server.sendToAllExceptTCP(id, object)— saadab sõnumi kõigile peale ühe kliendi.
Nende käskude jaoks on vaja Server objekti, mis luuakse
ChatServerklassis ja antakse edasi teistesse klassidesse. Selle saab kätte meetodigagetServer(), täpselt samamoodi nagu kliendipoolel kasutataksegetClient()meetodit sõnumite saatmiseks serverile.
Serveriga meil on kõik. Proovime meie chati tööle panna!
2.5. Chati käivitamine ja töövoog¶
2.5.1. Chati käivitamine¶
Serveri kaustas paneme käima serveri ChatServer.java.
Core kaustas paneme käima kliendi ChatClient.java.
Kui kõik õnnestub ja erroreid ei visata, saame kliendi konsoolist kirjutada sõnumeid. Juhuu!
Aga kus on teised kliendid? IntelliJ-s ühes projektis mitme kliendi korraga jooksutamiseks
(samal ajal mitu ChatClient.java) vali: Current File >
Run with Parameters... > Modify options >
Allow multiple instances

2.5.2. Chati töövoog serveri ja kliendi tasemel¶
Klient ühendub serveriga ->
connectedsündmus nii kliendil, kui ka serverilServer:
Ühendunud klient lisatakse klientide nimekirja, et jälgida online olevate inimeste arvu.
Kõigile ülejäänud klientidele saadetakse
TextMsg, mis sisaldab teavitust, et uus klient liitus. Sõnumil on märge, et see pärineb serverist, mitte mõnelt konkreetse ID-ga kliendilt. (Alternatiivselt oleks võinud luua eraldi ntNotificationMsg, et eristada teadete tüüpi ja lihtsustadaTextMsgkasutust.)
Klient
Konsooli prinditakse serveri määratud kliendi ID.
Serverile saadetakse
PeopleCountMsgpalvega teada saada online olevate inimeste arv. Server tagastab vastuse, mille järel kliendi konsoolis kuvatakse tervitusteade.
Sõnumi saatmine
Kui klient kirjutab teksti konsooli, saadetakse serverile
TextMsg.Server prindib konsooli info saadetud sõnumi ja selle autori kohta.
Seejärel saadetakse sõnum kõigile klientidele, sealhulgas saatjale, et nende terminalis kuvataks tekst koos saatja ID-ga.
Klient lahkub ->
disconnectedsündmus serverilKliendi ID eemaldatakse online klientide nimekirjast.
Kõigile ülejäänud klientidele saadetakse teade, et konkreetne klient lahkus.
See ongi praegu kõik! Edu projekti alustamisega.
2.6. Demoprojektid ja muud huvitavat¶
Vaata ka neid demoprojekte, mis meenutavad juba rohkem mängu:
eestikeelse juhendiga: https://gitlab.cs.taltech.ee/karaud/SampleKryo/
KryoNet-i enda näited (
exampleskaustas): https://github.com/EsotericSoftware/kryonet- Samuti näide meie mängu klient-server arhitektuurist: https://github.com/Rig14/example-game/tree/main.
Kataloogis "server" asub serveri kood, kataloogis "shared" on mõned ühised (nii kliendi kui ka serveri jaoks) kasulikud objektid (konstandid ja paketid).
Kliendi ühendus serveriga asub siin: "core/src/main/java/ee/taltech/examplegame/network"
Ühenduse kasutamine toimub peamiselt siin: "core/src/main/java/ee/taltech/examplegame/game"
Võib olla huvitav analüüsida loodavat võrguliiklust ka baitide tasemel, näiteks Wireshark’i abil:

Tihti tasub enne koodi kirjutamist sketšida välja, kuidas suhtlus klientide-serveri vahel olema hakkab. Selleks võib olla kasulik nt paber või https://excalidraw.com/
2.7. KKK (Korduma Kippuvad Küsimused)¶
Kui on küsimusi, kirjuta Discordi
Java: 2023. a sügisel tuli välja viimane Java LTS (Long-Term Support) versioon 21. Kuna nii KryoNet kui LibGDX on vanemad olijad ja uhiuue Java 21 implementatsioonid võivad olla veel “välja lihvimata”, võib tekkida probleeme (ei pruugi, katsetage)
Gradle: kuna enamik varasemaid projekte on Gradle’ga, soovitame Gradle’t (mitte nt Maven). Lisaks, 2023. a on Gradle’i vaikimisi DSL (Domain Specific Language) Kotlin, mitte Groovy. Hetkel aga tundub, et palju rohkem näiteid leidub internetist Groovy-ga
KryoNet: kuna enamik varasemaid projekte on KryoNet’iga, soovitame KryoNet-i (mitte nt Netty, mis vajab rohkem isetegemist). KryoNet-i pole küll pikalt uuendatud, kuid see on lihtne ja töötab hästi (TCP pole muutunud). KryoNet-ist on ka uuendatud fork’e, kõige populaarsem crykn/kryonet - võib jällegi katsetada ja öelda, kuidas läks
sendToAllExceptTCP(...) pakkumise kasutab KryoNet
sisemiselt Kryo-t, mis teisendab mälus olevad Java objektid (nt
Message) üle võrgu saadetavateks baitideks. Seda protsessi
nimetatakse “serialiseerimiseks”
(serialization/marshalling/pickling)
ja vastupidist, nö raw bytes -> object, nimetatakse
“deserialiseerimiseks”.