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 🥷

img_7.png

Õ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

  1. Loome uue projekti: File > New > Project...

  2. Valime projekti nime, nt my-server, ja

    • Build system: Gradle

    • JDK: hetkel soovituslikult mõni Java 17 implementatsioon

    • Gradle DSL: Groovy

    • img_3.png

  3. Lisame build.gradle faili KryoNet-i dependency:

    // ...
    dependencies {
        // ...
        implementation 'com.esotericsoftware:kryonet:2.22.0-RC1'
    }
    // ...
    
  4. Vajutame ilmunud “Load Gradle Changes” ikoonile img_4.png

    NB!

    Kui ikooni ei ilmu, võib kontrollida ekraani allservast, kas background taskid jooksevad veel

img_troubleshoot.png

  1. Loome uue paki (projekti failipuus parem klikk src/main/java kaustale ja New > Package), nt ee.example.server

  2. Loome paki sisse kolm uut faili (klassi):

    1. Network.java

    2. ChatServer.java

    3. 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 img_5.png img_6.png

See ongi praegu kõik! Edu projekti alustamisega.


2.4. Demoprojektid ja muud huvitavat


2.5. KKK (Korduma Kippuvad Küsimused)

Kui on küsimusi, kirjuta Discordi

Q: Kas peab tingimisi olema Java 17? KryoNet? Gradle?
A: Ei. Valikuid võite teha muidki, kuid siis võib olla väiksem tõenäosus, et keegi (kas mõni abiõppejõud, otsingumootor või keelemudel) oskab aidata.
  • 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

Q: Miks KryoNet-i vaja on?
A: KryoNet lihtsustab meie jaoks oluliselt madalama taseme võrgundust. Peale TCP/UDP ühenduste loomise ja hoidmise ning mugavate API-de nagu 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”.