I. Introduction▲
Pour les besoins de ce tutoriel, nous avons développé une application de chat se basant sur les sockets. Cette application est composée d'un serveur et d'un client. Nous utiliserons la classe System.Net.Sockets. Il eut été possible d'utiliser aussi les classes TcpClient et TcpServer. Nous travaillerons en mode synchrone multithread et avec des connexions en mode « connecté ». Le mode connecté signifie que lorsque le client a établi une connexion avec le serveur, celle-ci reste ouverte jusqu'à ce que l'un des process décide de la fermer ou qu'un problème réseau survient. Je fais cette précision, car il est possible de travailler en mode « déconnecté », c'est-à-dire qu'un client se connecte sur un serveur, celui-ci traite l'info et renvoie un « ack » (acknowledgement) au client qui dès réception de cet ack ferme la connexion.
Le chat comprend les fonctionnalités suivantes :
- pas de limitation au niveau du nombre de clients pouvant se connecter simultanément ;
- notification des connectés/déconnectés ;
- notification des messages reçus par un clignotement de fenêtre si celle-ci est réduite ;
- formatage des messages en RTF à la volée. Nous aurions pu nous contenter du mode texte qui est beaucoup plus facile à gérer. Car outre sa relative complexité, le mode RTF est moins performant et est sensible à certains caractères spéciaux comme les accolades par exemple.
I-A. Les principales méthodes de la classe Sockets que nous avons utilisées pour réaliser le chat▲
Méthode |
Description |
---|---|
Socket.Bind |
Lie la socket à un point de communication, renvoie une erreur si le port spécifié par le point de communication est déjà utilisé par un autre processus |
Socket.Listen |
Place en file d'attente (queue) toutes les connexions entrantes |
Socket.Accept |
Lit la file d'attente et accepte les connexions entrantes. Accept est bloquant, l'exécution du code est bloquée jusqu'à ce qu'accept détecte une connexion entrante dans la file d'attente |
Socket.Select |
Permet entre autres de vérifier si une socket connectée tente d'écrire quelque chose. En passant un tableau de connexion en paramètre à select, après l'exécution de select, le tableau ne contient plus que les sockets ayant des données prêtes à être lues |
Socket.Poll |
Permet, un peu comme select, de détecter si une socket est prête à être lue ou écrite. À l'inverse de select, elle permet aussi de détecter si une connexion est terminée (voir commentaire dans le code). |
Socket.Receive |
Permet de réceptionner les données qui ont été écrites sur la socket |
Socket.Send |
Permet d'écrire des données sur une socket |
Socket.Connect |
Permet de se connecter à une socket |
I-B. Architecture de notre chat▲
Comme vous pouvez le constater, la communication entre le serveur et le(s) client(s) est basée sur le protocole TCP/IP. Cette communication peut avoir lieu au sein d'un réseau ou d'une même machine. Les connexions établies restent donc en mode « connecté » jusqu'à ce que l'un des processus décide de les fermer.
Le code du serveur est « divisé » en quatre parties.
- Le thread principal : c'est lui qui crée la socket d'écoute sur le serveur, ensuite, dans une boucle infinie, il attend de nouvelles connexions entrantes (socket.accept). Il démarre préalablement le thread d'écoute et le thread vérifiant que les connexions clientes sont toujours actives. Le 2e et 3e thread sont démarrés avant même qu'une connexion cliente ait eu lieu. Étant donné qu'ils ne consomment quasiment aucune ressource, ce n'est pas préjudiciable. Il est en fait plus facile de les démarrer avant, car dans ce cas, on n’a pas à vérifier s'ils sont déjà démarrés ou pas lorsqu'on les démarre. J'aurais pu ne les démarrer que lorsqu'au moins une connexion cliente est effectuée, mais j'ai opté pour la facilité, d'autant que l'impact sur les performances est nul.
- Le 2e thread se charge de lire en permanence les données écrites par les clients. Il démarre le thread d'écriture lorsqu'il reçoit des données.
- Le 3e thread se charge de vérifier que les connexions clientes sont toujours actives.
- Le 4e thread est démarré lorsque le 2e thread a lu des données sur une socket.
Le code du client est lui, un peu plus simple .
- Le thread principal se charge de créer et de connecter la socket cliente au serveur et gère les évènements liés aux contrôles winform.
- Le 2e thread se charge de lire les données entrantes que le serveur envoie au client.
- Le 3e thread est démarré sur demande et se charge de faire clignoter la fenêtre.
Pour rappel, un thread est un ensemble d'instructions s'exécutant au sein d'un même processus en parallèle aux autres threads démarrés dans ce processus. Ceci permet donc d'avoir plusieurs « entités » de code plus ou moins autonomes. Cela permet donc d'accroître les performances. Pour illustrer ceci, prenons simplement le serveur, s'il n'y avait pas plusieurs threads, à chaque fois qu'il accepte une connexion, il devrait lire les données envoyées par celle-ci et renvoyer ces données aux autres clients. Pendant ce temps, il ne pourrait donc plus se remettre en attente de connexion. En mode multithread, dès qu'il reçoit une connexion, il se remet immédiatement en attente.
I-C. Copies d'écran▲
L'écran de connexion. Entrez successivement le nom du serveur sur lequel l'exécutable « serveur.exe » tourne suivi du nom du pseudo. Vous pouvez enregistrer les paramètres de connexion qui seront stockés dans un fichier pour ne plus devoir rentrer ultérieurement ces paramètres. L'option « Faire clignoter la fenêtre » permet d'activer le clignotement de la fenêtre lorsque celle-ci n'est pas active et que vous recevez un message.
L'écran du chat. C'est ici que vous entrerez vos propres messages et que vous visualiserez les messages des autres utilisateurs. Les notifications de connexion/déconnexion d'autres utilisateurs seront affichées dans le RichTextBox principal. Le RichTextBox dans lequel vous saisissez vos messages est limité à 4 ko.
I-D. Le serveur▲
Veuillez m'excuser pour l'indentation peu lisible du code. Ceci est principalement dû au fait que j'essaye que le code tienne dans une résolution 1024*768.
using
System;
using
System.
Net;
using
System.
Net.
Sockets;
using
System.
Collections;
using
System.
Threading;
using
System.
IO;
namespace
DefaultNamespace
{
public
class
Server:
forwardToAll
{
ArrayList readList=
new
ArrayList
(
);
//liste utilisée par socket.select
string
msgString=
null
;
//contiendra le message envoyé aux autres clients
string
msgDisconnected=
null
;
//Notification connexion/déconnexion
byte
[]
msg;
//Message sous forme de bytes pour socket.send et socket.receive
public
bool
useLogging=
false
;
//booleen permettant de logger le processing dans un fichier log
public
bool
readLock=
false
;
//Flag aidant à la synchronisation
private
string
rtfMsgEncStart=
"\pard\cf1
\b
0
\f
1 "
;
//Code RTF
private
string
rtfMsgContent=
"\cf2 "
;
//code RTF
private
string
rtfConnMsgStart=
"\pard\qc
\b\f
0
\f
s20 "
;
//Code RTF
public
void
Start
(
)
{
//réception de l'adresse IP locale
IPHostEntry ipHostEntry =
Dns.
Resolve
(
Dns.
GetHostName
(
));
IPAddress ipAddress =
ipHostEntry.
AddressList[
0
];
Console.
WriteLine
(
"IP="
+
ipAddress.
ToString
(
));
Socket CurrentClient=
null
;
//Création de la socket
Socket ServerSocket =
new
Socket
(
AddressFamily.
InterNetwork,
SocketType.
Stream,
ProtocolType.
Tcp);
try
{
//On lie la socket au point de communication
ServerSocket.
Bind
(
new
IPEndPoint
(
ipAddress,
8000
));
//On la positionne en mode "écoute"
ServerSocket.
Listen
(
10
);
//Démarrage du thread avant la première connexion client
Thread getReadClients =
new
Thread
(
new
ThreadStart
(
getRead));
getReadClients.
Start
(
);
//Démarrage du thread vérifiant l'état des connexions clientes
Thread pingPongThread =
new
Thread
(
new
ThreadStart
(
CheckIfStillConnected));
pingPongThread.
Start
(
);
//Boucle infinie
while
(
true
){
Console.
WriteLine
(
"Attente d'une nouvelle connexion..."
);
//L'exécution du thread courant est bloquée jusqu'à ce qu'un
//nouveau client se connecte
CurrentClient=
ServerSocket.
Accept
(
);
Console.
WriteLine
(
"Nouveau client:"
+
CurrentClient.
GetHashCode
(
));
//Stockage de la ressource dans l'arraylist acceptlist
acceptList.
Add
(
CurrentClient);
}
}
catch
(
SocketException E)
{
Console.
WriteLine
(
E.
Message);
}
}
//Méthode permettant de générer du logging
//dans un fichier log selon que le membre useLogging
//soit à true ou false
private
void
Logging
(
string
message)
{
using
(
StreamWriter sw =
File.
AppendText
(
"chatServer.log"
))
{
sw.
WriteLine
(
DateTime.
Now+
": "
+
message);
}
}
//Méthode démarrant l'écriture du message reçu par un client
//vers tous les autres clients
private
void
writeToAll
(
)
{
base
.
sendMsg
(
msg);
}
private
void
infoToAll
(
)
{
base
.
sendMsg
(
msgDisconnected);
}
private
void
CheckIfStillConnected
(
)
{
/* Étant donné que la propriété .Connected d'une socket n'est pas
* mise à jour lors de la déconnexion d'un client sans que l'on ait
* préalablement essayé de lire ou d'écrire sur cette socket, cette méthode
* parvient à déterminer si une socket cliente s'est déconnectée grâce à la méthode
* poll. On effectue un poll en lecture sur la socket, si le poll retourne vrai et que
* le nombre de bytes disponibles est 0, il s'agit d'une connexion terminée*/
while
(
true
)
{
for
(
int
i=
0
;
i<
acceptList.
Count;
i++
)
{
if
(((
Socket)acceptList[
i]
).
Poll
(
10
,
SelectMode.
SelectRead) &&
((
Socket)acceptList[
i]
).
Available==
0
)
{
if
(!
readLock)
{
Console.
WriteLine
(
"Client "
+((
Socket)acceptList[
i]
).
GetHashCode
(
)+
" déconnecté"
);
removeNick
(((
Socket)acceptList[
i]
));
((
Socket)acceptList[
i]
).
Close
(
);
acceptList.
Remove
(((
Socket)acceptList[
i]
));
i--;
}
}
}
Thread.
Sleep
(
5
);
}
}
//Vérifie que le pseudo n'est pas déjà attribué à un autre utilisateur
//La Hashtable matchlist ne sert qu'à ça. Pour des développements ultérieurs, elle
//pourrait aussi servir à envoyer la liste de tous les connectés aux utilisateurs
private
bool
checkNick
(
string
nick,
Socket Resource)
{
if
(
MatchList.
ContainsValue
(
nick))
{
//Le pseudo est déjà pris, on refuse la connexion.
((
Socket)acceptList[
acceptList.
IndexOf
(
Resource)]
).
Shutdown
(
SocketShutdown.
Both);
((
Socket)acceptList[
acceptList.
IndexOf
(
Resource)]
).
Close
(
);
acceptList.
Remove
(
Resource);
Console.
WriteLine
(
"Pseudo déjà pris"
);
return
false
;
}
else
{
MatchList.
Add
(
Resource,
nick);
getConnected
(
);
}
return
true
;
}
//Lorsqu'un client se déconnecte, il faut supprimer le pseudo associé à cette connexion
private
void
removeNick
(
Socket Resource)
{
Console.
Write
(
"DECONNEXION DE:"
+
MatchList[
Resource]
);
msgDisconnected=
rtfConnMsgStart+((
string
)MatchList[
Resource]
).
Trim
(
)+
" vient de se déconnecter!\par"
;
Thread DiscInfoToAll=
new
Thread
(
new
ThreadStart
(
infoToAll));
DiscInfoToAll.
Start
(
);
DiscInfoToAll.
Join
(
);
MatchList.
Remove
(
Resource);
}
//Cette méthode est exécutée dans un thread à part
//Elle lit en permanence l'état des sockets connectées et
//vérifie si celles-ci tentent d'envoyer quelque chose
//au serveur. Si tel est le cas, elle réceptionne les paquets
//et appelle forwardToAll pour renvoyer ces paquets vers
//les autres clients.
private
void
getRead
(
)
{
while
(
true
)
{
readList.
Clear
(
);
for
(
int
i=
0
;
i<
acceptList.
Count;
i++
){
readList.
Add
((
Socket)acceptList[
i]
);
}
if
(
readList.
Count>
0
)
{
Socket.
Select
(
readList,
null
,
null
,
1000
);
for
(
int
i=
0
;
i<
readList.
Count;
i++
)
{
if
(((
Socket)readList[
i]
).
Available>
0
)
{
readLock=
true
;
int
paquetsReceived=
0
;
long
sequence=
0
;
string
Nick=
null
;
string
formattedMsg=
null
;
while
(((
Socket)readList[
i]
).
Available>
0
)
{
msg =
new
byte
[((
Socket)readList[
i]
).
Available];
((
Socket)readList[
i]
).
Receive
(
msg,
msg.
Length,
SocketFlags.
None);
msgString=
System.
Text.
Encoding.
UTF8.
GetString
(
msg);
if
(
paquetsReceived==
0
)
{
string
seq=
msgString.
Substring
(
0
,
6
);
try
{
sequence=
Convert.
ToInt64
(
seq);
Nick=
msgString.
Substring
(
6
,
15
);
formattedMsg=
rtfMsgEncStart+
Nick.
Trim
(
)+
" a écrit:"
+
rtfMsgContent+
msgString.
Substring
(
20
,(
msgString.
Length-
20
))+
"\par"
;
}
catch
{
//Ce cas de figure ne devrait normalement
//jamais se produire. Il peut se produire uniquement
//si un client développé par quelqu'un d'autre
//tente de se connecter sur le serveur.
Console.
Write
(
"Message non conforme"
);
acceptList.
Remove
(((
Socket)readList[
i]
));
break
;
}
}
else
{
formattedMsg=
rtfMsgContent+
msgString+
"\par"
;
}
msg=
System.
Text.
Encoding.
UTF8.
GetBytes
(
formattedMsg);
if
(
séquence==
1
){
if
(!
checkNick
(
Nick,((
Socket)readList[
i]
)))
{
break
;
}
else
{
string
rtfMessage=
rtfConnMsgStart+
Nick.
Trim
(
)+
" vient de se connecter\par"
;
msg=
System.
Text.
Encoding.
UTF8.
GetBytes
(
rtfMessage);
}
}
if
(
useLogging)
{
Logging
(
formattedMsg);
}
//Démarrage du thread renvoyant le message à tous les clients
Thread forwardingThread=
new
Thread
(
new
ThreadStart
(
writeToAll));
forwardingThread.
Start
(
);
forwardingThread.
Join
(
);
paquetsReceived++;
}
readLock=
false
;
}
}
}
Thread.
Sleep
(
10
);
}
}
}
public
class
forwardToAll
{
public
ArrayList acceptList=
new
ArrayList
(
);
public
Hashtable MatchList=
new
Hashtable
(
);
public
forwardToAll
(
){}
public
void
sendMsg
(
byte
[]
msg)
{
for
(
int
i=
0
;
i<
acceptList.
Count;
i++
)
{
if
(((
Socket)acceptList[
i]
).
Connected)
{
try
{
int
bytesSent=((
Socket)acceptList[
i]
).
Send
(
msg,
msg.
Length,
SocketFlags.
None);
}
catch
{
Console.
Write
(((
Socket)acceptList[
i]
).
GetHashCode
(
)+
" déconnecté"
);
}
}
else
{
acceptList.
Remove
((
Socket)acceptList[
i]
);
i--;
}
}
}
public
void
sendMsg
(
string
message)
{
for
(
int
i=
0
;
i<
acceptList.
Count;
i++
)
{
if
(((
Socket)acceptList[
i]
).
Connected)
{
try
{
byte
[]
msg=
System.
Text.
Encoding.
UTF8.
GetBytes
(
message);
int
bytesSent=((
Socket)acceptList[
i]
).
Send
(
msg,
msg.
Length,
SocketFlags.
None);
Console.
WriteLine
(
"Writing to:"
+
acceptList.
Count.
ToString
(
));
}
catch
{
Console.
Write
(((
Socket)acceptList[
i]
).
GetHashCode
(
)+
" déconnecté"
);
}
}
else
{
acceptList.
Remove
((
Socket)acceptList[
i]
);
i--;
}
}
}
}
class
MainClass
{
public
static
void
Main
(
string
[]
args)
{
Server startIt=
new
Server
(
);
if
(
args.
Length>
0
)
{
if
(
args[
0
]==
"-log"
&&
args[
1
]==
"true"
)
{
startIt.
useLogging=
true
;
}
}
startIt.
Start
(
);
}
}
}
I-E. Le client▲
using
System;
using
System.
Windows.
Forms;
using
System.
Net;
using
System.
Net.
Sockets;
using
System.
Threading;
using
System.
IO;
using
System.
Runtime.
InteropServices;
using
System.
Text.
RegularExpressions;
namespace
chatClient
{
public
class
MainForm :
System.
Windows.
Forms.
Form
{
private
System.
Windows.
Forms.
TabPage Chat;
private
System.
Windows.
Forms.
TabControl tabControl1;
private
System.
Windows.
Forms.
RichTextBox msgArea;
private
System.
Windows.
Forms.
Button Save;
private
System.
Windows.
Forms.
Label label1;
private
System.
Windows.
Forms.
TextBox Nick;
private
System.
Windows.
Forms.
TextBox ServerHost;
private
System.
Windows.
Forms.
Button sendMsg;
private
System.
Windows.
Forms.
RichTextBox chatBody;
private
System.
Windows.
Forms.
Label label2;
private
System.
Windows.
Forms.
TabPage Connexion;
private
System.
Windows.
Forms.
Button Connect;
private
System.
Windows.
Forms.
CheckBox Notif;
public
Socket ClientSocket=
null
;
public
Thread DataReceived=
null
;
private
Thread Flasht =
null
;
public
string
NickName=
null
;
public
long
sequence=
0
;
public
int
numberMsg=
0
;
private
bool
allowBlink=
false
;
//Je mets cette déclaration sur 3 lignes pour ne pas avoir de scroll horizontal :)
//C'est en fait la déclaration de début d'un document RTF (merci wordpad)
private
const
string
rtfStart=
"{
\\
rtf1
\\
ansi
\\
ansicpg1252
\\
deff0
\\
deflang1033{\\
fonttbl{\\
f0\\
fswiss\\
fcharset0 Arial;}{\\
f1\\
fswiss\\
fprq2\\
fcharset0 Arial;}}{\\
colortbl ;\\
red0\\
green0\\
blue128;\\
red0\\
green128\\
blue0;}\\
viewkind4\\
uc1";
private
string
rtfContent=
null
;
public
MainForm
(
)
{
InitializeComponent
(
);
}
public
static
void
Main
(
string
[]
args)
{
Application.
Run
(
new
MainForm
(
));
}
//Tout le code ci-dessous est automatiquement créé par l'IDE
#region Windows Forms Designer generated code
private
void
InitializeComponent
(
)
{
System.
Resources.
ResourceManager resources =
new
System.
Resources.
ResourceManager
(
typeof
(
MainForm));
this
.
Notif =
new
System.
Windows.
Forms.
CheckBox
(
);
this
.
Connect =
new
System.
Windows.
Forms.
Button
(
);
this
.
Connexion =
new
System.
Windows.
Forms.
TabPage
(
);
this
.
label2 =
new
System.
Windows.
Forms.
Label
(
);
this
.
chatBody =
new
System.
Windows.
Forms.
RichTextBox
(
);
this
.
sendMsg =
new
System.
Windows.
Forms.
Button
(
);
this
.
ServerHost =
new
System.
Windows.
Forms.
TextBox
(
);
this
.
Nick =
new
System.
Windows.
Forms.
TextBox
(
);
this
.
label1 =
new
System.
Windows.
Forms.
Label
(
);
this
.
Save =
new
System.
Windows.
Forms.
Button
(
);
this
.
msgArea =
new
System.
Windows.
Forms.
RichTextBox
(
);
this
.
tabControl1 =
new
System.
Windows.
Forms.
TabControl
(
);
this
.
Chat =
new
System.
Windows.
Forms.
TabPage
(
);
this
.
Connexion.
SuspendLayout
(
);
this
.
tabControl1.
SuspendLayout
(
);
this
.
Chat.
SuspendLayout
(
);
this
.
SuspendLayout
(
);
this
.
Notif.
BackColor =
System.
Drawing.
Color.
IndianRed;
this
.
Notif.
ForeColor =
System.
Drawing.
SystemColors.
ControlLightLight;
this
.
Notif.
Location =
new
System.
Drawing.
Point
(
16
,
144
);
this
.
Notif.
Name =
"Notif"
;
this
.
Notif.
Size =
new
System.
Drawing.
Size
(
152
,
24
);
this
.
Notif.
TabIndex =
12
;
this
.
Notif.
Text =
"Faire clignoter la fenêtre"
;
this
.
Connect.
BackColor =
System.
Drawing.
Color.
IndianRed;
this
.
Connect.
ForeColor =
System.
Drawing.
Color.
WhiteSmoke;
this
.
Connect.
Location =
new
System.
Drawing.
Point
(
16
,
96
);
this
.
Connect.
Name =
"Connect"
;
this
.
Connect.
Size =
new
System.
Drawing.
Size
(
104
,
23
);
this
.
Connect.
TabIndex =
6
;
this
.
Connect.
Text =
"Se connecter"
;
this
.
Connect.
Click +=
new
System.
EventHandler
(
this
.
Button2Click);
this
.
Connexion.
BackgroundImage =
((
System.
Drawing.
Image)(
resources.
GetObject
(
"Connexion.BackgroundImage"
)));
this
.
Connexion.
Controls.
Add
(
this
.
Notif);
this
.
Connexion.
Controls.
Add
(
this
.
Save);
this
.
Connexion.
Controls.
Add
(
this
.
Nick);
this
.
Connexion.
Controls.
Add
(
this
.
label2);
this
.
Connexion.
Controls.
Add
(
this
.
label1);
this
.
Connexion.
Controls.
Add
(
this
.
ServerHost);
this
.
Connexion.
Controls.
Add
(
this
.
Connect);
this
.
Connexion.
Location =
new
System.
Drawing.
Point
(
4
,
22
);
this
.
Connexion.
Name =
"Connexion"
;
this
.
Connexion.
Size =
new
System.
Drawing.
Size
(
528
,
310
);
this
.
Connexion.
TabIndex =
1
;
this
.
Connexion.
Text =
"Connexion"
;
this
.
label2.
AutoSize =
true
;
this
.
label2.
Location =
new
System.
Drawing.
Point
(
16
,
48
);
this
.
label2.
Name =
"label2"
;
this
.
label2.
Size =
new
System.
Drawing.
Size
(
71
,
16
);
this
.
label2.
TabIndex =
9
;
this
.
label2.
Text =
"Votre pseudo"
;
this
.
chatBody.
Font =
new
System.
Drawing.
Font
(
"Tahoma"
,
8
.
25F
,
System.
Drawing.
FontStyle.
Regular,
System.
Drawing.
GraphicsUnit.
Point,
((
System.
Byte)(
0
)));
this
.
chatBody.
Location =
new
System.
Drawing.
Point
(
0
,
8
);
this
.
chatBody.
Name =
"chatBody"
;
this
.
chatBody.
ReadOnly =
true
;
this
.
chatBody.
Size =
new
System.
Drawing.
Size
(
512
,
192
);
this
.
chatBody.
TabIndex =
8
;
this
.
chatBody.
Text =
""
;
this
.
chatBody.
TextChanged +=
new
System.
EventHandler
(
this
.
HandleAutoScroll);
this
.
sendMsg.
BackColor =
System.
Drawing.
Color.
IndianRed;
this
.
sendMsg.
ForeColor =
System.
Drawing.
Color.
WhiteSmoke;
this
.
sendMsg.
Location =
new
System.
Drawing.
Point
(
208
,
272
);
this
.
sendMsg.
Name =
"sendMsg"
;
this
.
sendMsg.
TabIndex =
5
;
this
.
sendMsg.
Text =
"Envoyer"
;
this
.
sendMsg.
Click +=
new
System.
EventHandler
(
this
.
SendMessage);
this
.
ServerHost.
BackColor =
System.
Drawing.
Color.
LightGray;
this
.
ServerHost.
Location =
new
System.
Drawing.
Point
(
160
,
16
);
this
.
ServerHost.
Name =
"ServerHost"
;
this
.
ServerHost.
Size =
new
System.
Drawing.
Size
(
128
,
20
);
this
.
ServerHost.
TabIndex =
7
;
this
.
ServerHost.
Text =
""
;
this
.
Nick.
BackColor =
System.
Drawing.
Color.
LightGray;
this
.
Nick.
Location =
new
System.
Drawing.
Point
(
160
,
48
);
this
.
Nick.
Name =
"Nick"
;
this
.
Nick.
Size =
new
System.
Drawing.
Size
(
128
,
20
);
this
.
Nick.
TabIndex =
10
;
this
.
Nick.
Text =
""
;
this
.
label1.
AutoSize =
true
;
this
.
label1.
Location =
new
System.
Drawing.
Point
(
16
,
16
);
this
.
label1.
Name =
"label1"
;
this
.
label1.
Size =
new
System.
Drawing.
Size
(
130
,
16
);
this
.
label1.
TabIndex =
8
;
this
.
label1.
Text =
"Entrez le nom du serveur"
;
this
.
Save.
BackColor =
System.
Drawing.
Color.
IndianRed;
this
.
Save.
ForeColor =
System.
Drawing.
Color.
WhiteSmoke;
this
.
Save.
Location =
new
System.
Drawing.
Point
(
160
,
96
);
this
.
Save.
Name =
"Save"
;
this
.
Save.
Size =
new
System.
Drawing.
Size
(
216
,
23
);
this
.
Save.
TabIndex =
11
;
this
.
Save.
Text =
"Enregistrer les paramètres de connexion"
;
this
.
Save.
Click +=
new
System.
EventHandler
(
this
.
SaveClick);
this
.
msgArea.
Location =
new
System.
Drawing.
Point
(
0
,
200
);
this
.
msgArea.
MaxLength =
2048
;
this
.
msgArea.
Name =
"msgArea"
;
this
.
msgArea.
Size =
new
System.
Drawing.
Size
(
512
,
64
);
this
.
msgArea.
TabIndex =
9
;
this
.
msgArea.
Text =
""
;
this
.
tabControl1.
Controls.
Add
(
this
.
Chat);
this
.
tabControl1.
Controls.
Add
(
this
.
Connexion);
this
.
tabControl1.
Location =
new
System.
Drawing.
Point
(
16
,
16
);
this
.
tabControl1.
Name =
"tabControl1"
;
this
.
tabControl1.
SelectedIndex =
0
;
this
.
tabControl1.
Size =
new
System.
Drawing.
Size
(
536
,
336
);
this
.
tabControl1.
TabIndex =
6
;
this
.
Chat.
BackgroundImage =
((
System.
Drawing.
Image)(
resources.
GetObject
(
"Chat.BackgroundImage"
)));
this
.
Chat.
Controls.
Add
(
this
.
msgArea);
this
.
Chat.
Controls.
Add
(
this
.
chatBody);
this
.
Chat.
Controls.
Add
(
this
.
sendMsg);
this
.
Chat.
Location =
new
System.
Drawing.
Point
(
4
,
22
);
this
.
Chat.
Name =
"Chat"
;
this
.
Chat.
Size =
new
System.
Drawing.
Size
(
528
,
310
);
this
.
Chat.
TabIndex =
0
;
this
.
Chat.
Text =
"Chat"
;
this
.
AutoScaleBaseSize =
new
System.
Drawing.
Size
(
5
,
13
);
this
.
BackgroundImage =
((
System.
Drawing.
Image)(
resources.
GetObject
(
"$this.BackgroundImage"
)));
this
.
ClientSize =
new
System.
Drawing.
Size
(
576
,
389
);
this
.
Controls.
Add
(
this
.
tabControl1);
this
.
Name =
"MainForm"
;
this
.
Text =
"Chat"
;
this
.
Load +=
new
System.
EventHandler
(
this
.
MainFormLoad);
this
.
Activated +=
new
System.
EventHandler
(
this
.
StopBlink);
this
.
Deactivate +=
new
System.
EventHandler
(
this
.
StartBlink);
//Tenez cette instruction à l'oeil si vous passez en mode design avec
//Sharpdevelop, car il l'enlève automatiquement, il faudra donc la remettre ou gérer l'évènement
//différemment
this
.
Closing +=
new
System.
ComponentModel.
CancelEventHandler
(
OnClosing);
this
.
Connexion.
ResumeLayout
(
false
);
this
.
tabControl1.
ResumeLayout
(
false
);
this
.
Chat.
ResumeLayout
(
false
);
this
.
ResumeLayout
(
false
);
}
#endregion
//Appel à user32.dll, sert à faire clignoter la fenêtre
[ DllImport(
"user32.dll"
) ]
static
extern
int
FlashWindow
(
int
hwnd,
int
bInvert
);
//Sert à faire clignoter la fenêtre
private
void
Flash
(
)
{
while
(
true
)
{
if
(
allowBlink)
{
FlashWindow
((
int
)this
.
Handle,
1
);
Thread.
Sleep
(
500
);
}
else
{
Thread.
CurrentThread.
Abort
(
);
}
}
}
//La fenêtre ne peut clignoter que si elle n'est pas active
void
StartBlink
(
object
sender,
System.
EventArgs e)
{
allowBlink=
true
;
}
void
StopBlink
(
object
sender,
System.
EventArgs e)
{
allowBlink=
false
;
}
//Cette méthode traite le message à envoyer sur le serveur
void
SendMessage
(
object
sender,
System.
EventArgs e)
{
//On vérifie que le client est bien connecté
if
(
ClientSocket==
null
||
!
ClientSocket.
Connected)
{
MessageBox.
Show
(
"Vous n'êtes pas connecté"
);
return
;
}
try
{
if
(
msgArea.
Text!=
""
)
{
//Étant donné qu'on travaille en RTF, on échappe les caractères
//spéciaux avant de les envoyer sur le serveur qui nous renverra
//le message ainsi qu'aux autres clients connectés
//Si vous travaillez en mode texte, vous n'aurez pas à vous soucier
//de tout cela
string
reformattedBuffer=
msgArea.
Text.
Replace
(
"}"
,
"
\\
}"
);
reformattedBuffer=
reformattedBuffer.
Replace
(
"
\n
"
,
"
\\
par
\r\n
"
);
//Chaque message est précédé d'un numéro de séquence, ce qui permet
//de vérifier si le client vient de se connecter ou non
SendMsg
(
GetSequence
(
)+
NickName+
reformattedBuffer.
Replace
(
"{"
,
"
\\
{"
));
msgArea.
Clear
(
);
}
}
catch
(
Exception E)
{
MessageBox.
Show
(
"SendMessage:"
+
E.
Message);
}
}
//Si les paramètres de connexion ont été enregistrés, on les récupère
//via cette méthode
void
getParams
(
)
{
if
(
File.
Exists
(
"params.ini"
))
{
using
(
StreamReader SR=
new
StreamReader
(
"params.ini"
))
{
ServerHost.
Text=
SR.
ReadLine
(
);
Nick.
Text=
SR.
ReadLine
(
);
SR.
Close
(
);
}
}
}
//Cette méthode envoie le message sur le serveur
void
SendMsg
(
string
message)
{
byte
[]
msg =
System.
Text.
Encoding.
UTF8.
GetBytes
(
message);
int
DtSent=
ClientSocket.
Send
(
msg,
msg.
Length,
SocketFlags.
None);
if
(
DtSent ==
0
)
{
MessageBox.
Show
(
"Aucune donnée n'a été envoyée"
);
}
}
//Cette méthode permet de récupérer l'adresse ip du serveur sur lequel
//on désire se connecter
private
String GetAdr
(
)
{
try
{
IPHostEntry iphostentry =
Dns.
GetHostByName
(
ServerHost.
Text);
String IPStr =
""
;
foreach
(
IPAddress ipaddress in
iphostentry.
AddressList){
IPStr =
ipaddress.
ToString
(
);
return
IPStr;
}
}
catch
(
SocketException E)
{
MessageBox.
Show
(
E.
Message);
}
return
""
;
}
//Cette méthode est appelée par un thread à part qui lit constamment la socket
//pour voir si le serveur essaye d'écrire dessus
private
void
CheckData
(
)
{
try
{
while
(
true
)
{
if
(
ClientSocket.
Connected)
{
if
(
ClientSocket.
Poll
(
10
,
SelectMode.
SelectRead) &&
ClientSocket.
Available==
0
)
{
//La connexion a été clôturée par le serveur ou bien un problème
//réseau est apparu
MessageBox.
Show
(
"La connexion au serveur est interrompue. Essayez avec un autre pseudo"
);
Connect.
Enabled=
true
;
Thread.
CurrentThread.
Abort
(
);
}
//Si la socket a des données à lire
if
(
ClientSocket.
Available>
0
)
{
string
messageReceived=
null
;
if
(
Flasht==
null
)
{
if
(
allowBlink &&
Notif.
Checked)
{
Flasht =
new
Thread
(
new
ThreadStart
(
Flash));
Flasht.
Start
(
);
}
}
else
{
if
(
allowBlink &&
Notif.
Checked &&
Flasht.
IsAlive==
false
)
{
Flasht =
new
Thread
(
new
ThreadStart
(
Flash));
Flasht.
Start
(
);
}
}
while
(
ClientSocket.
Available>
0
)
{
try
{
byte
[]
msg=
new
Byte[
ClientSocket.
Available];
//Réception des données
ClientSocket.
Receive
(
msg,
0
,
ClientSocket.
Available,
SocketFlags.
None);
messageReceived=
System.
Text.
Encoding.
UTF8.
GetString
(
msg).
Trim
(
);
//On concatène les données reçues(max 4ko) dans
//une variable de la classe
rtfContent+=
messageReceived;
}
catch
(
SocketException E)
{
MessageBox.
Show
(
"CheckData read"
+
E.
Message);
}
}
try
{
//On remplit le richtextbox avec les données reçues
//lorsqu'on a tout réceptionné
chatBody.
Rtf=
rtfStart+
rtfContent;
this
.
BringToFront
(
);
}
catch
(
Exception E)
{
MessageBox.
Show
(
E.
Message);
}
}
}
//On temporise pendant 10 millisecondes, ceci pour éviter
//que le micro processeur s'emballe
Thread.
Sleep
(
10
);
}
}
catch
{
//Ce thread étant susceptible d'être arrêté à tout moment
//on catch l'exception afin de ne pas afficher un message à l'utilisateur
Thread.
ResetAbort
(
);
}
}
//Cette méthode enregistre les paramètres de connexion dans le fichier
void
SaveClick
(
object
sender,
System.
EventArgs e)
{
if
(
Nick.
Text !=
""
&&
ServerHost.
Text !=
""
)
{
using
(
StreamWriter SW=
new
StreamWriter
(
"params.ini"
))
{
SW.
WriteLine
(
ServerHost.
Text);
SW.
WriteLine
(
Nick.
Text);
SW.
Close
(
);
}
}
else
{
MessageBox.
Show
(
"Veuillez saisir le nom du serveur et le pseudo"
);
}
}
void
Button2Click
(
object
sender,
System.
EventArgs e)
{
if
(
Nick.
Text==
""
)
{
MessageBox.
Show
(
"Le pseudo ne peut tre null"
);
return
;
}
if
(
ServerHost.
Text==
""
)
{
MessageBox.
Show
(
"Le nom du serveur ne peut être null"
);
return
;
}
//On formate le pseudo sur une longueur de 15 caractères
NickName=
Nick.
Text.
Trim
(
);
if
(
NickName.
Length<
15
)
{
char
pad=
Convert.
ToChar
(
" "
);
NickName=
NickName.
PadRight
(
15
,
pad);
}
else
if
(
NickName.
Length >
15
)
{
MessageBox.
Show
(
"Le pseudo doit être de 15 caractères maximum"
);
return
;
}
//Chaque message sera précédé d'un numéro de séquence
//Le numéro de séquence 1 servira à identifier le pseudo
//côté serveur.
sequence=
0
;
IPAddress ip =
IPAddress.
Parse (
GetAdr
(
));
IPEndPoint ipEnd =
new
IPEndPoint (
ip,
8000
);
ClientSocket=
new
Socket
(
AddressFamily.
InterNetwork,
SocketType.
Stream,
ProtocolType.
Tcp );
try
{
ClientSocket.
Connect
(
ipEnd);
if
(
ClientSocket.
Connected)
{
SendMsg
(
GetSequence
(
)+
NickName);
Connect.
Enabled=
false
;
}
}
catch
(
SocketException E)
{
MessageBox.
Show
(
"Connection"
+
E.
Message);
}
try
{
DataReceived =
new
Thread
(
new
ThreadStart
(
CheckData));
DataReceived.
Start
(
);
}
catch
(
Exception E)
{
MessageBox.
Show
(
"Démarrage Thread"
+
E.
Message);
}
}
//Cette méthode génère le numéro de séquence collé en
//entête du message envoyé au serveur
string
GetSequence
(
)
{
sequence++;
string
msgSeq=
Convert.
ToString
(
sequence);
char
pad=
Convert.
ToChar
(
"0"
);
msgSeq=
msgSeq.
PadLeft
(
6
,
pad);
return
msgSeq;
}
//Cette méthode provoque l'auto-scroll du richtextbox dès
void
HandleAutoScroll
(
object
sender,
System.
EventArgs e)
{
chatBody.
SelectionStart =
chatBody.
Rtf.
Length;
chatBody.
Focus
(
);
msgArea.
Focus
(
);
}
//Cette méthode est exécutée lorsque l'on quitte l'application.
private
void
OnClosing
(
object
sender,
System.
ComponentModel.
CancelEventArgs e)
{
//Si le thread recevant les données a été démarré, on l'arrête
if
(
DataReceived!=
null
)
{
try
{
DataReceived.
Abort
(
);
DataReceived.
Join
(
);
}
catch
(
Exception E)
{
MessageBox.
Show
(
"Arrt Thread"
+
E.
Message);
}
}
if
(
ClientSocket !=
null
&&
ClientSocket.
Connected)
{
try
{
ClientSocket.
Shutdown
(
SocketShutdown.
Both);
ClientSocket.
Close
(
);
if
(
ClientSocket.
Connected) {
MessageBox.
Show
(
"Erreur: "
+
Convert.
ToString
(
System.
Runtime.
InteropServices.
Marshal.
GetLastWin32Error
(
)));
}
}
catch
(
SocketException SE)
{
MessageBox.
Show
(
"SE"
+
SE.
Message);
}
}
}
void
MainFormLoad
(
object
sender,
System.
EventArgs e)
{
getParams
(
);
}
}
}
II. Téléchargement▲
Le projet a été développé avec SharpDevelop, si vous désirez le modifier, le plus simple est de le faire avec SharpDevelop, mais rien ne vous empêche bien sûr de l'importer dans Visual Studio .NET. Téléchargez donc les deux zip :