Introduction
Dans cette deuxième partie, nous allons voir ensemble comment crée une base de communication pour notre logiciel de clavardage: les packets.
Nous allons voir quelque notions intéressante dans cette partie:
-La dérivation de classes (abstract, sealed).
-La sérialisation manuelle de données a l'aide de la classe MemoryWriter et MemoryReader
-Le cryptage simple de mot de passe a l'aide de l'algorithme MD5 (très facile en C#).
La hiérarchie des classes
Voila, avant de ce lancer en code, je vais faire un petit survol de ce que nous allons accomplir.
Nous désirons d'abord avoir une classe de base pour tout nos packets. Dans cette classe nous retrouverons seulement un code (identifiant) numérique et un array de bytes qui contient les données du packet ansi que une fonction publique GetPacket, qui englobe les données, l'identifiant et un unsigned integer définissant la longueur du packet (Données + Identifiant).
Ensuite nous séparons en deux partie: Une classe pour les packets en provenance du client et une pour ceux en provenance du serveur. Pour facilité les choses, nous allons également avoir besoin d'une énumération pour chaque partie qui définie les identifiants propre au serveur et au client.
Finalement, chaque packet spécifique proposera sa propre classe, dans laquelle nous serons en mesure de sérialiser et désérialiser les données. Chaque packet va comprendre deux constructeurs pour chacune de ses taches.
La classe de base: Packet
J'ai choisi de déclarer cette classe comme abstract car nous ne voulons absolument pas crée une instance directement a partir de cette classe.
public abstract class Packet
Ensuite les constructeur seront protected: ils pourront etre visible seulement a partir des classes dérivés. Nous avons ici deux constructeurs:
protected Packet(uint id, byte[] data)
{
this.Data = data;
this.Id = id;
}
protected Packet(uint id)
{
this.Id = id;
}
Le premier servira pour les packets a désérialiser tandis que le deuxieme pour les packets a sérialiser. Pour ce dernier, les classes dérivés seront en charge de définir la variable Data.
Finalement, nous avons besoin sur ce packet d'une fonction générique pour nous fournir un packet qui contient tout ce qui est dans l'array Data mais également l'identifiant et la longueur. Pour la longueur nous comptons seulement Identifiant + Data (nous ne comptons pas la longueur elle meme).
public byte[] GetPacket()
{
if (this.Data == null)
throw new NullReferenceException("PacketBase.Data is null");
byte[] packet = new byte[this.Data.Length + 8];
using (BinaryWriter bw = new BinaryWriter(new MemoryStream(packet)))
{
bw.Write((uint)(this.Data.Length + 4));
bw.Write((uint)this.Id);
bw.Write((byte[])this.Data);
}
return packet;
}
Nous devons déclarer la variable de retour a la longueur Data + 8, car nous avons besoin de 4 bytes pour écrire la longueur et 4 autre pour l'identifiant.
Les classes de base pour client et serveur
Avant de commencer a écrire nos packets pour des actions spécifiques, nous devons faire une classe pour le serveur et une pour le client qui dérivera de la classe de base Packet. Ensuite, tout les autre packets devront dériver soit de la base ClientPacket ou ServerPacket.
Avant de continuer, voyons nos énumérations pour Les type de packets. J'ai inclus seulement 3 type de packets pour chaque et nous travaillerons uniquement a partir de ces 6 packets pour notre logiciel de clavardage. Il est important de ne pas avoir les meme nombres qui se retrouvent dans nos deux enums, c'est pour cela que j'ai ajouter 0xF0000000 aux packets du serveur.
ClientPacketType enum
LoginRequest = 0x00000000,
BroadcastMessageRequest = 0x00000001,
PrivateMessageRequest = 0x00000002,
ServerPacketType enum
LoginResponse = 0xF0000000,
BroadcastMessage = 0xF0000001,
PrivateMessage = 0xF0000002,
Le role des classes dérivés pour le client et le serveur sera seulement d'accepter une de ses deux énumérations a la place d'un unsigned integer, et de faire la conversion pour la classe Packet. Voici donc pour la classe ClientPacket, je ne montrerai pas celle ServerPacket ici car c'est exactement la meme chose (nous changeons ClientPacketType pour ServerPacketType tout simplement).
public abstract class ClientPacket : Packet
{
protected ClientPacket(ClientPacketType packetType)
: base((uint)packetType)
{
}
protected ClientPacket(ClientPacketType packetType, byte[] data)
: base((uint)packetType, data)
{
}
}
Un exemple concret de dérivation pratique: la classe LoginRequest
Alors nous avons tout en place, maintenant il faut faire en sorte que cela serve a quel que chose! Je vous invite a découvrir la classe LoginRequest qui dérive de la classe ClientPacket. Ce packet servira a envoyer au serveur les informations relatives au nom d'utilisateur et au mot de passe.
Nous avons donc deux propriétés dans cette classe:
public string UserName { get; set; }
public byte[] PasswordHash { get; set; }
Nous avons ensuite besoin de deux constructeur différents: un pour le client qui utilisera un nom de compte et un mot de passe ainsi qu'un deuxieme pour le serveur qui sera responsable de lire les données recues et de populer les variables UserName et PasswordHash.
Pour le constructeur du client:
public LoginRequest(string userName, string password)
: base(ClientPacketType.LoginRequest)
Vous voyez ici que nous instancions notre classe de base avec notre enumeration. Pratique non? Une fois le hash du mot de passe généré a l'aide de la fonction HashPassword (voir plus loin), nous devons écrire sur le array Data de notre classe de base le nom d'usage et le hash du mot de passe. Pour commencer, nous devons trouver la bonne longueur pour notre variable Data. voici comment faire:
base.Data = new byte[userName.Length + 1 + this.PasswordHash.Length];
Je vous explique. La fonction écriture de string que nous allons voir plus bas ajoute un byte qui définit la longueuil du string. Donc si notre nom contient 10 lettres, il ecrire 11 bytes, si il en contient 20, il écrira 21 bytes, aisi de suite. Nous devons a la suite additioner la longueur de la variable PasswordHash qui est deja un array de bytes. Pour arriver a notre fin, nous allons utiliser la classe BinaryWriter disponible dans le namespace System.IO.
using (BinaryWriter br = new BinaryWriter(new MemoryStream(base.Data)))
{
br.Write(userName);
br.Write(PasswordHash);
}
Pour le constructeur du serveur c'est relativement semblable, nous devons seulement utiliser la classe BinaryReader:
using (BinaryReader reader = new BinaryReader(new MemoryStream(data)))
{
this.UserName = reader.ReadString();
this.PasswordHash = reader.ReadBytes(data.Length - (UserName.Length + 1));
}
A noter ici qu'il sera important d'enlever l'identifiant et la longueur avant de faire une nouvel instance de notre classe. Cela sera par contre entièrement gérer par notre system de buffer sur notre socket.
Voici donc un petit exemple d'utilisation de notre classe LoginRequest, nous pouvons crée une classe, et ensuite la recrée a partir des données générés pas la fonction GetPacket:
var loginRequest = new MikeDotNet.Examples.Chat.SharedLib.Packets.Client.LoginRequest("user", "pass");
byte[] loginRequestBytes = loginRequest.GetPacket();
byte[] onlyData = new byte[loginRequestBytes.Length - 8];
Array.Copy(loginRequestBytes, 8, onlyData, 0, onlyData.Length);
var loginRequest2 = new MikeDotNet.Examples.Chat.SharedLib.Packets.Client.LoginRequest(onlyData);
Alors voici, je vous laisse sur un petit bonus: une facon très simple de faire un hash md5 d'un string:
private static byte[] HashPassword(string password)
{
byte[] originalBytes, encodedBytes;
MD5 md5;
md5 = new MD5CryptoServiceProvider();
originalBytes = ASCIIEncoding.Default.GetBytes(password);
encodedBytes = md5.ComputeHash(originalBytes);
return encodedBytes;
}