Aller au contenu
Professeur

Les interactions Client <-> Serveur

Recommended Posts

Un peu de théorie

Le code de Minecraft peut être divisé en 2 « côtés » : le Client et le Serveur.

  • Le côté Serveur est chargé de la gestion du monde partagé par les clients connectés : rafraîchissement des blocs et des entités basé sur les packets qu'il reçoit du client, et envoi des informations mises à jour à tous les clients.
  • Le côté Client est là pour être à l'écoute du des actions du joueur (clics souris, clavier, etc.) et pour dessiner la fenêtre.
Il n'y a qu'un seul serveur, et plusieurs clients qui se connectent dessus. Même en mode solo, le serveur et le client tournent simultanément (dans des Threads séparés).

Certaines parties du code sont « communes » : elles sont utilisées à la fois par le client et par le serveur. Mais alors, comment savoir depuis un code qu'on écrit à la fois pour le Client et le Serveur qui l'a appelé ?

Depuis presque n'importe où dans le jeu, on peut avoir accès à un objet World (passé en field ou paramètre dans la plupart des cas). Dans ce cas là, il suffira de tester le booléen World.isRemote :

  • Si World.isRemote vaut true : on est côté Server.
  • Si World.isRemote vaut false : on est côté Client.
Quand le Client et le Serveur ont besoins d'être synchronisés, ils échangent des informations par le réseau (LAN ou non).

Même quand on est en mode solo, les deux côtés du jeu sont complètement séparés et n'accèdent pas aux objets de l'autre. Si le code executé (par exemple) côté Serveur tente d'accéder à des objets du côté Client, vous aurez affaire à des crash aléatoires ou des comportements étranges. Et de toute manière, votre mod ne pourra pas du tout fonctionner sur un serveur dédié (en multijoueur).

Le communication Client <--> Server en vanilla passe par des « packets », qui sont envoyés et reçus depuis les classe NetHandlerPlayClient et NetHandlerPlayServer, respectivement côté Client et Serveur. Un packet est un ensemble de données, qui peuvent être échangées entre le Client et le Serveur. Il en existe beaucoup, de différents types, et qui servent à des choses très différentes :

  • Les packets envoyé depuis le Client vers le Serveur servent à :
    • Transmettre la position du joueur et ses mouvements, ainsi que ses intéractions avec l'environnement (casser un bloc, clic sur une entité, etc.)
    • Envoyer des messages chat
  • Les packets envoyé depuis le Serveur vers le Client sont beaucoup plus nombreux, et servent à :
    • Informer le client des mouvements des entités
    • Informer le client des changements de blocs
    • Gérer les interfaces graphiques pour les containers
    • Et plein d'autres trucs utiles
Il est essentiel de noter que :
  • Les packets envoyés depuis le Client vers le Serveur commencent avec un C, par exemple C07PacketPlayerDigging.
  • Les packets envoyés depuis le Server vers le Client commencent avec un S, par exemple S06PacketUpdateHealth.
  • Il est possible de créer des packets déjà présents dans Minecraft à la main et de les envoyer directement en utilisant le NetHandler, mais ce n'est presque jamais nécessaire. Si on connais les bonnes méthodes vanilla, elles enverront les packets pour nous (par exemple, appeler ServerConfigurationManager.sendChatMsg() à la place de S02PacketChat).
Communiquer via le réseau avec nos propres packets

Pour envoyer un packet, l'entité réseau (Client ou Serveur) utilise les sockets. Elle écrit le packet sur le flux de sortie en utilisant la classe PacketBuffer, comme nous le renseigne l'interface Packet :

package net.minecraft.network;

import java.io.IOException;

public interface Packet
{
	/**
	 * Utilisé pour lire le contenu d'un packet que l'on reçoit.
	 */
	void readPacketData(PacketBuffer data) throws IOException;

	/**
	 * Utilisé pour écrire le contenu d'un packet que l'on envoie.
	 */
	void writePacketData(PacketBuffer data) throws IOException;

	/**
	 * Appelé lors de la réception du packet.
	 * @param handler Objet INetHandler qui contient toutes les méthodes de déléguation (ex : handleChat, handleEntity...).
	 */
	void processPacket(INetHandler handler);
}

Chaque packet reçu (ou envoyé) est identifié par son numéro ID unique. Depuis la version 1.7, le système de packets a été beaucoup amélioré, de fait que maintenant, on ne manipule plus directement les IDs.

Dans le thread d'écoute d'une entité réseau, seulement l'ID du packet reçu est lue directement. Ensuite, le système récupère une instance du packet correspondant grâce à la méthode EnumConnectionState.getPacket(EnumPacketDirection, int). Ensuite est appelée Packet.readPacketData(PacketBuffer) sur cette instance fraîchement obtenue, puis le packet est délégué.

L'enregistrement de toutes les sortes de packets se fait dans la classe EnumConnectionState, qui représente le stade de connexion d'un client à son serveur. Chaque stade correspond à une phase du jeu :

  • Le « handshaking », qui vérifie que la version du client est compatible avec celle du serveur ;
  • Le « play » (lorsque le client est en jeu), qui sert à envoyer les informations évoquées ci-dessus (entités, changements de map, etc.)
  • Le « status », qui sert à effectuer des requêtes sur le serveur, comme le ping ou l'information du nombre de connectés ;
  • Le « login », qui sert à s'authentifier lors de la connexion au serveur.
Nous allons, pour notre exemple, enregistrer un packet qui enverra au client un texte à afficher sur son écran. Ceci se passera dans la phase de jeu, on ajoute donc l'enregistrement de notre packet dans EnumConnectionState, en dessous de la déclaration du « play » :

// [...]
this.registerPacket(EnumPacketDirection.CLIENTBOUND, S48PacketResourcePackSend.class);
this.registerPacket(EnumPacketDirection.CLIENTBOUND, S49PacketUpdateEntityNBT.class);

// L'enum EnumPacketDirection sert à définir de quel côté le packet arrive. Ici, le client.
this.registerPacket(EnumPacketDirection.CLIENTBOUND, S50PacketDisplayText.class);

// [...]
La classe S50PacketDisplayText n'est pas trouvée ? Alors créons là !

package net.minecraft.network.play.server;

import java.io.IOException;

import net.minecraft.network.INetHandler;
import net.minecraft.network.Packet;
import net.minecraft.network.PacketBuffer;

public class S50PacketDisplayText implements Packet
{
	public S50PacketDisplayText() {}
	
	// L'implémentation de Packet nous oblige à implémenter ses méthodes
	
	@Override
	public void readPacketData(PacketBuffer data) throws IOException
	{
		
	}

	@Override
	public void writePacketData(PacketBuffer data) throws IOException
	{
		
	}

	@Override
	public void processPacket(INetHandler handler)
	{
		
	}

}
Nous avons expliqué ci-dessus que la réception des packets se fait en en créant une instance nouvelle. C'est pourquoi il faut laisse un constructeur sans arguments !

Nous pouvons maintenant ajouter les fields qui vont contenir les informations qu'on souhaite faire transiter. Dans notre cas, il n'y en aura qu'un :

private String displayText;
Et pour la commodité, ajouter un constructeur qui permette directement de renseigner la chaîne de caractères :

public S50PacketDisplayText(String text) 
{
	this.displayText = text;
}
Ainsi qu'une méthode pour récupérer le texte :

public String getDisplayText()
{
	return this.displayText;
}
Puis, il faut remplir les méthodes writePacketData et readPacketData, respectivement en y écrivant et lisant nos fields :

@Override
public void readPacketData(PacketBuffer data) throws IOException
{
	// Le 256 est la taille maximum de la chaîne de caractères
	this.displayText = data.readStringFromBuffer(256);
}

@Override
public void writePacketData(PacketBuffer data) throws IOException
{
	data.writeString(this.displayText);
}
Notre packet est presque terminé ! Il manque l'appel de la délégation lors de son arrivée. Dans l'interface INetHandlerPlayClient, on créée donc la méthode :

void handleDisplayText(S50PacketDisplayText packetDisplayText);
Nous serons obligés de l'ajouter également dans les classes qui l'implémentent, c'est à dire NetHandlerPlayClient :

@Override
public void handleDisplayText(S50PacketDisplayText packetDisplayText)
{
	System.out.println("Packet reçu avec le texte : " + packetDisplayText.displayText);
}
Et enfin, il nous reste à modifier la méthode processPacket(INetHandler) de notre S50PacketDisplayText :

@Override
public void processPacket(INetHandler handler)
{
	((INetHandlerPlayClient) handler).handleDisplayText(this);
}

Pour toute question, avis, suggestion, merci de vous rendre sur

Merci à Wytrem pour sa contribution.

  • Upvote 5

Partager ce message


Lien à poster
Partager sur d’autres sites

Créer un compte ou se connecter pour commenter

Vous devez être membre afin de pouvoir déposer un commentaire

Créer un compte

Créez un compte sur notre communauté. C’est facile !

Créer un nouveau compte

Se connecter

Vous avez déjà un compte ? Connectez-vous ici.

Connectez-vous maintenant

×