Bien que Silverlight 2 permette des connexions HTTP (basicHTTP) vers un serveur WCF, l’envie peut nous prendre d’en vouloir plus.

Pour pouvoir garder une connexion ouverte entre serveur et client, recevoir à n’importe quel instant une donnée, du client au serveur, ou même du serveur au client, rien de mieux qu’une connexion TCP/IP.

Dans cet article, nous allons voir pas à pas comment créer ce type de connexion ainsi que comment en rendre les assembly génériques et réutilisables.

Nous suivrons donc la route suivante :

  1. Comprendre TCP IP sous .Net
    1. Envoyer l’heure à travers TCP IP
  2. Comprendre le policy-file-request de Silverlight
    1. Ajouter à notre serveur un port écoutant les requêtes policy-file-request et y répondant correctement
  3. Créer un client TCP Silverlight 2
    1. Créer un client TCP Silverlight qui lit un nombre primitif
    2. Utiliser des DataContract et un DataContractSerializer
    3. Comprendre les KnowTypes
  4. Rendre le tout générique et réutilisable (article  préview)
[Downloadez la solution complete de cette article ici]

1. Comprendre TCP IP sous .Net

La plateforme .Net offre sous le namespace System.Net.Sockets tous les outils nécessaire à une création facile de connexion TCP.

Ainsi les deux objets suivants vont être utilisé :

  1. TcpListener : sert à ouvrir le port d’une machine en mode LISTEN. Des clients vont alors pouvoir s’y connecter. (Ceci se fait coté serveur)
  2. TcpClient : sert autant du coté server que du coté client. Une foi connecté, le client et le serveur se référence réciproquement par un objet TcpClient donnant au programmeur un accès facile au fonctions d’envoie de donnée, de gestion de la connexion, etc.

Voici un serveur n’acceptant qu’un seul client, lui envoyant une donnée (int data = 356) et se coupant ensuite.

using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;

namespace testServer
{
    class Program
    {
        /// <summary>
        /// Ce listener est utilisé pour écouter les
        /// requêtes de connexion de nouveaux clients
        /// </summary>
        TcpListener listener;

        /// <summary>
        /// Ce client représente l'unique client Tcp
        /// que nous allons accepter.
        /// C'est grâce à cet object que nous allons
        /// pouvoir envoyer une données au client
        /// </summary>
        TcpClient client;

        AutoResetEvent OperationCompleted;

        internal Program()
        {
            OperationCompleted = new AutoResetEvent(false);

            listener = new TcpListener(IPAddress.Any, 5000);
            listener.Start();
            listener.BeginAcceptSocket(OnClientConnected, null);

            OperationCompleted.WaitOne();
        }

        void OnClientConnected(IAsyncResult param)
        {
            int data = 356;

            client = listener.EndAcceptTcpClient(param);
            client.Client.Send(BitConverter.GetBytes(data));
            client.Close();

            OperationCompleted.Set();
        }

        static void Main(string[] args)
        {
            new Program();
        }

    }
}

Du coté client, c’est tout aussi simple :

using System;
using System.Net.Sockets;

namespace testClient
{
    class Program
    {
        internal Program()
        {

            //Création d’un buffer pour stocker les données reçues
            //du server
            byte[] buffer = new byte[sizeof(int)];

           TcpClient client = new TcpClient();
            client.Connect("localhost", 5000);
            client.Client.Receive(buffer);

            //conversion des données reçues du serveur
            int data = BitConverter.ToInt32(buffer, 0);

            Console.WriteLine("Received : {0}", data);
            Console.Read();
        }

        static void Main(string[] args)
        {
            new Program();
        }
    }
}

 image

1.1 Envoyer l’heure à travers TCP IP

Comme vous avez pu le constater, les seules choses qu’il est possible d’envoyer à travers une connexion TCP IP sont des bytes.

Pour pouvoir envoyer un objet plus complexe, tel qu’un DateTime, il va nous falloir le “sérialiser” avant l’envoie et le “désérialiser” à la réception.

La sérialisation est le procéder de transformation d’un objet en un tableau de byte. C’est en outre une bonne méthode pour sauvegarder des objets dans un fichier.

Pour pouvoir être sérialisé, la class d’un objet doit porter l’attribut ISerializable ou un attribut qui en dérive. Vous trouverez facilement plus d’info sur cet attribut sur internet. (Nous verrons plus tard que pour communiquer avec Silverlight l’attribut à utiliser est DataContract)

Les outils de sérialisation sont disponible dans le namespace System.Runtime.Serialization.

Du coté server, il nous suffit de changer la fonction OnClientConnected :

void OnClientConnected(IAsyncResult param)
{
    client = listener.EndAcceptTcpClient(param);

    //Recupération de l'heure
    DateTime now = DateTime.Now;

    //Création d'un buffer temporaire dans lequel
    //on va sérialiser l'heure
    byte[] buffer = new byte[100];

    //Encapsulation du buffer dans un stream mémoire
    //permetant ainsi une ecriture facile
    MemoryStream stream = new MemoryStream(buffer);

    //Création d'un BinaryFormatter et Sérialisation
    //de l'heure dans le stream -> donc dans le buffer
    BinaryFormatter bf = new BinaryFormatter();
    bf.Serialize(stream, now);

    //Envoie du buffer au client
    client.Client.Send(buffer);
    client.Close();

    OperationCompleted.Set();
}

Le client, lui, récupère le buffer et le désérialise en DateTime, de la même façon.

internal Program()
{
    byte[] buffer = new byte[100];

    TcpClient client = new TcpClient();
    client.Connect("localhost", 5000);
    client.Client.Receive(buffer);
    MemoryStream stream = new MemoryStream(buffer);
    BinaryFormatter bf = new BinaryFormatter();
    DateTime received = (DateTime)bf.Deserialize(stream);

    Console.WriteLine("Received : {0}", received.ToLongTimeString());
    Console.Read();
}

image

1.2 Rappel

Nous avons :

  • créer un server avec TcpLisener et TcpClient
  • créer un client avec TcpClient
  • Sérialiser et désérialiser avec BinaryFormatter

Nous n’avons pas envoyé de donnée du client au server, mais le procédé est le même. Notez que vous pouvez envoyer et recevoir des données simultanément sans risque de collision.

Nous avons utiliser les fonction Send et Receive de façon synchrone, leur version asynchrone existe aussi et est beaucoup plus intéressant dans un scénario réel.

2. Comprendre le policy-file-request de Silverlight

Aie :s

La sécurité, le truc par excellence qui ne marche jamais du premier coup, surtout en Silverlight avec les policy-file.

Commençons par le début, pour changer.

Pour qu’un service WCF fonctionne avec des clients Silverlight, il doit posséder un fichier clientaccesspolicy.xml. Ce fichier permet de spécifier à partir de quel nom de domaine les applications Silverlight peuvent accéder a ce service.

Si la plateforme Silverlight ne trouve pas ce fichier, elle va alors chercher son alter-ego Flash : crossdomain.xml.

Pour vous évitez une recherche, voici le contenu d’un clientaccesspolicy.xml spécifiant la sécurité minimum : tout le monde peux utiliser ce service.

using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;

namespace sl2Tcp
{
    public class PolicyFileServer
    {
        TcpListener listener;
        TcpClient client;

        AutoResetEvent OperationCompleted;

        //Buffer servant à sauvegarder le contenu du fichier
        //clientaccesspolicy.xml
        byte[] PolicyByteArray;

        //Requête valide
        const string PolicyRequestString = "<policy-file-request/>";

        /// <summary>
        /// Crée une instance de PolicyFileServer dont
        /// le fichier policy est situé sous le chemin
        /// FilePath
        /// </summary>
        /// <param name="FilePath"></param>
        public PolicyFileServer(string FilePath)
        {
            listener = new TcpListener(IPAddress.Any, 943);
            OperationCompleted = new AutoResetEvent(false);
            InitializeData(FilePath);
        }

        /// <summary>
        /// Charge le fichier Policy dans PolicyByteArray
        /// </summary>
        /// <param name="FilePath"></param>
        private void InitializeData(string FilePath)
        {
            if (!File.Exists(FilePath))
                throw new IOException("File not found : " + FilePath);

            PolicyByteArray = File.ReadAllBytes(FilePath);
        }

        /// <summary>
        /// Démarre le thread principal du server
        /// </summary>
        public void StartAsync()
        {
            Thread T = new Thread(Start);
            T.Start();
        }
        /// <summary>
        /// Thread principal du server, ecoute les
        /// requete de connexion sur le port 943
        /// </summary>
        internal void Start()
        {
            listener.Start();

            while (true)
            {
                listener.BeginAcceptSocket(OnClientConnected, null);
                OperationCompleted.WaitOne();
            }
        }

        /// <summary>
        /// Lorsqu'une connexion est établie sur le port 943 :
        ///      Receptionne une requete et la compare à PolicyRequestString
        ///      Si similaire, envoie PolicyByteArray
        /// </summary>
        /// <param name="param"></param>
        void OnClientConnected(IAsyncResult param)
        {
            client = listener.EndAcceptTcpClient(param);

            byte[] buffer = new byte[PolicyRequestString.Length];

            client.Client.Receive(buffer);
            string request = Encoding.UTF8.GetString(buffer, 0, buffer.Length);

            if (StringComparer.InvariantCultureIgnoreCase.Compare(request,
                                         PolicyRequestString) != 0)
            {
                client.Client.Close();
                OperationCompleted.Set();
                return;
            }

            //Envoie du buffer policy au client
            client.Client.Send(PolicyByteArray);
            client.Close();

            OperationCompleted.Set();
        }
    }
}

Ce nouvel objet définit, ajouter la gestion de la sécurité Silverlight 2 à un server devient un jeu d’enfant, remplaçons le constructeur de notre serveur par :

internal Program()
{
    PolicyFileServer policyFileServer =
        new PolicyFileServer(@"..\..\..\PolicyFileHolder\clientaccesspolicy.xml");
    policyFileServer.StartAsync();

    OperationCompleted = new AutoResetEvent(false);

    listener = new TcpListener(IPAddress.Any, 4502);
    listener.Start();
    listener.BeginAcceptSocket(OnClientConnected, null);

    OperationCompleted.WaitOne();
}

Avant de nous lancer dans l’écriture d’un client en Silverlight 2, ajoutons à notre client console un TcpClient supplémentaire se connectant au PolicyFileServer. Nous pourrons ainsi vérifier son bon fonctionnement.

Le code Program du client devient :

internal Program()
        {
            byte[] buffer = new byte[400];

            TcpClient sl2FakeFrameworrkClient = new TcpClient();
            sl2FakeFrameworrkClient.Connect("localhost", 943);
            byte[] PolicyRequestByteArray =
                              Encoding.UTF8.GetBytes(PolicyRequestString);
            sl2FakeFrameworrkClient.Client.Send(PolicyRequestByteArray);
            sl2FakeFrameworrkClient.Client.Receive(buffer);
            string PolicyFileReceived = Encoding.UTF8.GetString(buffer);
            Console.WriteLine("Received PolicyFile : \n{0}\n\n",
                                                                          PolicyFileReceived);

            //Ici le framework va tester si l'application Silverlight
            //a le droit d'acceder au service Tcp, dans le cas echeant
            //la connexion sera coupée

            TcpClient client = new TcpClient();
            client.Connect("localhost", 4502);
            client.Client.Receive(buffer);

            MemoryStream stream = new MemoryStream(buffer);
            BinaryFormatter bf = new BinaryFormatter();
            DateTime received = (DateTime)bf.Deserialize(stream);

            Console.WriteLine("Received : {0}", received.ToLongTimeString());
            Console.Read();
        }

image

3. Créer un client TCP Silverlight 2

La plateforme Silverlight 2 n’étant qu’un sous-ensemble de la plateforme .net, nous nous y retrouvons toujours le namespace System.Net.Sockets mais sans notre habituel TcpClient.

Nous allons devoir retirer une couche d’abstraction à notre programmation et utiliser le Socket de ce même namespace

Qui plus est, en Silverlight 2, toute communication doit être asynchrone.

3.1 Créer un client TCP Silverlight qui lit un nombre primitif

L’objet Socket ne garantit pas que sa fonction ReceiveAsync lise bel et bien le nombre de bytes qu’on lui a demande en paramètre.

C’est pourquoi le programmeur se doit de vérifier que toutes les données dont il a besoin ont bien été lue par le Socket avant d’essayer de les exploiter.

Pour ce faire, les arguments de l’évènement OnCompleted du Socket contiennent une propriété : BytesTransferred.

BytesTransferred représente le nombre de bytes effectivement lus par ReceiveAsync. Si ce nombre de bytes est insuffisant pour le programmeur, il faut alors relancer la fonction ReceiveAsync.

Une deuxième subtilité est à prendre en compte avec la fonction ReceiveAsync : cette fonction retourne un booléen.

Lorsque ce booléen est vrai, le framework à décider d’exécuter ReceiveAsync de manière asynchrone et un appel à l’évènement Socket.OnCompleted sera lancer.

Par contre, lorsque ce booléen est faux, la plateforme à décider d’éxécuter ReceiveAsync de manière synchrone et l’évènement Socket.OnCompleted ne sera pas lancé, il nous faudra alors nous en charger nous même.

Voici le code d’une class gérant une connexion Tcp cliente Silverlight 2 :

using System;
using System.Net;
using System.Net.Sockets;

namespace sl2NumberClient
{
    /// <summary>
    /// Delégé servant pour l'event OnDataReceived de sl2Client
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="ReceivedData"></param>
    public delegate void sl2ClientEventHandler(object sender, int ReceivedData);

    public class sl2Client
    {
        /// <summary>
        /// Lorqu'une donnée est recue par un client, il exécute
        /// ce délégué pour propager cette données à ses abonnés
        /// </summary>
        public sl2ClientEventHandler OnDataReceived;

        Socket socket;
        byte[] buffer = new byte[sizeof(int)];

        //Nombre de bytes commulé lu par les appels
        //successif de Socket.ReceiveAsyn(...)
        int bytesRead = 0;

        public void Start()
        {
            //Création du socket client Tcp
            socket =
                new Socket(AddressFamily.InterNetwork,
                           SocketType.Stream,
                           ProtocolType.Tcp);

            //Création du EndPoint
            //Le endpoint est l'adresse ip:port du serveur
            //utilisez 'Application.Current.Host.Source.DnsSafeHost'
            //à la place de localhost en mode production
            DnsEndPoint ep =
                new DnsEndPoint(
                    "localhost",
                    4502);

            //Création des paramètre de connexion
            //Cet EventArgs sera utilisé tout au long de la
            //communication serveur-client
            SocketAsyncEventArgs ConnectionParam =
                new SocketAsyncEventArgs()
                {
                    RemoteEndPoint = ep
                };

            //Lorsque la connexion au serveur réusit ou
            //echoue, exécuter la fonction 'OnConnected'
            ConnectionParam.Completed += OnConnected;

            //Se connecter au server définit dans les params
            socket.ConnectAsync(ConnectionParam);
        }

        void OnConnected(object sender,
            SocketAsyncEventArgs ConnectionParam)
        {
            //L'enumeration SocketError représente l'état de la
            //connexion. SocketError.Success signifie que la
            //connexion à étée établie ET que les autorisations du
            //PolicyFile sont correctes
            if (ConnectionParam.SocketError == SocketError.Success)
            {
                ConnectionParam.Completed -= OnConnected;
                ConnectionParam.Completed += SocketOperationCompleted;

                //Commencer à lire des données sur le flux Tcp
                readMoreData(ConnectionParam);
            }
            else
                throw new Exception("Connection fault : " +
                                    ConnectionParam.SocketError);
        }

        void readMoreData(SocketAsyncEventArgs ConnectionParam)
        {
            ConnectionParam.SetBuffer(buffer,
                                      bytesRead,
                                      (buffer.Length - bytesRead));
            try
            {
                //Subitilité numéro 2
                if (socket.ReceiveAsync(ConnectionParam))
                    SocketOperationCompleted(socket, ConnectionParam);
            }
            catch (InvalidOperationException)
            {
                //Tentative de lancer un ReveiveAsync alors qu'un
                //autre est déjà en cours --> on s'en moque :D
            }
            catch (Exception ex)
            {
                throw new Exception("readMoreData faild", ex);
            }
        }

        void SocketOperationCompleted(object sender,
            SocketAsyncEventArgs ConnectionParam)
        {
            if (ConnectionParam.BytesTransferred > 0)
            {
                bytesRead += ConnectionParam.BytesTransferred;

                //subtilité numéro 1
                if (bytesRead >= buffer.Length)
                {
                    int ReceivedData = BitConverter.ToInt32(buffer, 0);
                    if (OnDataReceived != null)
                        OnDataReceived(this, ReceivedData);
                    socket.Close();
                }
                else
                    readMoreData(ConnectionParam);
            }
        }

    }
}

Je passe la définition de l’interface et de son code behind qui n’ont dans le cadre de cet article aucun intérêt. Si vous êtes toute foi demandeur à ce sujet, vous les trouverez tout deux dans les code sources fournit avec cet article.

image

3.2 Utiliser des DataContract et un DataContractSerializer

Dans le point 1.1 de cet article, nous avons envoyer l’heure par DateTime à travers une connexion Tcp entre deux client .Net 3.5.

Pour ce faire, nous avons utiliser un BinaryFormatter sérialisant l’objet DateTime possédant l’attribut ISerializable.

Les BinaryFormatter ne sont pas disponible en Silverlight 2.

Pour pouvoir sérialiser des objets sur le flux Tcp il faut utiliser un DataContractSerializer sérialisant des objet possédant l’attribut DataContract.

DateTime possède l’attribut DataContract, tout comme il possède ISerializable.

Cette modification de BinaryFormatter à DataContractSerializer est à faire coté client, mais aussi coté serveur.

Une dernière subtilité est à comprendre, le résultat d’une sérialisation par DataContractSerializer est un tableau de bytes de taille variable.

Du coté client, il est impossible de savoir à l’avance combien de bytes nous devront lire avant de pouvoir désérialiser.

Nous allons donc envoyer un nombre (UInt32) du serveur au client avant l’envoie du DateTime, Ce nombre spécifiera la taille prise par le DateTime sérialisé.

Nous allons donc devoir modifier la fonction OnClientConnected notre serveur du point 2.1 de cet article :

(Notez que dans ce cas, les fonctions de sérialisation ne sont plus Serialize et Deserialize mais WriteObject et ReadObject)

void OnClientConnected(IAsyncResult param)
{
    client = listener.EndAcceptTcpClient(param);

    //Recupération de l'heure
    DateTime now = DateTime.Now;

    //Encapsulation du buffer dans un stream
    //permetant ainsi une ecriture facile
    MemoryStream stream = new MemoryStream();

    //Création d'un DataContractSerializer et Sérialisation
    //de l'heure dans le stream --> donc dans le buffer
    DataContractSerializer dc =
        new DataContractSerializer(typeof(DateTime));
    dc.WriteObject(stream, now);

    //Récupération du buffer généré dans le MémoryStream
    byte[] buffer = stream.ToArray();

    //Evoie de la taille du buffer au client
    client.Client.Send(
        BitConverter.GetBytes(
            (Int32)buffer.Length
            )
        );

    //Envoie du buffer au client
    client.Client.Send(buffer);
    client.Close();

    OperationCompleted.Set();
}

Le client, doit alors :

  1. Créer un Socket Tcp
  2. Lire la taille du DateTime Sérialiser
  3. Lire le DateTime

using System;
using System.Net;
using System.Net.Sockets;
using System.Runtime.Serialization;
using System.IO;

namespace sl2DateTimeClient
{
    /// <summary>
    /// Delégé servant pour l'event OnDataReceived de sl2Client
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="ReceivedData"></param>
    public delegate void sl2ClientEventHandler(object sender, DateTime ReceivedData);

    public class sl2Client
    {
        /// <summary>
        /// Lorqu'une donnée est recue par un client, il exécute
        /// ce délégué pour propager cette données à ses abonnés
        /// </summary>
        public sl2ClientEventHandler OnDataReceived;

        Socket socket;
        byte[] buffer = new byte[sizeof(UInt32)];

        //Nombre de bytes commulé lu par les appels
        //successif de Socket.ReceiveAsyn(...)
        int bytesRead = 0;

        public void Start()
        {
            //Création du socket client Tcp
            socket =
                new Socket(AddressFamily.InterNetwork,
                           SocketType.Stream,
                           ProtocolType.Tcp);

            //Création du EndPoint
            //Le endpoint est l'adresse ip:port du serveur
            //utilisez 'Application.Current.Host.Source.DnsSafeHost'
            //à la place de localhost en mode production
            DnsEndPoint ep =
                new DnsEndPoint(
                    "localhost",
                    4502);

            //Création des paramètre de connexion
            //Cet EventArgs sera utilisé tout au long de la
            //communication serveur-client
            SocketAsyncEventArgs ConnectionParam =
                new SocketAsyncEventArgs()
                {
                    RemoteEndPoint = ep
                };

            //Lorsque la connexion au serveur réusit ou
            //echoue, exécuter la fonction 'OnConnected'
            ConnectionParam.Completed += OnConnected;

            //Se connecter au server définit dans les params
            socket.ConnectAsync(ConnectionParam);
        }

        void OnConnected(object sender,
            SocketAsyncEventArgs ConnectionParam)
        {
            //L'enumeration SocketError représente l'état de la
            //connexion. SocketError.Success signifie que la
            //connexion à étée établie ET que les autorisations du
            //PolicyFile sont correctes
            if (ConnectionParam.SocketError == SocketError.Success)
            {
                ConnectionParam.Completed -= OnConnected;
                ConnectionParam.Completed += SocketOperationCompleted;

                //Commencer à lire des données sur le flux Tcp
                readMoreData(ConnectionParam);
            }
            else
                throw new Exception("Connection fault : " +
                                    ConnectionParam.SocketError);
        }

        void readMoreData(SocketAsyncEventArgs ConnectionParam)
        {
            ConnectionParam.SetBuffer(buffer,
                                      bytesRead,
                                      (buffer.Length - bytesRead));
            try
            {
                //Subitilité numéro 2
                if (socket.ReceiveAsync(ConnectionParam))
                    SocketOperationCompleted(socket, ConnectionParam);
            }
            catch (InvalidOperationException)
            {
                //Tentative de lancer un ReveiveAsync alors qu'un
                //autre est déjà en cours --> on s'en moque :D
            }
            catch (Exception ex)
            {
                throw new Exception("readMoreData faild", ex);
            }
        }

        void SocketOperationCompleted(object sender,
            SocketAsyncEventArgs ConnectionParam)
        {
            if (ConnectionParam.BytesTransferred > 0)
            {
                bytesRead += ConnectionParam.BytesTransferred;

                //subtilité numéro 1
                if (bytesRead >= buffer.Length)
                {
                    if(buffer.Length == sizeof(UInt32))
                    {
                        //Lire la taille de DataTime Sérialiser
                        //qui va suivre dans le flux
                        int bufferSize = BitConverter.ToInt32(buffer,0);

                        //Réinitialiser le buffer
                        buffer = new byte[bufferSize];
                        bytesRead = 0;

                        //Recommencer la lecture du flux
                        readMoreData(ConnectionParam);
                    }
                    else
                    {
                        DataContractSerializer dc =
                            new DataContractSerializer(typeof(DateTime));

                        //Désérialiser le DateTime
                        DateTime ReceivedData = (DateTime)
                            dc.ReadObject(new MemoryStream(buffer));

                        if (OnDataReceived != null)
                            OnDataReceived(this, ReceivedData);
                        socket.Close();
                    }
                }
                else
                readMoreData(ConnectionParam);
            }
        }
    }
}

image

3.3 Comprendre les KnowTypes

Le DataContractSerializer du point précédent ne sérialise que des DateTime.

DataContractSerializer dc = 
        new DataContractSerializer(typeof(DateTime));

Si vous essayer de lui demander de sérialiser un autre DataContract, une exception sera levée.

En effet, le DataContractSerializer ne peut convertir en bytes que ce qu’il connait. Pour lui apprendre de nouveaux DataContract, il faut lui en passer les types en paramètre lors de sa construction.

DataContractSerializer bf = 
    new DataContractSerializer(typeof(object),
                                             une List<Type>);

Cette List<Type> est appelée la liste des KnowTypes.

4. Rendre le tout générique et réutilisable

Dans mon prochain article nous verrons comment créer une librairie réutilisable pour gérer toutes ces manipulations répétitives.

Nous verrons aussi comment permettre à une assembly d’être partagée (en référence) entre .Net 3.5 et Silverlight. Ceci est très utile pour la création de notre propre class ayant l’attribut DataContract.

Nous apprivoiserons correctement les possibilité duplex de ce type de connexion Tcp.

Et enfin, nous utiliserons le tout pour créer un petit chat en Silverlight.

En espérant que cette lecture vous aura été utile.

Merci.
Simon Boigelot.

 

Source :
Tamir Khason – Just Code