Dans une application web les WebSocket sont un moyen de communication entre clients et serveur. Cette technologie permet de faire communiquer votre client (navigateur) et votre serveur différemment et plus rapidement.
A quoi ça sert ?
Tout d’abord, expliquons comment un serveur et un client communique de manière standard. Le client demande une page (représentée par une URL), et le serveur lui répond en lui renvoyant la page demandée. A cela, on peut ajouter la possibilité d’envoyer des variables en même temps que la demande de la page (POST ou GET), cela permet notamment de personnaliser la page renvoyée ou d’enregistrer les réponses à un formulaire. Les WebSocket permettent de faire beaucoup plus qu’une simple requête de page sur un serveur, ils créent un canal de communication entre un serveur et un client qui restera ouvert sans consommer de bande passante (pas de polling) et qui sera utilisable dans les deux sens de communication. Un WebSocket est ouvert par le serveur, il écoute sur un port donné en attendant la connexion d’un ou plusieurs client. Une fois le client connecté, le client comme le serveur peuvent envoyer un message à l’autre partie.
Les WebSocket permettent une communication bidirectionnel full-duplex (pour les puristes, le full-duplex inclu la bidirectionnalité ;). Tout d’abord, arrêtons nous sur le premier terme : bidirectionnel, cela signifie simplement que l’on peut envoyer des messages dans un sens (client vers serveur) ou dans l’autre (serveur vers client). Ensuite, le full-duplex nous permet de communiquer dans les deux sens en même temps. Ainsi, nous n’avons pas à gérer la synchronisation entre le client et le serveur (comme des talky-walky). Tout le monde peut parler en même temps. Il est également possible d’envoyer des messages n’importe quand, ils arriveront à bon port. Nous sommes sûr que les messages arrivent à destination car les WebSocket utilisent une connexion TCP, ce qui nous donne donc un mode connecté et l’assurance de la bonne réception des données. Fonctionnant au travers du protocole HTTP, il peut passer au travers des pare-feu et des proxis.
Voici un schéma qui explique cela :
Le petit truc qu’on ne pouvait pas faire avant …
Avec les WebSocket, il est possible que le serveur envoie un message au client. Ceci peut paraître anodin, mais sans ce mécanisme, il est quasi-impossible au serveur de parler directement au client. Par exemple, avant, pour réaliser un t’chat, il fallait que le client interroge régulièrement le serveur pour savoir si un nouveau message était arrivé, cela n’était pas très optimisé. Avec les WebSocket, il est possible de créer un réel dynamisme à nos pages web (encore plus qu’avec de l’Ajax). En effet, l’Ajax nous permettait d’interroger le serveur. C’est maintenant le serveur qui nous envoi directement les informations sans qu’on ait à lui demander.
Comment faire en ASP.NET MVC ?
Le mieux avec cette technologie, c’est qu’elle est très simple d’utilisation, coté serveur comme coté client. Dans l’exemple suivant, nous allons utiliser un serveur sur lequel est déployée une application web en ASP.NET MVC. Il est aussi possible de créer un WebSocket avec d’autres types d’applications (PHP, Java, Python, C++, … ). Mon exemple sera un jeu de Dames.
Nous allons tester les WebSocket en envoyant des ordres et des paramètres séparés par des virgules, le tout sous la forme d’une chaîne de caractères. Dans jeu de Dames, le serveur fait office de plateau de jeu et les clients sont des joueurs. Ainsi, les joueurs (client) informent le plateau (serveur) de son déplacement et le plateau informe tous les joueurs de ce déplacement. Pour cela, je vais envoyer une chaîne de caractère formatée comme suit :
<ordre>;<param1>;<param2>;<param3>;…
Voila les différents ordres possible :
- CANPLAY (serveur -> client : il indique au client que c’est à sont tour de jouer)
- ONPLAYING (client -> serveur : indique qu’un joueur veut déplacer un pion)
- ONPLAYED (serveur -> client : indique qu’un joueur a déplacé son pion)
Une partie se déroule comme suit :
- le serveur se lance
- le WebSocket est initialisé
- les deux joueurs se rendent sur l’application et se connectent au WebSocket
- l’un des deux joueurs (le joueur 1) est désigné par le serveur puis un message lui est envoyé (CANPLAY)
- le joueur 1 joue en déplaçant un pion, puis son navigateur envoie le déplacement effectué au serveur (ONPLAYING)
- le serveur vérifie que le déplacement est possible puis l’envoie à tous les joueurs (ONPLAYED)
- le serveur change de joueur et lui envoi le message CANPLAY.
Coté serveur
L’utilisation de WebSocket coté serveur est très simple. Pour l’exemple, j’ai choisi d’utiliser le package Nugget Fleck. D’autre package Nuget existe (WebSocket4Net, SocketIO4Net. Client, Microsoft. WebSockets). Sachez aussi que la version 4.5 de WCF l’implémente aussi. Nous allons créer un projet ASP.NET MVC et ajouter le package NuGet Fleck.
La référence Fleck dans le projet
Pour notre exemple nous allons créer certains fichiers (vide pour le moment) :
- créez un nouveau controller GameController
- créez une classe Board qui serra le moteur de jeu
- créez une classe CommunicationManager qui utilisera notre WekSocket
- créez un fichier Javascript SocketConnection. js
Voilà pour les bases de notre solution, vous devriez avoir une arborescence comme ceci :
Arborescence du projet complet
Maintenant nous allons remplir tout cela. Le package Fleck nous expose une classe WebSocketServer, qui sera notre socket coté serveur et qui aura pour but d’écouter ce qui se passe sur un port donné. L’interface IWebSocketConnection permet d’identifier un client qui se connecte ou qui envoi un message. Voici le détail de cette interface :
namespace Fleck
{
public interface IWebSocketConnection
{
IWebSocketConnectionInfo ConnectionInfo { get; }
bool IsAvailable { get; }
Action<byte[]> OnBinary { get; set; }
Action OnClose { get; set; }
Action<Exception> OnError { get; set; }
Action<string> OnMessage { get; set; }
Action OnOpen { get; set; }
Action<byte[]> OnPing { get; set; }
Action<byte[]> OnPong { get; set; }
void Close();
void Send(byte[] message);
void Send(string message);
void SendPing(byte[] message);
void SendPong(byte[] message);
}
}
On peut apercevoir la propriété ConnectionInfo de type IWebSocketConnectionInfo :
namespace Fleck
{
public interface IWebSocketConnectionInfo
{
string ClientIpAddress { get; }
int ClientPort { get; }
IDictionary<string, string> Cookies { get; }
string Host { get; }
Guid Id { get; }
string NegotiatedSubProtocol { get; }
string Origin { get; }
string Path { get; }
string SubProtocol { get; }
}
}
Cette propriété nous permettra d’identifier un client qui se connecte, envoi un message ou se déconnecte. Pour les besoins de l’exemple, la classe Board ne contiendra que deux méthodes afin d’assurer un minimum de gestion. Le méthode CheckMovement permet de vérifier si un mouvement est possible et la méthode ApplyMovement permet d’appliquer un déplacement de pion sur le plateau de jeu.
public class Board
{
public bool CheckMovement(int x1, int y1, int x2, int y2)
{
//check if the movement is valid
//...
return true;
}
public void ApplyMovement(int x1, int y1, int x2, int y2)
{
//Apply the movement on the server board
//...
}
}
Ensuite, notre WebSocket doit être initialisé une seul fois. Pour cela, la classe CommunicationManager qui va gérer le Websocket sera un Singleton. Ce Singleton sera instancié une première fois dans la méthode application_start.
La classe CommunicationManager doit contenir une propriété de type WebSocketServer qui provient du package Fleck, se sera notre socket. Le WebSocket nous offre trois actions : OnOpen, OnClose et OnMessage. Nous allons aussi garder la liste des clients connectés afin de pouvoir leur envoyer des messages par la suite. Cette liste de client sera :
List<IWebSocketConnection> _clients;
Voici une première version de la classe Communication Manager :
namespace WebSocket
{
public class CommunicationManager
{
private static CommunicationManager _instance;
public static CommunicationManager GetInstance()
{
if (_instance == null)
_instance = new CommunicationManager();
return _instance;
}
private WebSocketServer _socket;
private List<IWebSocketConnection> _clients;
private CommunicationManager()
{
_socket = new WebSocketServer("ws://localhost:8181");
_socket.Start(socket =>
{
socket.OnOpen = () => OnOpen<g(socket);
socket.OnClose = () => OnClose(socket);
socket.OnMessage = message => OnReceivedMessage(socket, message);
});
}
public void OnOpen(IWebSocketConnection client)
{
_clients.Add(client);
}
public void OnClose(IWebSocketConnection client)
{
_clients.Remove(client);
}
public void OnReceivedMessage(IWebSocketConnection client, string message)
{
}
}
}
Ensuite, nous allons traiter les messages qui arrivent. Le serveur ne peut recevoir qu’un seul message, “ONPLAYING”. Ce message indique qu’un joueur a déplacé un pion, nous recevons aussi les coordonnées de départ et d’arrivée du pion en paramètre. A la réception de ce message nous devons :
- s’assurer de la possibilité du déplacement
- déplacer le pion sur le plateau
- informer tout les joueurs du déplacement
Voici la classe complétée :
public class CommunicationManager
{
private static CommunicationManager _instance;
public static CommunicationManager GetInstance()
{
if (_instance == null)
_instance = new CommunicationManager();
return _instance;
}
private WebSocketServer _socket;
private Board _board;
private List<IWebSocketConnection> _clients;
private CommunicationManager()
{
_board = new Board();
_clients = new List<IWebSocketConnection>();
_socket = new WebSocketServer("ws://localhost:8181");
_socket.Start(socket =>
{
socket.OnOpen = () => OnOpen(socket);
socket.OnClose = () => OnClose(socket);
socket.OnMessage = message => OnReceivedMessage(socket, message);
});
}
public void OnOpen(IWebSocketConnection client)
{
_clients.Add(client);
}
public void OnClose(IWebSocketConnection client)
{
_clients.Remove(client);
}
public void OnReceivedMessage(IWebSocketConnection client, string message)
{
var args = message.Split(';');
switch ((string)args[0])
{
case "ONPLAYING":
//check the movement before applying it
if (_board.CheckMovement(Int32.Parse(args[1]),
Int32.Parse(args[2]),
Int32.Parse(args[3]),
Int32.Parse(args[4])))
{
//apply the movement (on the server)
_board.ApplyMovement(Int32.Parse(args[1]),
Int32.Parse(args[2]),
Int32.Parse(args[3]),
Int32.Parse(args[4]));
//apply the movement (on each client)
foreach (var c in _clients)
c.Send(string.Format("ONPLAYED;{0};{1};{2};{3}",
args[1],
args[2],
args[3],
args[4]));
}
else
{
//the movement is not applicable, play again
client.Send("CANPLAY");
}
break;
}
}
}
Voila, nous avons terminé notre code serveur, nous allons maintenant voir la partie client.
Coté client
Pour pouvoir se connecter à notre WebSocket coté client, c’est à dire dans un navigateur, tout le code se ferra en Javascript. Une API WebSocket est disponible en HTML5 et qui ne nécessite aucune référence externe (pas de fichiers Javascript à inclure dans le projet et pas de bug de la part de ces fichiers). J’entends déjà certains dire que l’utilisation n’est pas possible sur tout les navigateurs. Et bien si ! Cette technologie est accessible sur la plupart des navigateurs aujourd’hui. Comme du coté serveur, le WebSocket nous offre trois actions :
var ThisIsMyTurn = false
var socket = new Websocket("ws://localhost:8181/");
socket.onopen = function (e) { }
socket.onmessage = function (e) { };
socket.onclose = function (e) { }
Lorsque le joueur a terminé le déplacement de son pion la fonction OnMoveEnded est appelée. La fonction MovePawn permet de déplacer visuellement un pion sur le plateau d’un joueur (dans le navigateur). Un joueur peut recevoir deux ordres du serveur : CANPLAY et ONPLAYED, il peut aussi en envoyer un au serveur ONPLAYING. Voici le code compléter coté client :
var ThisIsMyTurn = false
var socket = new Websocket("ws://localhost:8181/");
socket.onopen = function (e) { }
socket.onmessage = OnMessage;
socket.onclose = function (e) { }
function MovePawn(x1, y1, x2, y2)
{
// Move a pawn on the board from coordinates (x1, y1) to (x2, y2)
//...
}
function MyTurn()
{
//Allow the current player to play his turn
ThisIsMyTurn = true;
// ...
}
function OnMoveEnded(x1, y1, x2, y2)
{
socket.send('ONPLAYING;' + x1 + ';' + y1 + ';' + x2 + ';' + y2);
ThisIsMyTurn = false;
}
function OnMessage(event)
{
var args = event.msg.split(';');
switch(args[0])
{
case 'CANPLAY' :
MyTurn();
break;
case 'ONPLAYED' :
MovePawn(args[1], args[2], args[3], args[4]);
break;
}
}
Et voila, le coté client est réalisé ! Ce n’est pas plus compliqué. A noter qu’avec les WebSocket il est aussi possible d’envoyer des objets au format JSon.
Pour plus d’informations voici quelques liens qui mon permis de rédiger cet article :
http://en.wikipedia.org/wiki/WebSocket
http://www.nuget.org/packages/Fleck/
http://alexjmackey.wordpress.com/2012/05/01/websockets-with-asp-net-4-5-and-visual-studio-11/
http://msdn.microsoft.com/en-us/library/hh674271(v=vs.110).aspx
http://fr.wikipedia.org/wiki/Duplex_(canal_de_communication)
http://www.html5rocks.com/en/tutorials/websockets/basics/
http://www.websocket.org/quantum.html
http://germanlinux.blogspot.fr/2012/11/quest-quun-websocket.html