Débuter dans la développement socket D2
Table des matières
1) Introduction
2) Pré-requis
3) Interface du programme
4) Se connecter au serveur d'authentification (Socket.Connect())
5) Écouter la socket (Boucle d'écoute dans un thread))
6) Décoder les paquets (Fonctionnement du protocle D2)
7) Lire les paquets (Classe de lecture de paquets DataReader)
8) Envoyer un paquet (Classe d'écriture de paquets DataWriter)
9) Identification au serveur
10) Conclusion
1) Introduction
Vous débarquez dans l'univers de programmation de bot par socket ? Et en plus vous avez choisi le C#, je tiens à vous féliciter pour votre choix qui s'avère très judicieux !
Je vais vous apprendre la base pour le développement des bots par socket, vous allez ainsi savoir comment communiquer avec les serveurs du jeu afin de pouvoir recevoir et envoyer les données avec votre programme.
2) Pré-requis
La communication informatique se fait grâce aux petits chiffres 0 et 1. Vous devez avoir un minimum de connaissances en binaire pour savoir que 1 byte = 8 bits, 1 short = 2 bytes = 16 bits, 1 int = 4 bytes = 32 bits, 1 long = 2 int. Le hexadécimal est tout aussi important : 0x00 = 1 byte.
Je vous invite à lire ces explications pour en apprendre d'avantage : Le langage binaire et hexadécimal
Sachez toutefois que si vous êtes un amateur en C#, ce tutoriel ne vous sera guère utile... Je ne vais pas vous apprendre à programmer en C#. C'est pour cela que pour pouvoir comprendre ce tutoriel vous devez avoir les bases du langage, c'est à dire :
- Le C# en général
- Les threads
- Les sockets
- Les delegates
Par ailleurs, je ne vais détailler le fonctionnement du protocole de communication du jeu, il existe un tutoriel très bien expliqué écrit par bouh2 : Comprendre le protocole de D2.0.
Et enfin, vous allez avoir besoin des sources du jeu. Il va falloir les chercher dans le forum :)
Si vous vous sentez d'appoint, Let's Go !
3) Interface du programme
Nous allons commencer par dessiner notre interface pour bien comprendre le fonctionnement de la communication avec le serveur.
Pour ce tutoriel nous allons avoir besoin d'une zone de texte qui servira à faire un journal des paquets reçus/envoyés. Cela nous permettra de savoir quand on reçoit un paquet, l'identifier et éventuellement déboguer son contenu.
Nous allons aussi avoir besoin de deux autres zones de textes qui contiendront le nom du compte de jeu avec son mot de passe associé.
Et enfin, un simple bouton qui permettra de lancer la connexion.
Je vous propose ma solution, libre à vous de faire comme bon vous semble.
Cliquez pour révéler
Cliquez pour masquer
Loading Image
- accountNameLabel
- accountPasswdLabel
- accountNameTextBox
- accountPasswdTextBox
- PasswordChar : X
- connectionButton
- logTextBox
- Multiline : True
- ReadOnly : True
- ScrollBars : Vertical
4) Se connecter au serveur d'authentification
Avant toute chose, nous allons commencer par créer une méthode Log() qui permettra d'ajouter des lignes à notre logTextBox. Je la détaille pas, le code est assez commenté pour ça :
private void Log(string Text)
{
Action log_callback = (Action)delegate
{
logTextBox.Text += Text + "\r\n"; // Ajout du texte avec un saut à la ligne en fin.
logTextBox.Select(logTextBox.Text.Length, 0); // On place le curseur à la fin de la zone de texte.
logTextBox.ScrollToCaret(); // On descend la barre de défilement jusqu'au curseur.
};
this.Invoke(log_callback);
}
Lorsqu'on sniffe les paquets envoyés au client, on remarque que l'adresse IP du serveur d'identification est 213.248.126.180 et le port 5555.
Donc, dans notre évènement Click de notre bouton on va se connecter en Socket à cette adresse.
Mais avant, sachant qu'on va travailler avec les sockets, on va utiliser son espace de nom par défaut :
using System.Net.Sockets;
Nous allons aussi déclarer un attribut de notre classe formulaire de type Socket afin de pouvoir y accéder partout. Le protocole utilisé pour la communication avec le serveur d'authentification du jeu est le TCP et non en UDP comme beaucoup de jeux l'utilise. On va avoir une socket en type Stream et évidemment la famille de l'adresse est InterNetwork.
Pour déclarer notre socket avec toutes ces données c'est simple :
private Socket _Socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
On respecte aussi les normes de programmation indiquant que les attributs doivent être en privés et qu'ils doivent commencer par un underscore ( "_" ). On devrait mettre aussi ses accesseurs mais on ne aura pas besoin dans notre tutoriel.
Maintenant au clic du bouton on se connecte à l'adresse du serveur d'authentification du jeu grâce à la méthode Connect() de la socket.
La classe Socket génère une erreur de type "SocketException" en cas d'exception, pour cela nous allons utiliser le try{} catch{} pour attraper une éventuelle exception.
Une fois la méthode Connect() appelée, il faut qu'on sache si la socket est bien connectée ou non au serveur d'authentification du jeu afin de savoir si on continue notre programme ou pas. Une simple condition sur l'attribut booléen Connected de la socket pour le savoir ;)
On obtient le code suivant pour la connexion de la socket :
private void connectionButton_Click(object sender, EventArgs e)
{
try
{
_Socket.Connect("213.248.126.180", 5555); // Connexion au serveur d'authentification.
// On test l'état de la connexion.
if (_Socket.Connected)
{
Log("La connexion au serveur d'authentification est réussie.");
_ReceptionThread = new Thread(new ThreadStart(Reception));
_ReceptionThread.Start();
}
else
{
Log("La connexion au serveur d'authentification a échouée.");
}
}
catch (SocketException sock_ex)
{
Log("[SocketException] " + sock_ex.Message);
}
}
A ce stade nous avons une simple socket qui se connecte au serveur d'authentification du jeu et on affiche un message en fonction de l'état de connexion de la socket. Voici le code final si vous vous êtes perdus en route.
Cliquez pour révéler
Cliquez pour masquer
using System;
using System.Net.Sockets;
using System.Windows.Forms;
namespace TutoD2Socket
{
public partial class connectionForm : Form
{
#region Attributs
private Socket _Socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
#endregion
#region Constructeurs
public connectionForm()
{
InitializeComponent();
}
#endregion
#region Méthodes privées
private void Log(string Text)
{
Action log_callback = (Action)delegate
{
logTextBox.Text += Text + "\r\n"; // Ajout du texte avec un saut à la ligne en fin.
logTextBox.Select(logTextBox.Text.Length, 0); // On place le curseur à la fin de la zone de texte.
logTextBox.ScrollToCaret(); // On descend la barre de défilement jusqu'au curseur.
};
this.Invoke(log_callback);
}
private void connectionButton_Click(object sender, EventArgs e)
{
try
{
_Socket.Connect("213.248.126.180", 5555); // Connexion au serveur d'authentification.
// On test l'état de la connexion.
if (_Socket.Connected)
{
Log("La connexion au serveur d'authentification est réussie.");
_ReceptionThread = new Thread(new ThreadStart(Reception));
_ReceptionThread.Start();
}
else
{
Log("La connexion au serveur d'authentification a échouée.");
}
}
catch (SocketException sock_ex)
{
Log("[SocketException] " + sock_ex.Message);
}
}
#endregion
}
}
Notez que par mesure de lisibilité du code, je l'ai séparé les grandes parties du code par des régions. Je vous conseille d'en faire autant pour vous retrouver plus facilement entres vos lignes de code ;)
5) Écouter la socket
C'est cool, on est connecté au serveur d'authentification mais on lit comment les données ? C'est ce que je vais vous expliquer.
Pour écouter la socket nous allons avoir besoin d'un thread pour ne pas estomper l'affichage et bloquer l'utilisation de la fenêtre de notre programme puisque nous allons l'écouter en boucle (while)
Comme tout bon programmeur on court utiliser l'espace de nom par défaut :
using System.Threading;
Puis on crée un nouvel attribut de type Thread
private Thread _ReceptionThread;
On s'en occupera du thread un peu plus tard ;)
Maintenant on s'attaque à notre méthode d'écoute. Pour pouvoir écouter on devoir utiliser la méthode Receive() de la socket qui prend en paramètre une variable buffer de type byte[] et comme par magie la socket possède un attribut Available qui contient le nombre de bytes à recevoir.
Notre méthode de réception ressemblera donc à ceci :
private void Reception()
{
// On reçoit les données tant qu'on est connecté.
while (_Socket.Connected)
{
// On crée notre buffer dynamique.
byte[] _buffer = new byte[_Socket.Available];
// Si le buffer n'est pas vide, on le parse.
if (_buffer.Length != 0)
{
_Socket.Receive(_buffer); // Récéption des données.
ParseData(_buffer); // Parsing des données.
}
}
}
Le code est explicitement commenté pour que je vous l'explique en détail.
Vous avez remarqué qu'on appelle la méthode ParseData! Je vais vous expliquer ce que fait cette méthode.
6) Décoder les paquets
ParseData va tout simplement décoder les données reçus afin de connaître le paquet, d'où la connaissance du protocole de communication D2.
Vous êtes censés le connaître, donc je ne vais pas vous détailler son fonctionnement. Le but sera donc de sortir le (ou les) paquet des données comme ceci :
private void ParseData(byte[] DataToParse)
{
// Déclaration des variables qui seront utilisées
int index = 0;
short id_and_packet_lenght_type, packet_id, packet_lenght_type;
Int32 packet_lenght = 0;
byte[] packet_content;
// Lecture jusqu'à la fin de byte[] data
while (index != DataToParse.Length)
{
// Décodage du header
id_and_packet_lenght_type = (short)(DataToParse[index] * 256 + DataToParse[index + 1]); // Selection des 2 premiers octets du paquet
packet_id = (short)(id_and_packet_lenght_type >> 2); // Récupérer l'ID du paquet
packet_lenght_type = (short)(id_and_packet_lenght_type & 3); // Récupérer la taille de la taille de la longueur du paquet
index += 2; // On se déplace 2 octets plus loin
// Récupération de la taille du paquet
switch (packet_lenght_type)
{
case 0:
packet_lenght = 0;
break;
case 1:
packet_lenght = DataToParse[index];
break;
case 2:
packet_lenght = DataToParse[index] * 256 + DataToParse[index + 1];
break;
case 3:
packet_lenght = DataToParse[index] * 65536 + DataToParse[index + 1] * 256 + DataToParse[index + 2];
break;
}
// Récupération du contenu du paquet
packet_content = new byte[(int)packet_lenght];
Array.Copy(DataToParse, index + packet_lenght_type, packet_content, 0, packet_lenght);
// Création de la variable contenant le contenu du paquet en héxadécimal
string content_hex = string.Empty;
int huit_bytes = 0;
foreach (byte b in packet_content)
{
if (huit_bytes == 8)
{
content_hex += "\r\n";
huit_bytes = 0;
}
content_hex += b.ToString("X2") + " ";
huit_bytes++;
}
// Jounalisation
Log("[Reçu] ID = " + packet_id + " | Taille du contenu = " + packet_lenght + "\r\n" + content_hex);
// Traitement du paquet
TreatPacket(packet_id, packet_content);
// Définition de l'index qui démarre le paquet suivant
index += packet_lenght + packet_lenght_type;
}
}
En attendant vous allez créer la méthode TreatPacket vide. Car le traitement des paquets se fait à l'étape suivante ;)
private void TreatPacket(int PacketID, byte[] PacketContent)
{
}
Maintenant pour pouvoir enchaîner et avoir un programme qui tourne, on va devoir lancer le thread juste après que la connexion soit réussie :
private void connectionButton_Click(object sender, EventArgs e)
{
try
{
_Socket.Connect("213.248.126.180", 5555); // Connexion au serveur d'authentification.
// On test l'état de la connexion.
if (_Socket.Connected)
{
Log("La connexion au serveur d'authentification est réussie.");
_ReceptionThread = new Thread(new ThreadStart(Reception));
_ReceptionThread.Start();
}
else
{
Log("La connexion au serveur d'authentification a échouée.");
}
}
catch (SocketException sock_ex)
{
Log("[SocketException] " + sock_ex.Message);
}
}
Vous devez obtenir quelque chose de ce genre :
Cliquez pour révéler
Cliquez pour masquer
using System;
using System.Net.Sockets;
using System.Threading;
using System.Windows.Forms;
namespace TutoD2Socket
{
public partial class connectionForm : Form
{
#region Attributs
private Socket _Socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
private Thread _ReceptionThread;
#endregion
#region Constructeurs
public connectionForm()
{
InitializeComponent();
}
#endregion
#region Méthodes privées
private void Log(string Text)
{
Action log_callback = (Action)delegate
{
logTextBox.Text += Text + "\r\n"; // Ajout du texte avec un saut à la ligne en fin.
logTextBox.Select(logTextBox.Text.Length, 0); // On place le curseur à la fin de la zone de texte.
logTextBox.ScrollToCaret(); // On descend la barre de défilement jusqu'au curseur.
};
this.Invoke(log_callback);
}
private void Reception()
{
// On reçoit les données tant qu'on est connecté.
while (_Socket.Connected)
{
// On crée notre buffer dynamique.
byte[] _buffer = new byte[_Socket.Available];
// Si le buffer n'est pas vide, on le traite.
if (_buffer.Length != 0)
{
_Socket.Receive(_buffer); // Récéption des données.
ParseData(_buffer); // Traitement des données.
}
}
}
private void ParseData(byte[] DataToParse)
{
// Déclaration des variables qui seront utilisées
int index = 0;
short id_and_packet_lenght_type, packet_id, packet_lenght_type;
Int32 packet_lenght = 0;
byte[] packet_content;
// Lecture jusqu'à la fin de byte[] data
while (index != DataToParse.Length)
{
// Décodage du header
id_and_packet_lenght_type = (short)(DataToParse[index] * 256 + DataToParse[index + 1]); // Selection des 2 premiers octets du paquet
packet_id = (short)(id_and_packet_lenght_type >> 2); // Récupérer l'ID du paquet
packet_lenght_type = (short)(id_and_packet_lenght_type & 3); // Récupérer la taille de la taille de la longueur du paquet
index += 2; // On se déplace 2 octets plus loin
// Récupération de la taille du paquet
switch (packet_lenght_type)
{
case 0:
packet_lenght = 0;
break;
case 1:
packet_lenght = DataToParse[index];
break;
case 2:
packet_lenght = DataToParse[index] * 256 + DataToParse[index + 1];
break;
case 3:
packet_lenght = DataToParse[index] * 65536 + DataToParse[index + 1] * 256 + DataToParse[index + 2];
break;
}
// Récupération du contenu du paquet
packet_content = new byte[(int)packet_lenght];
Array.Copy(DataToParse, index + packet_lenght_type, packet_content, 0, packet_lenght);
// Création de la variable contenant le contenu du paquet en héxadécimal
string content_hex = string.Empty;
int huit_bytes = 0;
foreach (byte b in packet_content)
{
if (huit_bytes == 8)
{
content_hex += "\r\n";
huit_bytes = 0;
}
content_hex += b.ToString("X2") + " ";
huit_bytes++;
}
// Jounalisation
Log("[Reçu] ID = " + packet_id + " | Taille du contenu = " + packet_lenght + "\r\n" + content_hex);
// Traitement du paquet
TreatPacket(packet_id, packet_content);
// Définition de l'index qui démarre le paquet suivant
index += packet_lenght + packet_lenght_type;
}
}
private void TreatPacket(int PacketID, byte[] PacketContent)
{
}
private void connectionButton_Click(object sender, EventArgs e)
{
try
{
_Socket.Connect("213.248.126.180", 5555); // Connexion au serveur d'authentification.
// On test l'état de la connexion.
if (_Socket.Connected)
{
Log("La connexion au serveur d'authentification est réussie.");
_ReceptionThread = new Thread(new ThreadStart(Reception));
_ReceptionThread.Start();
}
else
{
Log("La connexion au serveur d'authentification a échouée.");
}
}
catch (SocketException sock_ex)
{
Log("[SocketException] " + sock_ex.Message);
}
}
#endregion
}
}
On lance le programme pour pouvoir le tester, et voilà ce que vous devez similairement obtenir :
Cliquez pour révéler
Cliquez pour masquer
Loading Image
7) Lire les paquets
Maintenant nous pouvons recevoir les paquets. Mais nous ne savons pas encore les lire...
Il faut que nous créons un reader capable de lire en BigEndian. Je vous propose un reader codé pour l'occasion, vraiment simple et basique, à vous de rajouter des fonctionnalités.
Cliquez pour révéler
Cliquez pour masquer
using System;
using System.IO;
using System.Text;
namespace TutoD2Socket
{
/// <summary>
/// Représente un lecteur capable de lire une série de données binaires en BigEndian.
/// </summary>
public class DataReader
{
#region Attributs
private byte[] _Content;
private int _Position = 0;
#endregion
#region Propriétés
/// <summary>
/// Obtient ou défini le tableau d'octets en lecture.
/// </summary>
public byte[] Content
{
set { this._Content = value; }
get { return this._Content; }
}
/// <summary>
/// Obtient ou défini la position du curseur de lecture.
/// </summary>
public int Position
{
set { this._Position = value; }
get { return this._Position; }
}
#endregion
#region Constructeurs
/// <summary>
/// Initialise une nouvelle instance de la classe D2Com.IO.DataReader.
/// </summary>
public DataReader()
{
}
/// <summary>
/// Initialise une nouvelle instance de la classe D2Com.IO.DataReader.
/// </summary>
/// <param name="content">Tableau d'octets à lire.</param>
public DataReader(byte[] content)
{
this._Content = content;
}
#endregion
#region Méthodes publiques
/// <summary>
/// Lit le prochain octet.
/// </summary>
/// <returns>L'octet.</returns>
public byte ReadByte()
{
if (this._Content.Length >= 1)
{
byte result;
result = this._Content[_Position];
this._Position++;
return result;
}
else
{
throw new Exception("Il n'y a plus d'octet à lire.");
}
}
/// <summary>
/// Lit le nombre spécifié des prochains octets.
/// </summary>
/// <param name="number">Nombre d'octets à lire.</param>
/// <returns>Le tableau d'octets lus.</returns>
public byte[] ReadBytes(int number)
{
if (this._Content.Length >= number)
{
byte[] result = new byte[number];
for (int i = 0; i != number; i++)
{
result = this.ReadByte();
}
return result;
}
else
{
throw new Exception("Il n'y a plus d'octets à lire.");
}
}
/// <summary>
/// Lit les prochains octets de la longueur d'un short.
/// </summary>
/// <returns></returns>
public short ReadShort()
{
if (this._Content.Length >= 2)
{
return (short)((this.ReadByte() << 8) + this.ReadByte());
}
else
{
throw new Exception("Il n'y a plus d'octets à lire.");
}
}
/// <summary>
/// Lit les prochains octets de la longueur d'un int codé en 16 bits.
/// </summary>
/// <returns>Contenu de l'int.</returns>
public int ReadIntOn16Bits()
{
if (_Content.Length >= 2)
{
int i = (int)((Convert.ToUInt16(ReadByte()) << 8) + Convert.ToUInt16(ReadByte()));
return i;
}
else
{
throw new Exception("Il n'y a plus d'octets à lire.");
}
}
/// <summary>
/// Lit les prochains octets de la longueur d'un int.
/// </summary>
/// <returns>Contenu de l'int.</returns>
public int ReadInt()
{
if (_Content.Length >= 4)
{
return (int)((ReadByte() << 24) + (ReadByte() << 16) + (ReadByte() << 8) + ReadByte());
}
else
{
throw new Exception("Il n'y a plus d'octets à lire.");
}
}
/// <summary>
/// Lit les prochains octets codés en UTF8.
/// </summary>
/// <returns>Contenu de la châine.</returns>
public string ReadString()
{
byte[] string_bytes = ReadBytes(ReadIntOn16Bits());
return Encoding.UTF8.GetString(string_bytes);
}
/// <summary>
/// Lit le prochain octet de la valeur d'un byte en le convertissant en bool
/// </summary>
/// <returns>Contenu du bool.</returns>
public bool ReadBool()
{
if (ReadByte() == 01)
return true;
else
return false;
}
#endregion
}
}
On va pas chercher à comprendre car cela faisait parti des pré-requis.
Maintenant nous allons ouvrir les fichiers dans les sources du jeu :
com.ankmgms.d0fus.network.messages.handshake RequiredProtocol.as
com.ankamagames.d0fus.network.messages.connection HelloConnectMessage
Ces fichiers permettent de savoir comment sont construits les paquets.
Ne tardons pas à commencer notre fonction TreatPacket()
private void TreatPacket(int PacketID, byte[] PacketContent)
{
DataReader reader = new DataReader(PacketContent);
switch (PacketID)
{
case 1:
// Ici nous savons que nous sommes bien connecté au serveur.
break;
}
}
Cette fonction va permettre de faire réagir votre programme en fonction des paquets reçus.
Nous allons juste nous contenter de lire les données du premier paquet reçu.
Si on regarde les sources du paquet RequiredProtocol, on remarque qu'il comporte deux variables avec le type int. Plus loin dans le fichier, à la fonction Deserialize_as... on voit qu'il faut faire un "ReadInt" pour obtenir les informations. C'est ce que nous allons faire aussi.
case 1:
int required = reader.ReadInt();
int currrent = reader.ReadInt();
Log("Required = " + required + " - Current = " + currrent + "\r\n");
break;
Vous venez de décoder votre premier paquet !
Mainteant nous allons décoder le deuxième paquet, HelloConnectMessage.
On regarde les variables : connectionTypede type int et key de type string.
Plus loin dans le code, à la fonction Deserialize_as... on voit qu'il faut faire un ReadByte pour connectionType et un ReadString pour key. Ne tardons pas à le faire !
case 3:
int connectionType = reader.ReadByte();
string key = reader.ReadString();
Log("ConnectionType = " + connectionType + " - Key = " + key + "\r\n");
break;
A partir de là, le serveur attends qu'on se connecte. Je ne détaille pas le décodage du paquet IdentificationMessage mais c'est lui qu'il faut envoyer, et il dépend de quelques fichiers Type.
L'écriture des paquets se fait à l'étape suivante :)
8) Envoyer un paquet
Le serveur attend fraîchement qu'on lui envoi notre paquet d'identification. Il va falloir donc pouvoir écrire en BigEndian. Pour celà je vous propose une autre classe qui va permettre d'écrire des paquets.
Cliquez pour révéler
Cliquez pour masquer
using System;
using System.Collections.Generic;
using System.Text;
namespace TutoD2Socket
{
/// <summary>
/// Représente un writer capable d'écrire une série de données binaires en BigEndian.
/// </summary>
public class DataWriter
{
#region Attributs
private List<byte> _Bytes = new List<byte>();
#endregion
#region Constructeurs
/// <summary>
/// Initialise une nouvelle instance de la classe D2Com.IO.DataWritter.
/// </summary>
public DataWriter()
{
}
#endregion
#region Méthodes privées
/// <summary>
/// Calcule la taille de la taille des données écrites.
/// </summary>
/// <returns>La taille de la taille des données écrites.</returns>
private int _GetLenghtType()
{
int packet_lenght_type = 0;
if (this._Bytes.Count > short.MaxValue)
packet_lenght_type = 3;
else if (this._Bytes.Count > byte.MaxValue)
packet_lenght_type = 2;
else if (this._Bytes.Count > 0)
packet_lenght_type = 1;
return packet_lenght_type;
}
/// <summary>
/// Crée l'entête des données écrites.
/// </summary>
/// <param name="protocolID"></param>
/// <returns></returns>
private short _GetHeader(int protocolID)
{
return (short)((protocolID << 2) | this._GetLenghtType());
}
#endregion
#region Méthodes publiques
/// <summary>
/// Crée le paquet avec les données écrites.
/// </summary>
/// <param name="protocolID">ProtocolID du paquet à créer.</param>
/// <returns>Tableau d'octets contenant le paquet crée.</returns>
public byte[] Pack(int protocolID)
{
// Définition des variables à utiliser
int index = 0;
int packet_lenght = this._Bytes.Count;
int pack_lenght_type = this._GetLenghtType();
short packet_header = this._GetHeader(protocolID);
// Création du Header
byte[] packet = new byte[2 + pack_lenght_type + packet_lenght];
packet[0] = (byte)(packet_header >> 8);
packet[1] = (byte)(packet_header - 256 * packet[0]);
switch (pack_lenght_type)
{
case 1:
packet[2] = (byte)(packet_lenght);
index = 3;
break;
case 2:
packet[2] = (byte)(packet_lenght >> 8);
packet[3] = (byte)(packet_lenght - 256 * packet[2]);
index = 4;
break;
case 3:
packet[2] = (byte)(packet_lenght >> 16);
packet[3] = (byte)(packet_lenght >> 8);
packet[4] = (byte)(packet_lenght - 256 * packet[3] - 256 * 256 * packet[2]);
index = 5;
break;
}
// Remplissage du paquet
for (int i = index; i < index + packet_lenght; i++)
{
packet = this._Bytes[i - index];
}
return packet; // Renvoi du contenu intégral du paquet
}
/// <summary>
/// Ecrit une donnée de la valeur d'un octet à la suite du paquet.
/// </summary>
/// <param name="Value">Valeur à écrire.</param>
public void WriteByte(int value)
{
this._Bytes.Add((byte)value);
}
/// <summary>
/// Ecrit une donnée de la valeur d'un short à la suite du paquet.
/// </summary>
/// <param name="Value">Valeur à écrire</param>
public void WriteShort(int value)
{
this.WriteIntOn16Bits(value);
}
/// <summary>
/// Ecrit une donnée de la valeur d'un int codé en 16 bits à la suite du paquet.
/// </summary>
/// <param name="Value">Valeur à écrire</param>
public void WriteIntOn16Bits(int value)
{
byte[] int_bytes = BitConverter.GetBytes(value);
this.WriteByte(int_bytes[1]);
this.WriteByte(int_bytes[0]);
}
/// <summary>
/// Ecrit une donnée de la valeur d'un int à la suite du paquet.
/// </summary>
/// <param name="Value"></param>
public void WriteInt(int value)
{
byte[] int_bytes = BitConverter.GetBytes(value);
this.WriteByte(int_bytes[3]);
this.WriteByte(int_bytes[2]);
this.WriteByte(int_bytes[1]);
this.WriteByte(int_bytes[0]);
}
/// <summary>
/// Ecrit une chaîne de caractères codé en UTF8 à la suite du paquet.
/// </summary>
/// <param name="Value">Chaîne à écrire.</param>
public void WriteString(string text)
{
byte[] text_bytes = Encoding.UTF8.GetBytes(text);
WriteShort(text_bytes.Length);
foreach (byte b in text_bytes)
{
this.WriteByte(b);
}
}
/// <summary>
/// Ecrit une donnée de la valeur d'un bool à la suite du paquet.
/// </summary>
/// <param name="Value">Valeur à écrire.</param>
public void WriteBool(bool value)
{
if (value)
this.WriteByte(1);
else
this.WriteByte(0);
}
#endregion
}
}
Nous allons utiliser immédiatement notre nouvelle classe pour écrire un paquet.
Comme je vous l'ai dit, le serveur est en attente de notre paquet d'identification qui se situe com.ankamagames.d0fus.network.messages.connection IdentificationMessage.
Ce paquet deviens un tout petit peu plus complexe, ce pourquoi je ne vais pas vous détailler son explication.
Juste avant de continuer, veuilez créer une nouvelle fonction qui va permettre de hacher les chaînes en MD5. On va en avoir besoin pour le mot de passe.
Cliquez pour révéler
Cliquez pour masquer
public string MD5(string String)
{
System.Security.Cryptography.MD5CryptoServiceProvider cryptor = new System.Security.Cryptography.MD5CryptoServiceProvider();
byte[] hash_bytes = System.Text.Encoding.UTF8.GetBytes(String);
hash_bytes = cryptor.ComputeHash(hash_bytes);
string buffer = "";
foreach (byte by in hash_bytes)
{
buffer += by.ToString("x2");
}
return buffer;
}
9) Authentification au serveur
Retournons à notre super switch. Nous allons désormais envoyer le paquet 4, celui qui va permettre l'authentification au serveur.
Le mot de passe à envoyer au serveur doit être haché en MD5 avec la clé. Je m’explique :
D'abord on hache le mot de passe de l'utilisateur. Ensuite avec ce hash nous allons le hacher avec la valeur de la clé de HelloConnectMessage concaténé.
Nous finissons par obtenir :
string mdp_hash = MD5(MD5(accountPasswdTextBox.Text) + key);
Trêve de bavardages, en place pour le code !
Notre case 3 va être modifié :
case 3:
int connectionType = reader.ReadByte();
string key = reader.ReadString();
Log("ConnectionType = " + connectionType + " - Key = " + key + "\r\n");
string mdp_hash = MD5(MD5(accountPasswdTextBox.Text) + key); // Hachage du mot de passe
DataWriter writer = new DataWriter();
writer.WriteByte(2);
writer.WriteByte(3);
writer.WriteByte(7);
writer.WriteShort(35100);
writer.WriteByte(0);
writer.WriteByte(0);
writer.WriteString(accountNameTextBox.Text);
writer.WriteString(mdp_hash);
writer.WriteShort(0);
writer.WriteBool(true);
_Socket.Send(writer.Pack(4));
break;
Afin de rentre ce tuto un tout petit plus interactif, je vous propose de rajouter deux cases :
case 20:
MessageBox.Show("Authentification ratée !");
break;
case 22:
reader.ReadByte();
MessageBox.Show("Salut " + reader.ReadString());
break;
10) Conclusion
Ce tutoriel est désormais fini. J’espère qu'il vous a permis de mieux comprendre comment établir une communication au serveur de jeu.
Je comprends bien que la partie où vous devez décoder les paquets vous paraisse plutôt abstraite, ceci dit, le plus gros travail c'est de les décoder justement et surtout de les comprendre. Le mieux c'est de transcrire le code orignal en AS en C# pour en créer une classe. Ainsi vous appellerez les méthodes Deserialize pour pouvoir lire le paquet et Serialize pour pouvoir l'écrire. Beaucoup de développeurs de bot D2 conçoivent eux même un programme automatisant la tâche.
Sachez toutefois que le code n'est absolument pas optimisé ! C'est uniquement dans le but de vous servir d'une base afin de commencer votre propre programme.
Il faudrait par exemple créer une classe qui gère la connexion de la socket avec un évènement qui obtient le contenu du paquet reçu pour organiser un peu votre code.
Les classes Reader/Writer ne sont pas optimisées, vous pourrez les optimiser afin de rajouter des fonctionnalités de lecture ou bien en recommençant en s'aidant de la classe Bitconverter qui lit en LittleEndian mais que vous pourrez facilement trouver une solution, comme par exemple inverser le tableau des données...
Puisque c'est un stream, il faudrait implémente l'interface IDisposeable pour entre dans les normes afin que ça soit propre. Surtout si vous utilisez d'autres fonctions des autres classes streams Disposeables.
Consulter le billet Implémenter IDisposable correctement chez MSDN pour en savoir d'avantage.
Pour que vous pussiez mieux comprendre ce tutoriel, je vous ai archivé l'intégralité du code source et son exécutable.
Bon codage à tous :)
J'attends avec impatience vos retours :)