See demoprojekt on hoitud võimalikult lihtsana, et põhifookus hoida mitte mängu loogikal endal, vaid kliendi ja serveri ülespanekul. Siin loodud 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. Hoiame serveri ja kliendi eraldi projektides, et hiljem mugavalt erinevates IDEA akendes nii üht kui teist samaaegselt arendada ja jooksutada.
2.1. Server¶
2.1.1. Setup¶
Loome uue projekti:
File
>New
>Project...
Valime projekti nime, nt
my-server
, jaBuild system:
GradleJDK:
hetkel soovituslikult mõni Java 17 implementatsioonGradle DSL:
Groovy
Lisame
build.gradle
faili 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
Loome uue paki (projekti failipuus parem klikk
src/main/java
kaustale jaNew
>Package
), ntee.example.server
Loome paki sisse kolm uut faili (klassi):
Network.java
ChatServer.java
Main.java
2.1.2. Kood¶
Alustame Network.java
-st:
package ee.example.server;
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryonet.EndPoint;
// This class is a convenient place to keep things common to both the client and server.
public class Network {
public static final int TCP_PORT = 8080;
// This registers objects that are going to be sent over the network.
static public void register(EndPoint endPoint) {
Kryo kryo = endPoint.getKryo();
kryo.register(Message.class);
}
static public class Message {
public String text;
}
}
Siin klassis hoiame kõiki objekte, mida soovime üle võrgu edastada (siin
demos vaid Message
klass). Täpselt samasugune Network.java fail tuleb hiljem ka
kliendipoolele.
Järgmiseks ChatServer.java
:
package ee.example.server;
import com.esotericsoftware.kryonet.Connection;
import com.esotericsoftware.kryonet.FrameworkMessage;
import com.esotericsoftware.kryonet.Listener;
import com.esotericsoftware.kryonet.Server;
import com.esotericsoftware.minlog.Log;
import java.io.IOException;
import static ee.example.server.Network.Message;
public class ChatServer {
private final Server server;
public ChatServer() throws IOException {
Log.set(Log.LEVEL_DEBUG); // Log.LEVEL_INFO for less verbosity
server = new Server();
Network.register(server);
server.addListener(new Listener() {
public void connected(Connection c) {
sendMessageTo(c.getID(),
"Welcome to anonchat. %d other users online.".formatted(server.getConnections().length - 1));
sendMessageToAllExcept(c.getID(),
"User #%d has joined the chat.".formatted(c.getID()));
}
public void disconnected(Connection c) {
sendMessageToAllExcept(c.getID(),
"User #%d has left the chat.".formatted(c.getID()));
}
public void received(Connection c, Object o) {
if (o == null || o instanceof FrameworkMessage.KeepAlive) {
return;
}
if (o instanceof Message) {
Log.info("Received message: '%s' from client with ID='%d' (%s)".formatted(
((Message) o).text, c.getID(), c.getRemoteAddressTCP()));
// Forward the message to other clients
sendMessageToAllExcept(c.getID(),
"#%d: %s".formatted(c.getID(), ((Message) o).text));
}
}
});
server.bind(Network.TCP_PORT);
server.start();
}
private void sendMessageTo(int clientId, String text) {
Message msg = new Message();
msg.text = text;
server.sendToTCP(clientId, msg);
}
private void sendMessageToAllExcept(int clientId, String text) {
Message msg = new Message();
msg.text = text;
server.sendToAllExceptTCP(clientId, msg);
}
}
Siin toimub juba mitu olulist asja. Kõige tähtsam osa võiks hetkel olla, et defineerime, mis juhtub, kui:
1. serveriga ühendub uus klient
(connected(...)
)
2. mõni klient kaotab serveriga ühenduse
(disconnected(...)
)
3. me saame mõnelt juba ühendunud kliendilt
andmeid (received(...)
)
Viimaks Main.java
, mis on hetkel vaid lihtne kest programmi
alustamiseks:
package ee.example.server;
import java.io.IOException;
public class Main {
public static void main(String[] args) {
try {
new ChatServer();
} catch (IOException e) {
e.printStackTrace();
System.err.println("Error starting the server: " + e.getMessage());
System.err.println("Make sure the port is not already in use.");
System.exit(1);
}
}
}
Kui kõik õige, peaksime saama panna tööle Main.java
ja server juba
töötabki!
2.2. Klient¶
2.2.1. Setup¶
Kordame kõike nagu serveri puhul: loome uue projekti, nimeks näiteks
my-client
. Lisame jälle Kryonet-i dependency. Loome paki ee.example.client
ja tekitame Java lähtekoodi
failid: 1. Network.java
2. ChatClient.java
3. Main.java
2.2.2. Kood¶
Nagu ülal öeldud, siis Network.java
on täpselt sama sisuga mis
serveriprojektis (v.a pakinimi). Seda tuleb ka oma projekti tegemisel
silmas pidada.
Järgmiseks ChatClient.java
:
package ee.example.client;
import com.esotericsoftware.kryonet.Client;
import com.esotericsoftware.kryonet.Connection;
import com.esotericsoftware.kryonet.FrameworkMessage;
import com.esotericsoftware.kryonet.Listener;
import com.esotericsoftware.minlog.Log;
import java.io.IOException;
import static ee.example.client.Network.Message;
public class ChatClient {
private final Client client;
private final String SERVER_IP = "127.0.0.1";
public ChatClient() throws IOException {
Log.set(Log.LEVEL_WARN);
client = new Client();
client.start();
Network.register(client);
client.addListener(new Listener.ThreadedListener(new Listener() {
public void connected(Connection c) {
Log.info("Connected to the server: our ID %d".formatted(c.getID()));
}
public void received(Connection c, Object o) {
if (o == null || o instanceof FrameworkMessage.KeepAlive) {
return;
}
if (o instanceof Message) {
System.out.println(((Message) o).text);
}
}
}));
final int timeout = 5000;
client.connect(timeout, SERVER_IP, Network.TCP_PORT);
}
public void sendMessage(String text) {
Message msg = new Message();
msg.text = text;
client.sendTCP(msg);
}
}
Nagu sissejuhatuses räägitud, peab klient teadma serveri IP-d, kuna
klient alustab esimesena vestlust. Muu on ehituselt serveriga sarnane.
Nagu näha, kasutavad nii klient kui server Network.register(...)
funktsiooni, et üheselt määrata, mis tüüpi objekte hakatakse edasi-tagasi edastama.
Viimaks Main.java
:
package ee.example.client;
import java.io.IOException;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
ChatClient client = null;
try {
client = new ChatClient();
} catch (IOException e) {
e.printStackTrace();
System.err.println("Error starting the client: " + e.getMessage());
System.err.println("Make sure the server is up and reachable, and the correct IP address and port are set.");
System.exit(1);
}
Scanner scanner = new Scanner(System.in);
while (true) {
String input = scanner.nextLine();
client.sendMessage(input);
}
}
}
Siin on võrreldes serveripoolega ka natuke mänguloogikat sisse toodud:
kliendi konsool ootab pidevalt uut rida (Enter
vajutamist) - kui
klient seda teeb, saadetakse serverisse ja sealt teistele klientidele
tema sisse trükitud sõnum.
2.3. Mitme kliendiga jooksutamine¶
Proovime mängu tööle panna!
Ühes projektiaknas paneme käima serveri Main.java
.
Teises projektiaknas paneme käima kliendi Main.java
.
Kui kõik õnnestub ja erroreid ei visata, saame kliendi konsoolist kirjutada sõnumeid serverile. Juhuu!
Aga kus on teised kliendid? IntelliJ-s ühes projektis mitme kliendi korraga jooksutamiseks
(samal ajal mitu Main.java
) vali: Current File
>
Run with Parameters...
> Modify options
>
Allow multiple instances
See ongi praegu kõik! Edu projekti alustamisega.
2.4. 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 (
examples
kaustas): https://github.com/EsotericSoftware/kryonet
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.5. 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”.