RELEASE Ausführliches Tutorial zu ClientFriends

  • Du kannst jetzt eigene Werbung schalten.

    Melde dich einfach via Unterhaltung bei "Artur" und bespreche alles weitere (Preis, Art von Werbung, etc.).

    Wir behalten uns das Recht vor, Werbeanfragen kommentarlos abzulehnen.
  • Bist du schon auf unserem Discord, Guest?

    » https://discordapp.com/invite/XnGCxmp
    Bereits beigetreten? Klicke auf das ✘ oben, rechts.

VertexCode

Member
Premium Member
Sep 24, 2016
19
31
13
23
Okay, da ich grade irgendwie nichts zu tun habe, und sich das öfters mal von mir gewünscht wurde kann man das ja einfach mal machen.

Erstmal: Ihr braucht gewisse Vorkenntnisse (so 100% full skid wird das Tutorial nicht sein.), einen Server mit eigener IP und einen freien Port auf diesem Server.

Gliederung:
1. Login / Username Screen
2. Server Side
3. Client Side
4. Das Modul


1. Login / Username Screen
Das erste das ihr Brauchen werdet wäre ein Login Screen, der den Client Username an den Server sendet, falls ihr AAL o.Ä. benutzt sollte das ganze auch über die InjectionAPI funktionieren.

Dieser GuiScreen sollte legentlich aus einer TextBox bestehen, in diesem Tutorial wird nicht arg auf Sicherheit geachtet, wenn ihr wollt könnt ihr natürlich auch ein Passwort dann mitsenden und Server Side überprüfen.

Ein Beispiel:
Etwas extrem Simples reicht, das ist jetzt z.B. Null



Hierzu werde ich in dem Fall kein Code geben.​

2. Server Side

Ihr werdet ein neues Projekt brauchen, worauf der Server läuft, heißt: IntelliJ / eclipse öffnen, links im Project Explorer Rechtsklick, im falle von IntelliJ Neues Modul, bei eclipse New -> Project. Das könnt ihr nennen wie ihr wollt.

Erstellt ein neues Package, heißt pw.vertexcode.server oder wie auch immer, in diesem package dann die Hauptklasse, z.B. "ClientFriendsServer" als normale Java Datei.

Das erste was in diese Klasse hineinsollte ist die main methode des servers:

Code:
public static void main(String[] args) {
        try {
            new ClientFriendsServer();
        } catch (IOException e) {
            e.printStackTrace();
        }
}
Die IOException wird verwendet da später im Constructor ein neuer ServerSocket erstellt wird, wenn dieser nicht starten kann, z.B. wegen einem belegten Port wird hier der Fehler ausgegeben.

Als nächstes Folgt der Constructor, welcher einen neuen ServerSocket initialisiert.

Code:
public ClientFriendsServer() throws IOException {
        /* Set Instance */
        instance = this;

        /* Init Connection Listener */
        this.socket = new ServerSocket(1616);
        new ConnectionListener(socket);
}

Jetzt werden es erstmal 2 Fehler ausgeben, einer da später in dieser Klasse alle clients abgespeichert werden, und einer da noch nicht die Klasse "ConnectionListener" erstellt wurde.

Der erste Fehler wird einfach behoben mit einem

Code:
public static ClientFriendsServer instance;
am Anfang der klasse.

Der Zweite wird behoben in dem zu der Klasse "ClientFriendsServer" eine weitere namens "ConnectionListener" erstellt.

Diese Klasse macht nichts weiteres als ein neuen Thread zu starten der auf Verbindungen wartet.

Code:
import pw.vertexcode.server.ClientFriendsServer;

import java.net.ServerSocket;
import java.net.Socket;

public class ConnectionListener implements Runnable {

    private ServerSocket listenSocket;

    public ConnectionListener(ServerSocket socket) {
        this.listenSocket = socket;

        new Thread(this, "Main Socket").start();
        System.out.println("Server now listening on port " + listenSocket.getLocalPort());
    }

    public ServerSocket getListenSocket() {
        return listenSocket;
    }

    @Override
    public void run() {
        while (true) {
            try {
                Socket socket = listenSocket.accept();
                Client client = new Client(socket);
                ClientFriendsServer.instance.connectedClients.add(client);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

Der nächste Fehler wird sein das 1. Die klasse "Client" noch nicht existiert, und dass es noch keine Liste namens "connectedClients" gibt.

Heißt selbes Spiel wie gerade eben, neue klasse name: "Client".
Dann wieder zurück in ClientFriendsServer.java und unter die instance:

Code:
public List<Client> connectedClients = new ArrayList<>();
Jetzt werden noch in der Client klasse ein paar Fields hinzugefügt, die einmal den MC Username speichern, den vorhin eingegebenen Account name und einen Thread, der auf anfragen des Clients wartet.
Dazu folgen noch ein paar getter und setter + eine send Methode.

Code:
import java.io.*;
import java.net.Socket;
import java.util.UUID;

public class Client implements Runnable {

   /* Base Information */
   private final Socket socket;

   /* Receive Thread */
   private Thread receiveThread;

   /* Other Information */
   private String mcUsername;
   private String username;

   /**
    * Creates a new client, requires a socket for receiving and sending
    *
    * @param socket client socket
    */
   public Client(Socket socket) {
       this.socket = socket;

       this.receiveThread = new Thread(this, "receive user @ " + socket.getInetAddress().getHostAddress());
       this.receiveThread.start();
   }

   @Override
   public void run() {
       try {
           while (this.isConnected()) {
               BufferedReader br = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
               String line = null;
         
               while ((line = br.readLine()) != null) {
                   handle(line);
               }
           }
       } catch (Exception e) {
           // no need to handle this - only caught on disconnect
       }
       ClientFriendsServer.instance.connectedClients.remove(this);
   }

   private void handle(String line) {
   }

   private void sendUsernames() {
   }

   /**
    * Checks if the socket is still connected / used in run method.
    *
    * @return connected yes / no
    */
   private boolean isConnected() {
       return socket.isConnected() && !socket.isInputShutdown() && !socket.isOutputShutdown() && !socket.isClosed();
   }

   public void send(String data) {
       try {
           BufferedWriter bw = new BufferedWriter(this.socket.getOutputStream());
           bw.write(data);
           bw.newLine();
           bw.flush();
       } catch (Exception e) {
           e.printStackTrace();
       }
   }

   public void setMinecraftUsername(String mcUsername) {
       this.mcUsername = mcUsername;
   }

   public String getMinecraftUsername() {
       return mcUsername;
   }

   public void setUsername(String username) {
       this.username = username;
   }

   public String getUsername() {
       return username;
   }
}

Jetzt folgt noch die handle Methode in der Client.java, welche später die ganzen requests des Servers handelt. In diesem Punkt ist wahrscheinlich auch die größte Schwachstelle, aber das system wird folgendermaßen funktionieren:

Wenn der erste Buchstabe ein u ist, wird der username auf alles nach u gesetzt.
wenn der erste "m" ist, wird der minecraft username auf alles nachfolgende gesetzt.

Der Buchstabe "r" ohne sonstiges wird alle clientsnames zurücksenden.

heißt:

Code:
private void handle(String line) {
    if (line.startsWith("u"))
        this.setUsername(line.substring(1));
    if (line.startsWith("m"))
        this.setMinecraftUsername(line.substring(1));
    if (line.equals("r"))
        this.sendUsernames();
}
** öhh ich bin dize da könnte man ein switch statement verwenden **

Der letzte teil des servers wird nun nur noch die sendUsernames methode sein, welche alle clients nimmt und an den user zurücksendet.

Code:
private void sendUsernames() {
    StringBuilder builder = new StringBuilder("r");

    ClientFriendsServer.instance.connectedClients.stream()
            .filter(client -> (client.getMinecraftUsername() != null))
            .filter(client -> (client.getUsername() != null))
            .forEach(client -> {
                builder.append(":");
                builder.append(client.getUsername());
                builder.append("-");
                builder.append(client.getMinecraftUsername());
            });

    send(builder.toString());
}

Jetzt nurnoch den Server exportieren, und auf einen vserver packen. starten mit java -jar server.jar

offline starten mit screen -dmS server java -jar server.jar
falls screen nicht installiert ist: apt-get install screen

3. Client Side
Als nächstes folgt der Client, das heißt, das was in euer MCP rein muss.
Dafür geht ihr in eure Hauptklasse und erstellt ein neues Field.

Code:
public Connection connection;
dieses Field wird beim client start (onStart Methode oder was ihr auch immer habt) auf eine new Connection gesetzt.

Code:
public void onStart() {
...
    connection = new Connection();
...
}

Da die klasse "Connection" aber noch nicht existiert wird diese erstmal erstellt, dies kann egal wo sein, Hauptsache es ist später in der hauptklasse importiert. Diese Klasse wird der von der Client klasse aus dem Server sehr ähneln, da beides eine Connection, eine send, und eine receive Methode besitzt.

Code:
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

public class Connection implements Runnable {

    /* Socket / Listen Thread */
    private Socket socket;
    private Thread receive;

    /* Streams */
    private OutputStream output;
    private InputStream input;

    /* Online Users */
   private Map<String, String> connectedUsers = new HashMap<>();

    public Connection() {
        try {
            this.socket = new Socket("<server ip des vservers>", 1616);

            this.receive = new Thread(this);
            this.receive.start();

            this.output = this.socket.getOutputStream();
            this.input = this.socket.getInputStream();

            /* Thread that keeps sending the minecraft username */
            new Thread(() -> {
                while (this.isConnected()) {
                    try {
                        Thread.sleep(5 * 1000);
                    } catch (InterruptedException ignored) {
                    }

                    this.send("m" + Minecraft.getMinecraft().getSession().getUsername());
                    this.send("r");
                }
            }, "Server Update").start();
        } catch (IOException e) {
                // Handle offline server
        }
    }

    /**
     * Packet Receive Thread
     */
    @Override
    public void run() {
        try {
            while (this.isConnected()) {          
               BufferedReader br = new BufferedReader(new InputStreamReader(this.input));
               String line = null;
         
               while ((line = br.readLine()) != null) {
                   handle(line);
               }
            }
        } catch (IOException e) {
            System.err.println("An error occurred while receiving ? bytes of data.");
            System.err.println("Line: " + e.getStackTrace()[0].toString());
        }

        System.out.println("something went wrong...");
    }

    private void handle(String line) {
    }

   public void send(String data) {
       try {
           BufferedWriter bw = new BufferedWriter(this.output);
           bw.write(data);
           bw.newLine();
           bw.flush();
       } catch (Exception e) {
           e.printStackTrace();
       }
   }

    /**
     * Checks if the socket is still connected / used in run method.
     *
     * @return connected yes / no
     */
    private boolean isConnected() {
        return socket.isConnected() && !socket.isInputShutdown() && !socket.isOutputShutdown() && !socket.isClosed();
    }
}

Hier gibt es jetzt 2 Besonderheiten: Einmal der Thread der alle 5 Sekunden deinen Minecraft Username an den Server sendet & sich die aktuellsten Daten holt, und dass mal wieder die handle Methode leer ist, in dem teil wird aber dieses Mal nicht der username gesetzt, sondern die clients die der Server sendet verarbeitet.

Also folgt als nächstes die handle Methode:
Diese nimmt den String der zuvor von dem Server kommt wieder auseinander :) = neuer user, - = user + mc name)

Code:
private void handle(String line) {
    if (line.startsWith("r")) line = line.substring(1);

    String[] users = line.split(":");
    connectedUsers.clear();

    for (int i = 1; i < users.length; i++) {
        String user = users[i];
        if (user.contains("-")) {
            String[] userMinecraft = user.split("-");
            String clientUsername = userMinecraft[0];
            String minecraftUsername = userMinecraft[1];

            connectedUsers.put(minecraftUsername, clientUsername);
        }
    }
}

Nachdem das fertig ist sollte theoretisch in der Map "connectedUsers" eine liste mit allen aktuell verbundenen Leuten sein.

Das Letzte was jetzt noch fehlt ist der am Anfang erwähnte GuiLogin, wenn nun man seinen Username eingegeben hat dann sollte ein gewisser Code ausgeführt werden.

Code:
Client.instance.connection.send("u" + username.getText());
Das field username steht für die TextBox.


4. Das Modul

Nachdem es jetzt eine liste mit Connected Usern gibt, muss man eigentlich nur ein neues Modul erstellen, in die Killaura klasse gehen und gucken ob 1. das Modul an ist und 2. Ob der Spieler den man Attacken will als Key in der Map eingespeichert ist.


Paul - Untested stuff
 
Last edited:

CrazyMemeCoke

Administrator
Staff member
Sep 6, 2016
874
2,551
152
Deutschland
www.masterof13fps.de
Super Beitrag, Paul!
Endlich mal jemand, der wirklich Ahnung hat.
 

Flaflo

Senior Developer
Premium Member
Developer
Nov 22, 2016
109
56
43
Germany
flaflo.xyz
Wtf, ich hoffe das spiegelt nicht deine wahren Kompetenzen wieder sonst hast du mich echt enttäuscht.
Alleine die Tatsache das Packets Strings sind die mit einem Anfangsbuchstaben auseinander gehalten werden.
Warum vermittelt man solche Tabus der Networkingprogrammierung. Pfui.

Packets sollten zumindest eine packet id als byte haben und ihre länge mitschicken.
 
  • Like
Reactions: Marco_MC

VertexCode

Member
Premium Member
Sep 24, 2016
19
31
13
23
Wird alles in meinem Client gemacht, mir ist klar dass das hier der größte Krebs code ist... xd
 
  • Like
Reactions: Flaflo

sn0wz

New Member
Jul 26, 2017
9
0
1
Code:
public void send(String data) {
       try {
           BufferedWriter bw = new BufferedWriter(this.socket.getOutputStream());
           bw.write(data);
           bw.newLine();
           bw.flush();
       } catch (Exception e) {
           e.printStackTrace();
       }
   }
Hierzu: bei mir ist das falsch... Was so ich machen?
 
Last edited by a moderator:

Doritoboyxxxx

New Member
Sep 27, 2016
180
26
28
22
Code:
public void send(String data) {
       try {
           BufferedWriter bw = new BufferedWriter(this.socket.getOutputStream());
           bw.write(data);
           bw.newLine();
           bw.flush();
       } catch (Exception e) {
           e.printStackTrace();
       }
   }
Hierzu: bei mir ist das falsch... Was so ich machen?
Was soll man den bitte darauf antworten xD
 

Gareth

New Member
Jul 1, 2018
1
0
1
Wenn man java auch nur ein wenig kann müsste man das eig alles können XD
 

sn0wz

New Member
Jul 26, 2017
9
0
1
Schalom 1 Jahr später. Kannst du bitte das Problem mit dem BufferedWriter genauer erklären? Danke