[1.7.2 ++] Le network
-
Sommaire
Introduction
Depuis la 1.7, le système de packet Forge n’est plus le même. Il a été totalement refait, nous utilisons maintenant le framework Netty.
Je vous post donc la traduction d’un tutoriel réalisé a partir des posts de Diesieben07, modérateur de www.minecraftforge.net et de Ablaze.J’ai une manière différente de Robin ou des autres de concevoir un tutoriel, par conséquent, il est possible qu’il ne se lise pas de la même façon.
J’ai l’habitude de donner des aspects théoriques avec le moins d’exemple possible.
Cependant, dans cette partie théorique c’est à vous de construire vos exemples quand c’est possible. Si c’est possible et que vous réussissez à construire votre propre exemple, c’est que vous avez compris
Ensuite, je donne un exemple abstrait. Si vous avez compris la partie théorique, il vous sert a savoir comment la mettre en application.
Et enfin je finis par un exemple concret, pour ceux qui n’ont rien comprit x) ou ceux qui veulent se rassurer.
Mais pour ça il me faut un thème concret, il est possible que je vous demande de me donner un énoncé, c’est le cas de ce tutoriel.
Je le ferais surement dans les tutoriels qui traitent de notions assez large. Quand j’aurais reçu ou choisis un énoncer qui reprend tout les points du tutoriel, qui n’est pas trop long à mettre en place et qui me plait, je réaliserai l’exemple.
Dans cette exemple concret, je crée étape par étape un mod simple qui utilise toutes les fonctionnalités vu dans le tutoriel.
Attention cependant, essayez de comprendre ce que je vous raconte, ne faites pas un copier/coller de l’exemple.
Si vous n’avez pas comprit le tutoriel et que vous suivez l’exemple concret, relisez le tutoriel avec l’exemple sous les yeux.
Je suis disponible pour répondre a vos questions, celle-ci sont très utile pour vous mais aussi pour moi. Ça me permet d’ettofer le tutoriel pour le rendre accessible à tous.
Idem pour les feedbacks, n’hésitez pasVous l’aurez compris, comme je souhaite faire des tutoriels accessibles au plus grand nombre, mais aussi qui expliquent tout les points, (Je ne vous direz jamais de mettre un boolean à true dans un constructeur sans vous expliquer pourquoi, comme on peut le voir dans certains tutoriels ^^), c’est fortement probable que le tutoriel soit long ^^ Mais bon pas la peine de vous attarder sur les points qui ne vous posent pas de difficultés, vous pouvez lire en diagonale, c’est pour cette raison que je fais des tutos écrit et que je ne ferais jamais de tutos vidéo
Pré-requis
Je n’expliquerai pas dans ce tutoriel les notions liées au langage Java.
Il est donc important de connaitre les notions de base de Java et de la programmation objet pour comprendre ce tutoriel.- Savoir ce qu’est une application client/serveur, la notion de packet.
- Savoir comment Forge sépare le client et le serveur.
- Avoir quelques notions de sécurité.
Lexique
Packet : ensemble de données qui est destiné à transiter à travers le réseau
Message : ce que contient le packet
Handler : N’a pas vraiment de tradution en français, c’est quelque chose charger de prendre en charge une autre chose.
Sérialiser : mettre en forme des données pour qu’elles puissent être inscrite dans un flux d’octect
Désérialiser : l’inverse de sérialiser.
Channel : C’est un canal dans lequel passe des packets
Mod Class : C’est la classe de base de votre mod, celle avec l’annotation @ModUne application client-serveur
Une application client-serveur peut être scindée en 2 parties : la partie cliente et la partie serveur.
En règle générale, le client gère les entrées/sorties. C’est à dire qu’il s’occupe de l’affichage de données reçues du serveur et qu’il envoie au serveur par exemple ce que l’utilisateur rentre au clavier.
Le serveur lui, envoie au client les données à afficher, et reçoit les données qu’entre l’utilisateur.Toutes ses données qui transitent à travers le réseau sont misent sous forme de packets.
Chaque packet contient des données sérialisables, c’est à dire pouvant être transformées en une suite d’octect.
Concernant les packets, pas besoin d’en savoir plus pour comprendre. Cependant, si ça vous interesse, vous pouvez vous renseigner sur le modèle OSIQuand vous jouer solo sur un jeu utilisant une architecture client/serveur, vous envoyez tout de même des packets. Vous les envoyez a vous même via vos adresses locales.
La séparation client/serveur sous Forge
Il est important d’éliminer toutes confusions concernant la séparation client/serveur.
C’est le point sensible, il y a beaucoup de confusion la-dessus.La séparation client/serveur est un modèle de communication standardisé.
Minecraft et donc Forge respectent ce standard.Rappelez-vous, même si vous jouer en solo, vous utilisez tout de même le modèle client/serveur.
Il vous faut donc le serveur pour jouer seul.Par conséquent, il existe 2 applications Minecraft faites par Mojang :
- Le client-serveur.exe (C’est ce que vous avez chez vous)
- Le serveur.exe (C’est ce que vous utilisez quand vous voulez lancer un serveur sur une machine dédiée)
Forge suit ce modèle avec les proxy :
Common représente le code qui sera exécuté à la fois chez client-serveur.exe et serveur.exe. (L’ajout de bloc, d’item, etc…)
Side.Client représente le code qui n’est exécuté QUE chez client-serveur.exe (interface graphique)
Side.Server représente le code qui n’est éxécuté QUE chez serveur.exe. Ce qui signifie que ce code la n’est pas présent sur le client que vous jouer en solo ou en multi via l’hébergement de partie locales intégré.
Vous l’aurez compris, ça n’a que très peu d’intéret. Moi même j’ai du mal à trouver un exemple pour illustrer le cas Side.Server.Nous allons donc oublier cette notion de Side.Server : nous ne l’utiliserons jamais.
Le code uniquement client n’est écrit QUE dans Side.Client c’est à dire client-serveur.exe. Ce qui ne devrait poser aucun soucis de compréhension puisque que vous jouez solo, multi-local ou multi-distant, vous avez besoin d’une interface graphique, d’un render pour afficher les blocs a l’écran etc…
Le code uniquement serveur est inscrit dans Common, et la ça pose des complications !
En effet, le code uniquement serveur est mélangé avec le code commun aux 2 parties.
Ça ne pose pas de problème quand on joue solo ou qu’on héberge un serveur local via le client-serveur.exe.
En revanche, lorsque vous jouez sur un serveur distant, vous n’avez pas besoin d’éxécuter le code uniquement serveur. C’est la machine distante qui le fait.Maintenant on y voit déjà un peu plus clair.
Seulement comment savoir quand nous sommes dans le Common, si nous utilisons un code serveur ou un code commun au client et au serveur ?
Pour cela il y a 3 méthodes :-
lorsque vous avez une variable de type World, vous avez accés à un attribut : isRemote.
Si vous avez suivit, c’est toujours le serveur qui gère le monde, le client lui ne fait que l’afficher.
isRemote signifie : est distant.
Ainsi si le monde est distant, vous êtes dans la partie cliente. Sur serveur.exe, cette attribut renvera toujours faux.
Rappelez-vous, même si vous jouez solo, il y a 2 partie : client et serveur, qui communiquent entre elles via des packets.
Donc même en solo, la partie cliente verra le monde comme “distant”. -
Vous pouvez vérifier le type d’EntityPlayer
Si votre player est de type EntityPlayerMP -> Vous êtes dans la partie serveur -
Vous pouvez utilisez getEffectiveSide()
(J’ai trop peu d’info la dessus pour le moment)
Nous allons prendre comme exemple la sauvegarde du monde.
Si vous avez suivis, cette tache est réservée au serveur.
Immaginons que nous developpons cette tache en faisant un mod.
Nous allons l’écrire dans le CommonProxy, c’est à dire client-serveur.exe et serveur.exe.
Même si c’est une tache serveur, si nous l’écrivont dans le Side.Server, un joueur ne pourra jamais sauvegarder son monde s’il joue au jeu via client-serveur.exe.
Nous allons enregistrer tout les évènements puisque lors de l’init, on ne sait pas si le joueur va faire une partie solo, ou se connecter à un serveur.Voici la signature de notre fonction :
@SubscribeEvent public void onSave(WorldEvent.Save event)
Avant de sauvegarder quoique se soit, il faut vérifier si nous sommes dans la partie serveur ou la partie cliente.
Si vous ne vérifiez pas cette condition le code sera aussi effectué dans la partie cliente, ce que signifie :
Que lorsque vous jouer solo, il sera exécuté 2 fois.
Lorsque vous jouer sur un serveur distant, il sera exécuté alors que c’est le serveur distant qui est chargé de faire les sauvegardes.Voici donc ce qu’il faut ajouter :
@SubscribeEvent public void onSave(WorldEvent.Save event) { if(!event.world.isRemote) { //On sauvegarde ... } }
Bon heureusement, nous n’avons pas à gérer la sauvegarde du monde, c’est déjà dans Minecraft.
Voila pour les proxies et cette séparation, j’espère que vous y voyez maintenant plus clair.Quelques notions de sécurité
Quelques règles simples et générales :
Ne jamais envoyer des données sensibles en clair, c’est à dire sans les crypter.
Ne jamais faire son propre algorithme de chiffrement/cryptage.L’erreur classique des débutants ou des ferrues d’informatique, sachez que ceux qui font les algorithmes de cryptage sont des mathématiciens hautement diplomés.
Votre cryptage sera décryptable au mieux en quelques centième de seconde. Utilisez des algorithmes existants et performant (MD5, SHA etc…)Ne faites jamais confiance aux données envoyées par le client.
Ce n’est pas parceque vous faites un mod ou le client envoye un packet i=6 que vous êtes sur que vous recevrez un packet avec i=6.
N’importe qui peut créer un packet avec ce qu’il veut dedans et vous l’envoyer, sans passer par votre mod.C’est le principe des cheats.
Si i désigne le nombre de dégat infligé à une entité, il peut mettre 50 à la place de 6 et vous l’envoyer.
Ainsi le calcul de dégat doit se faire coté serveur. C’est comme ça que ça fonctionne sur Minecraft.Si i désigne “est ce que je suis en vol ?” et “qu’elle est ma position en y ?”, si vous envoyez un packet avec “oui” et “maPositionY+1” vous pouvez volez.
Enfin ça marchait jusqu’en 1.4 Mojang a corrigé ce problème ensuiteMais bon il existe plein de cheat toujours réalisables via l’envoie de packet.
Sachez que le serveur Mojang ne vérifie pas si vous visez réellement une entité lorsque vous l’attaquez au CAC. Il vérifie juste sa distance.
Il ne vérifie pas non plus si vous voyez le boc que vous essayez de casser ou de placer, ainsi avec un peu de connaissance, vous pouvez casser un bloc qui est derrière un autre.Tout ça ce n’est pas pour vous encourager le faire, bien que bonne chance ^^
Quand vous cheatez, ça se voit, c’est pour ça qu’il faut une police anti-cheat dans un serveur
Bon entre nous honnetement si vous voulez cheater pour cheater, utiliser des softs qui le font déjà, si vous voulez le faire pour apprendre, faites le par vous mêmeNéanmoins tout ça c’est pour insister sur l’aspect “vérification coté serveur” qui est essenstielle ! Il en va de la survie du serveur !
Je vous ai donné des exemples qui permettent au cheater de modifier son gameplay.
Mais imaginons que le serveur va chercher dans monTableau* je ne sais pas quoi.
Même si vous avez codé votre mod de sorte que la donnée envoyée par le client ne peut pas dépasser la taille du tableau ou être négative, un petit *** pourra s’amuser à vous envoyer un packet avec i= -1 et là c’est le crash serveur.Voila pourquoi la survie de votre serveur en dépend.
Tutoriel
Ce qui va suivre est la manière recommandé d’implémenter les Packets en 1.7. Il n’est pas conseillé d’utiliser directement Netty, car ça peut causer des problèmes (NDT : Fuite de mémoire notament).
Enregistrer votre Channel
Premièrement, nous avons besoin d’une instance de SimpleNetworkWrapper pour notre Channel. Vous l’obtenez en utilisant NetworkRegistry.INSTANCE.newSimpleChannel(“votreChannel”).
“votreChannel” doit être un identifiant unique, si vous n’avez qu’un seul channel, vous pouvez par exemple mettre votre MODID.
Stockez le résultat de cette fonction dans un champs static accéssible par tout ce qui utilisera ce channel (ex : dans votre Mod Class, ou votre proxy).Enregistrer un Packet
Les packets sont fabriqués via des classes distinctes pour chaque Packet-ID (Aussi appelé “discriminator” (NDT: donc discriminant en français)). Si vous utilisiez des énormes switch-case en 1.6 comme beaucoup de personnes, c’est le moment de changer.
Pour chaque type de Packet que vous voulez envoyer, nous aurons besoin de 2 classes.
Une pour le Packet qui implémentera l’interface IMessage, et une autre pour prendre en charge le packet qui implémentera IMessageHandler.
Pour avoir un code plus propre, j’implémente IMessageHandler dans une classe interne du Packet.
Pour enregistrer votre Packet, utilisez registerMessage(MyMessageHandler.class, MyMessage.class, packetID, receivingSide) accessible depuis votre variable static de votre Mod Class (voir plus haut : newSimpleChannel).
Les 2 premiers paramètres sont explicites, PacketID est le même qu’en 1.6, habituellement vous pouvez commencer à 0 et incrémenter l’ID pour chaque type de Packet.
Attention gardez en tête que l’ID maximum que vous pouvez utiliser est 255. Si vous avez besoin de plus de packet, vous pouvez ouvrir un autre channel
Le receivingSide est la partie qui reçoit le message et peut être soit Side.CLIENT ou Side.SERVER.Implementation de votre Packet
L’interface IMessage vous demande d’écrire 2 fonctions : fromBytes et toBytes.
Vous pouvez considérer votre implémentation d’IMessage comme un grand conteneur d’octect. Du fait que le réseaux, ce n’est qu’un flux d’octect, vous devez sérialiser et désérialiser votre message dans ce flux.
C’est ce que doivent faire ces 2 fonctions.
toBytes doit écrire vos données dans le flux (pour ça, utilisez les fonctions write*** de ByteBuf) et fromBytes doit lire les données du message vers votre Packet (pour ça, utilisez les fonctions read*** de ByteBuf).
Gardez en tête que vous avez absolument besoin d’écrire le constructeur par défaut (c’est à dire public et sans paramètre) de votre classe (Même s’il est vide), sans quoi FML ne pourra pas l’utiliser.(NDT : FML utilise l’introspection pour gérer vos différents type de packets. Sans un constructeur public sans aucun parametre, il ne pourra pas instancier votre classe)
Implementation de l’Handler de votre Packet
L’interface IMessageHandler requiert simplement une fonction, onMessage.
Cette fonction est appelée lorsque votre packet est reçu, après l’instanciation de votre implémentation d’IMessage et l’appel à votre fonction fromBytes.
Faites ce que vous voulez que votre packet fasse dans cette fonction.Envoie de Packets
La classe SimpleNetworkWrapper vous fournit des fonctions pour envoyer des instances d’IMessage à diverses cibles.
Elles devraient être explicites (sendToServeur, sendTo, etc…).
Si un packet vanilla est requit par votre IMessage (ex : pour utiliser des packets de descriptions de TileEntity), vous pouvez utiliser la fonction getPacketFrom.Réponses de Packets
Cette fonctionnalité est un processus interne à l’interface IMessageHandler.
Déclarez que vous envoyez un packet depuis le client vers le serveur, ensuite vous pourrez juste simplement envoyer une réponse directement depuis la fonction onMessage en renvoyant le Packet de réponse.
C’est à ça que sert le second paramètre d’IMessageHandler. Si vous n’envoyez pas de réponse, vous pouvez laisser IMessage comme second paramètre.Exemple abstrait
@Mod class MyMod { public static SimpleNetworkWrapper network; @EventHandler public void preInit(FMLPreInitializationEvent event) { network = NetworkRegistry.INSTANCE.newSimpleChannel("MyChannel"); network.registerMessage(MyMessage.Handler.class, MyMessage.class, 0, Side.SERVER); // network.registerMessage(SecondMessage.Handler.class, SecondMessage.class, 1, Side.CLIENT); // … } }
class MyMessage implements IMessage { private String text; public MyMessage() { } public MyMessage(String text) { this.text = text; } @Override public void fromBytes(ByteBuf buf) { text = ByteBufUtils.readUTF8String(buf); // this class is very useful in general for writing more complex objects } @Override public void toBytes(ByteBuf buf) { ByteBufUtils.writeUTF8String(buf, text); } public static class Handler implements IMessageHandler <MyMessage, IMessage> { @Override public IMessage onMessage(MyMessage message, MessageContext ctx) { System.out.println(String.format("Received %s from %s", message.text, ctx.getServerHandler().playerEntity.getDisplayName())); return null; // no response in this case } } }
// Sending packets: MyMod.network.sendToServer(new MyMessage("foobar")); MyMod.network.sendTo(new SomeMessage(), somePlayer);
Exemple concret
J’attends vos propositions d’énoncé
Crédits
Rédaction :
- Blackout
Correction :
Mention spéciales :
- Diesieben07
- Ablaze
Ce tutoriel de Minecraft Forge France est mis à disposition selon les termes de la licence Creative Commons Attribution - Pas d’Utilisation Commerciale - Partage dans les Mêmes Conditions 4.0 International -
Voila, j’ai mis en forme le tutoriel
J’attends vos propositions d’énoncé -
pour les énoncer regarde ce qu’a fait kevin_68 pour NanotechMod, je sais qu’il en utilise et dans des cas varié il me semble
-
Le mod est tellement grand, que je ne vais pas m’amuser a check tout ce qui utilise les packets ^^
Puis l’exemple concret ce n’est pas pour moi ^^ Je n’en ai pas besoin. C’est pour vous
Ce n’est pas les idées qui manquent, j’ai plein d’idée de thème que je pourrais traiter.
Mais je préfère que ce soit vous qui me donnez un sujetJ’aurais aimé vous montrer comment exiger une authentification à un site web avant de pouvoir se connecter au serveur, mais ça demande des compétences supplémentaires que tous n’ont pas
-
Juste :
Ainsi si le monde est distant, vous êtes chez le client. Sur serveur.exe, cette attribut renvera toujours faux.
Si vous êtes dans la partie serveur, cette variable est vrai. Donc sur serveur.exe cette variable renvera toujours vrai.Heu ? Petit problème ici non ? (c’est dans l’introduction).___Sinon c’est un très bon tutoriel, ça change du style habituel et ça force le lecteur à bien comprendre et pas juste à copier/coller bêtement les codes, j’aime bien
-
Ha oui effectivement xD
C’est corrigé -
Merci Blackout pour ce tuto, il vas bien m’aider pour enfin comprendre les paquets !
-
Merci pour le tuto, un petit problème (je sais pas si je dois le mettre ici on dans support) quand j’utilise la methode registermessage(…) eclipse me met une erreur (souligné en rouge), donc je met le commentaire qu’il me donne :
The method registerMessage(Class>, Class<req>, int, Side) in the type SimpleNetworkWrapper is not applicable for the arguments (Class<packetshema.packethandler>, Class<packetshema.packet>, int, Side)
Mon code :
:::public static void register(){ Defaults.channel.registerMessage(PacketShema.PacketHandler.class, PacketShema.Packet.class, Defaults.shemapacid, Side.SERVER); }
:::
ma classe PacketShema :
:::public class PacketShema{ public class Packet implements IMessage{ public void fromBytes(ByteBuf buf) { } @Override public void toBytes(ByteBuf buf) { } } public class PacketHandler implements IMessageHandler{ @Override public IMessage onMessage(IMessage message, MessageContext ctx) { return null; } } }
:::
Merci pour votre aide.
Edit : je suis en 1.7.10</packetshema.packet></packetshema.packethandler></req>
-
public class PacketShema{ public class Packet implements IMessage{ public void fromBytes(ByteBuf buf) { } @Override public void toBytes(ByteBuf buf) { } } public class PacketHandler implements IMessageHandler<packet, imessage="">{ @Override public IMessage onMessage(IMessage message, MessageContext ctx) { return null; } } }
Ta classe devrait être comme ça.</packet,>
-
Comme l’a dit Robin, ton IMessageHandler prend 2 parametres de type :
IMessageHandler<packetarecevoir, packetaretourner=“”></packetarecevoir,> -
Ok merci, je vais revoir quelques cours de java sur les impléments ça ne fera pas de mal.
-
Si je veux envoyer un message du client au serveur et avoir une réponse du serveur ( au client logiquement ) je dois faire un autre type de channel ? ( NetworkRegistry.INSTANCE.newChannel ) ou le ( NetworkRegistry.INSTANCE.newSimpleChannel ) suffit ?
-
Tu utilises ton channel pour l’envoie de message, que se soit client ou serveur.
Tu ne crées un nouveau channel que si tu as plus de 255 types de packet. -
Salut j’ai un probleme:
le message ne s’affiche pas dans la console quand je mets ce codepackage com.harrypotter.sosoh.proxy; import io.netty.buffer.ByteBuf; import cpw.mods.fml.common.network.ByteBufUtils; import cpw.mods.fml.common.network.simpleimpl.IMessage; import cpw.mods.fml.common.network.simpleimpl.IMessageHandler; import cpw.mods.fml.common.network.simpleimpl.MessageContext; public class PaquetHandler implements IMessage { private String text; public PaquetHandler() { } public PaquetHandler(String text) { this.text = text; } @Override public void fromBytes(ByteBuf buf) { text = ByteBufUtils.readUTF8String(buf); // this class is very useful in general for writing more complex objects } @Override public void toBytes(ByteBuf buf) { ByteBufUtils.writeUTF8String(buf, text); } public static class Handler implements IMessageHandler <paquethandler, imessage="">{ @Override public IMessage onMessage(PaquetHandler message, MessageContext ctx) { if(message.text == "p"){ System.out.println(String.format("Received %s from %s", message.text, ctx.getServerHandler().playerEntity.getDisplayName())); } return null; // no response in this case } } }
mais si je mets lui oui:
package com.harrypotter.sosoh.proxy; import io.netty.buffer.ByteBuf; import cpw.mods.fml.common.network.ByteBufUtils; import cpw.mods.fml.common.network.simpleimpl.IMessage; import cpw.mods.fml.common.network.simpleimpl.IMessageHandler; import cpw.mods.fml.common.network.simpleimpl.MessageContext; public class PaquetHandler implements IMessage { private String text; public PaquetHandler() { } public PaquetHandler(String text) { this.text = text; } @Override public void fromBytes(ByteBuf buf) { text = ByteBufUtils.readUTF8String(buf); // this class is very useful in general for writing more complex objects } @Override public void toBytes(ByteBuf buf) { ByteBufUtils.writeUTF8String(buf, text); } public static class Handler implements IMessageHandler <paquethandler, imessage="">{ @Override public IMessage onMessage(PaquetHandler message, MessageContext ctx) { System.out.println(String.format("Received %s from %s", message.text, ctx.getServerHandler().playerEntity.getDisplayName())); return null; // no response in this case } } }
le code pour send le packet:
protected void actionPerformed(GuiButton guiButton) { if(guiButton.id == 0) { ModHarryPotter.network.sendToServer(new PaquetHandler("p")); } } ```</paquethandler,></paquethandler,>
-
Pour les String utilise equals plutôt que ==
message.text.equals(“p”) -
Ok merci !
-
Bonjour j’ai suivi ton tuto sur les packets tout est operationnelle (super tuto appart que personne a demandé d’exemple concret ^^)
Mais je ne voie pas quelle méthode rajouter dans la partie implémentation de l’handler pour pouvoir téléporter un joueur à une position x, y, z défini
Merci d’avance
-
GG quand j’aurais le temps je vais venir le grignoté tranquillement ! j’ai trop de retard en ce moment j’ai du refaire mon mod 3 fois d’affilé " une corde svp" !!
-
@‘Antoine_’:
Bonjour j’ai suivi ton tuto sur les packets tout est operationnelle (super tuto appart que personne a demandé d’exemple concret ^^)
Mais je ne voie pas quelle méthode rajouter dans la partie implémentation de l’handler pour pouvoir téléporter un joueur à une position x, y, z défini
Merci d’avance
A mon avis, tu n’as pas besoin de paquet pour téléporter un joueur.
Je pense que le système de paquet est déjà implémenté de base pour cette action.
Tu dois avoir une méthode coté serveur pour le faire. Cette méthode envoie d’elle même les paquets qui vont bien pour que le client reste synchronisé.Si tu veux TP un joueur a partir d’un client, dans ce cas, il faut envoyer un paquet qui contient toutes les informations nécessaire pour que le serveur TP le joueur avec cette même méthode.
-
Sinon tu peux faire un player.setPositionAndUpdate