Cours Sockets 2
Cours Sockets 2
Cours Sockets 2
(version complète)
Propriétés Description
Ce document n’est pas une étude exhaustive du domaine. Il présente simplement une manière de
réaliser des applications communicantes en C#.
Les applications fournies ont été réalisées avec Visual Studio 2008 et sont destinées au framework
dotnet 3.5, elles peuvent facilement être recompilées avec un autre outil et/ou cibler une autre version
du framework.
L’exécution d’une application utilisant les sockets à partir d’un disque réseau peut poser des problè-
mes de sécurité. Si vous obtenez une exception concernant la sécurité, configurez les paramètres de
sécurité du framework. Vous pouvez consulter la page http://msdn.microsoft.com/fr-
1. Les sockets
Les sockets fournissent un mécanisme générique de communication entre les processus. Elles sont
apparues pour la première fois en 1986 dans la version UNIX de l’université de Berkeley.
Un processus peut être sommairement défini comme une application (ou un service) en cours
d’exécution.
Un socket permet à un processus (le client) d’envoyer un message à un autre processus (le serveur).
Le serveur qui reçoit ce message peut alors accomplir toutes sortes de tâche et éventuellement re-
tourner un résultat au processus client.
Lors de la création d’un socket, il faut préciser le type d’adressage, le type de message et le protocole
transport utilisés. Nous utiliseront : IPV4, les datagrammes simples et le protocole UDP.
2. Point de terminaison
Un point de terminaison (EndPoint) est défini par une adresse IP et un numéro de port. Une communi-
cation s’établit entre deux points de terminaison.
Un processus serveur reçoit les messages destinés à un numéro de port et à un protocole transport
(UDP, TCP, ...) déterminés. Un client désirant envoyer un message à un serveur doit donc créer un
point de terminaison représentant le récepteur du message en fournissant l’adresse IP du serveur et
le numéro de port.
Pour envoyer un message, un processus doit également créer un point de terminaison représentant
l’émetteur du message en fournissant sa propre adresse IP. Il ne fournit pas de numéro de port, c’est
le système qui attribuera un numéro de port libre pour la communication.
3. Principe de communication
Le client :
public Fm_client()
{
InitializeComponent();
adrIpLocale = getAdrIpLocaleV4();
}
private IPAddress adrIpLocale;
private IPAddress getAdrIpLocaleV4()
{
string hote = Dns.GetHostName();
IPHostEntry ipLocales = Dns.GetHostEntry(hote);
foreach (IPAddress ip in ipLocales.AddressList)
{
if (ip.AddressFamily == AddressFamily.InterNetwork)
{
return ip;
}
}
return null; // aucune adresse IP V4
}
public Fm_serveur()
{
InitializeComponent();
adrIpLocale = getAdrIpLocaleV4();
}
private IPAddress adrIpLocale;
private IPAddress getAdrIpLocaleV4()
{
string hote = Dns.GetHostName();
IPHostEntry ipLocales = Dns.GetHostEntry(hote);
foreach (IPAddress ip in ipLocales.AddressList)
{
if (ip.AddressFamily == AddressFamily.InterNetwork)
{
return ip;
}
}
return null; // aucune adresse IP V4
}
private void bt_recevoir_Click(object sender, EventArgs e)
{
byte[] message = new byte[40];
Socket sock = new Socket( AddressFamily.InterNetwork,
SocketType.Dgram, ProtocolType.Udp);
IPEndPoint epRecepteur = new IPEndPoint(adrIpLocale, 33000);
sock.Bind(epRecepteur);
EndPoint epTemp = (EndPoint)new IPEndPoint(IPAddress.Any, 0);
sock.ReceiveFrom(message, ref epTemp);
IPEndPoint epEmetteur = (IPEndPoint)epTemp;
string strMessage = Encoding.Unicode.GetString(message);
MessageBox.Show( epEmetteur.Address.ToString()
+ " -> " + strMessage);
}
Remarques :
- La fonction getAdrIpLocaleV4 retourne l’adresse IP V4 de l’hôte en utilisant le service DNS.
Le nom de l’hôte local est récupéré en utilisant le DNS.
- Elle est appelée dans le constructeur du formulaire.
- La méthode ReceiveFrom de la classe Socket attend un paramètre de type EndPoint. Il faut
donc opérer une conversion de type pour récupérer le point de terminaison émetteur.
- Le message est un tableau de bytes. Il faut le transformer en chaîne de caractères.
Mise en œuvre :
1. Principe
Lorsque l’on clique sur le bouton recevoir du serveur, celui-ci se trouve bloqué en attente du message
et ne peut accomplir aucune autre tâche. Pour y remédier, il est possible d’utiliser le mécanisme de
réception asynchrone fourni par Dotnet. Ce mécanisme est basé sur les threads. Un thread peut être
vu comme un « mini processus » interne à une application. Une application peut avoir plusieurs
threads et donc exécuter plusieurs tâches simultanément (cette simultanéité n’est que fictive, en tous
cas sur une machine mono-processeur).
- Le socket est mis en état de réception dans un thread de réception différent du thread princi-
pal. Le thread principal poursuit immédiatement son exécution, le programme n’est pas blo-
qué par cet appel.
- L’avant dernier paramètre indique que la méthode recevoir doit être appelée dès la réception
d’un message par le thread de réception.
Quand le thread de réception reçoit le message, il le prend en charge (l’affiche par exemple), et se
termine. L’idéal est de remettre le socket en état de réception immédiatement, de manière à accepter
plusieurs messages les uns après les autres.
public Fm_serveur()
{
InitializeComponent();
init();
}
private int lgMessage = 40;
private IPAddress adrIpLocale;
private Socket sock;
private IPEndPoint epRecepteur;
byte[] message;
private IPAddress getAdrIpLocaleV4()
{
// Idem version pécédente
}
Remarques :
- Le serveur se met en réception dès le lancement de l’application, il n’y a donc plus besoin
d’un bouton recevoir.
- Dès la réception d’un message, le serveur relance un nouveau thread de réception.
- L’attente de réception ne s’arrête qu’à la fermeture du programme.
- Vous pouvez tester l’envoi de messages à partir de deux clients différents.
1. Problème potentiel
La liste a été créée par le thread principal de l’application. C’est le thread de réception qui exécute la
méthode recevoir et tente donc de mettre à jour cette liste. Ceci n’est pas permis car cela pourrait
conduire à une situation de blocage si plusieurs threads tentaient de le faire en même temps (ou plu-
tôt si un thread tentait de commencer à le faire alors qu’un autre thread n’aurait pas terminé de le
faire).
2. Une solution
Il s’agit de créer un troisième thread chargé de traiter le message lors de son arrivée. Cette opération
peut être réalisée à l’aide d’un objet de la classe BackgroundWorker.
- Ajouter une propriété nbMessages à la classe Fm_serveur. Cette propriété sera destinée à
compter le nombre de messages reçus (c’est juste un prétexte permettant d’illustrer le fonc-
tionnement d’un travailleur, on pourrait compter les messages plus simplement).
- Ce nombre de messages sera affiché dans un label lbl_nbMessages.
- Mettre en place le mécanisme du travailleur dans la méthode recevoir. La méthode wor-
ker_DoWork reçoit en paramètre le message reçu, incrémente le nombre de messages et
renseigne son résultat (le message lui-même).
- A la fin de l’exécution du travailleur, la méthode worker_RunWorkerCompleted récupère le ré-
sultat de l’exécution du travailleur (worker_DoWork) et met à jour l’interface utilisateur.
1. Principe
On peut avoir à transmettre des messages plus complexes qu’une simple chaîne. Au moins deux
solutions :
- Créer une classe MessageReseau sérialisable et utiliser la sérialisation d’objets. Cette solu-
tion sera examinée plus loin dans ce document.
- Transmettre les différents éléments du message dans une seule chaîne de caractères en utili-
sant un séparateur. Le mieux est de créer une classe MessageReseau encapsulant les opéra-
tions nécessaires.
2. Classe MessageReseau
class MessageReseau
{
private static char separateur = '#';
private IPAddress ipEmetteur;
public IPAddress IpEmetteur
{
get { return ipEmetteur; }
set { ipEmetteur = value; }
}
private string texte;
public string Texte
{
get { return texte; }
set { texte = value; }
}
public MessageReseau(IPAddress p_ipEmetteur, string p_texte)
{
ipEmetteur = p_ipEmetteur;
texte = p_texte;
}
public byte[] GetInfos()
{
string infos = ipEmetteur.ToString() + separateur.ToString()
+ texte + separateur.ToString();
return Encoding.Unicode.GetBytes(infos);
}
public MessageReseau(byte[] p_infos)
{
string infos = Encoding.Unicode.GetString(p_infos);
string[] tabInfos = infos.Split(new char[] { separateur });
ipEmetteur = IPAddress.Parse(tabInfos[0]);
texte = tabInfos[1];
}
}
- La méthode GetInfos permettra de créer le tableau de bytes lors de l’envoi du message par le
client.
- Le constructeur MessageReseau(byte[] p_infos) permettra de construire l’objet MessageRe-
seau lors de la réception par le serveur.
- Le caractère séparateur en fin de message dans GetInfos est important, il permet de ne récu-
pérer que le texte réellement envoyé dans le constructeur MessageReseau(byte[] p_infos).
C’est cette fois un objet de la classe MessageReseau qui est transmis au travailleur.
Le message sera reçu par tous les hôtes en état de réception sur le port UDP 33000.
2. Définir un protocole
Il s’agit de fixer les règles de la communication entre les différentes applications. Imaginons l’exemple
d’un pauvre chat :
- Connexion (type C)
Emetteur : un client qui se connecte
Récepteur : le serveur
Contenu : L’adresse IP et le pseudo du client
Réaction du serveur : mémorisation du pseudo
- Envoi (type E)
Emetteur : un client qui envoi un texte sur le chat
Récepteur : le serveur
Contenu : l’adresse IP du client et le texte envoyé
Réaction du serveur : réémission du texte à tous les clients
- Déconnexion (type D)
Emetteur : un client qui se déconnecte, c'est-à-dire qui quitte l’application
Récepteur : le serveur
Contenu : l’adresse IP du client et son pseudo
Réaction du serveur : mise à jour de la liste des clients connectés
1. La classe MessageChat
class MessageChat
{
private static char separateur = '#';
private char typeMessage;
public char TypeMessage
{
get { return typeMessage; }
}
private IPAddress ipEmetteur;
public IPAddress IpEmetteur
{
get { return ipEmetteur; }
}
private string texte;
public string Texte
{
get { return texte; }
}
public MessageChat( char p_typeMessage, IPAddress p_ipEmetteur,
string p_texte)
{
typeMessage = p_typeMessage;
ipEmetteur = p_ipEmetteur;
texte = p_texte;
}
public byte[] GetInfos()
{
string infos = typeMessage.ToString()+ separateur
+ ipEmetteur.ToString() + separateur.ToString()
+ texte + separateur.ToString();
return Encoding.Unicode.GetBytes(infos);
}
Cette classe, la fonction GetAdrIpV4 ainsi que les différentes constantes peuvent facilement être re-
groupées dans une DLL utilisée par les deux applications. Le dossier ExempleCommun illustre cette
possibilité en reprenant l’exemple 4.
- Le projet Commun contient la classe MessageReseau et la classe UtilIP. Il s’agit d’un projet
de type « Librairie de classes ». Le résultat de sa génération est donc une DLL.
- Les deux projets Client et Serveur utilisent Commun.dll. Il suffit pour cela d’ajouter la référen-
ce à cette DLL dans ces projets (« Explorateur de solution », clic droit sur le dossier « Réfé-
rences », « Ajouter une référence », onglet « Parcourir », désigner « Commun.dll »).
- Ajouter « using Commun » dans le source de Fm_client et Fm_serveur.
2. Le client
Généralités :
- L’adresse IP du serveur est supposée connue.
- Il faut également déterminer les ports utilisés par le serveur et par les clients.
Quand l’utilisateur clique sur « se connecter », le message de connexion est envoyé au serveur et le
client se met en état de réception. Il devient ensuite possible de poster un texte sur le chat.
A la réception d’un message, le client met à jour son interface à l’aide d’un BackgroundWorker. La
méthode worker_DoWork se contente de transmettre son argument.
Quand l’utilisateur clique sur le bouton « envoyer », le texte saisi est envoyé au serveur.
3. Le serveur
Outre les constantes nécessaires, le serveur déclare un dictionnaire qui lui permettra de gérer la liste
des clients connectés. Dès son lancement, le serveur se met en état de réception sur son port.
Une méthode envoyerBroadcast permet l’envoi d’un message sur l’ensemble du réseau.
Les messages reçus sont traités par un BackgroundWorker en fonction de leur type.
4. Bilan
Il reste à l’améliorer :
- Faire en sorte de mieux gérer : le problème de l’unicité des pseudos, le problème des pseu-
dos ou messages contenant un #, le problème de la fermeture du serveur (peut-être faudrait-il
prévenir les clients), etc…
- Afficher la liste des clients connectés sur l’interface des clients.
- Afficher les messages sur l’interface du serveur pour permettre une modération du chat par un
« superviseur ».
- Permettre aux clients de choisir une couleur d’écriture.
- Imaginer la possibilité de dialogues privés entre deux clients.
- Gérer plusieurs salons.
- Sérialiser les messages (voir plus loin).
- Envoyer les messages en multicast et non en broadcast (voir plus loin).
Un simple conseil : avant de vous lancer, réorganisez votre application pour utiliser une DLL commu-
ne au client et au serveur pour éviter la duplication du code.
Ah oui, encore une chose… Le protocole de communication doit être défini correctement avant toute
étape de codage.
La sérialisation binaire consiste à transformer un objet en une suite de bytes. L’opération inverse, la
désérialisation binaire permet de construire un objet à partir d’une suite de bytes.
La classe MessageReseau
Cette méthode statique produit un tableau de bytes à partir d’un objet MessageReseau.
Cette méthode statique construit un objet MessageReseau à partir d’un tableau de bytes.
Le client
Il s’agit cette fois pour le serveur de n’envoyer les messages qu’aux hôtes connectés au chat et non à
l’ensemble du réseau local.
Pour cela, il faut utiliser une adresse de multicast IP (plage 224.0.0.1 -> 239.255.255.254). Une
adresse multicast identifie un groupe de machines.
Seule la méthode initReception est modifiée, le client s’ajoute au groupe de réception multicast.