Projection d'Appareil dans les Systèmes Distribués : Implémentation C# avec TCP

Implémentation Pratique

Cet article présente une implémentation concrète du modèle de projection d'appareil, permettant la gestion d'appareils distants via une connexion TCP. L'exemple se compose de deux applications : un contrôleur maître (LightMaster) et un simulateur d'appareil (LightDevice).

Architecture Générale

Le système repose sur deux exécutables principaux :

  • LightDevice.exe : Simule un appareil physique distant. Il établit une connexion TCP vers le contrôleur, expose un état de lampe modifiable localement ou via commande réseau, et transmet son état périodiquement (toutes les 300 ms).
  • LightMaster.exe : Représente l'application de contrôle central. Il écoute les connexions TCP des appareils, maintient une liste de leurs états projetés et permet d'envoyer des commandes de contrôle à un appareil sélectionné.

Conception des Composants Fondamentaux

Le cœur du système est construit autour d'abstractions génériques permettant d'isoler la logique métier des détails de communication.

Interfaces de Base pour l'Appareil

namespace DeviceProjectionSample
{
    public interface IAppareilEsquisse : IDeviceSketch
    {
        IPEndPoint PointAccesReseau { get; }
    }

    public interface IControleurAppareil : IDeviceControl
    {
        bool EtatLumiere { get; }
        void ActiverLumiere();
        void DesactiverLumiere();
    }
}

Ces interfaces définissent le contrat pour l'identifiant réseau (esquisse) et les opérations de contrôle, séparant ainsi les préoccupations.

Représentation Principale de l'Appareil

namespace DeviceProjectionSample
{
    public class AppareilLumiere : Device<IAppareilEsquisse, IControleurAppareil>
    {
        public AppareilLumiere(IAppareilEsquisse esquisse, IControleurAppareil controleur)
            : base(esquisse, controleur) { }

        public IPEndPoint PointAccesReseau
        {
            get { return this.EsquisseAppareil.PointAccesReseau; }
        }

        public bool EtatLumiere
        {
            get { return this.ControleurAppareil.EtatLumiere; }
        }

        protected override bool ImporterPropriete(IAppareilEsquisse esquisse)
        {
            return false; // Logique d'importation personnalisée si nécessaire
        }

        public void ActiverLumiere()
        {
            this.ControleurAppareil.ActiverLumiere();
        }

        public void DesactiverLumiere()
        {
            this.ControleurAppareil.DesactiverLumiere();
        }
    }
}

Cette classe concrète agrège les interfaces et expose une API unifiée pour l'appareil.

Gestion des Collections d'Appareils

namespace DeviceProjectionSample
{
    public class CollectionAppareilsLumiere : DeviceCollection<AppareilLumiere, IAppareilEsquisse, IControleurAppareil>
    {
        private readonly List<AppareilLumiere> _inventaireAppareils = new List<AppareilLumiere>();

        protected override AppareilLumiere ObtenirAppareil(IAppareilEsquisse esquisse)
        {
            if (esquisse == null) throw new ArgumentNullException(nameof(esquisse));

            return _inventaireAppareils.Find(a =>
                a.PointAccesReseau.ToString() == esquisse.PointAccesReseau.ToString());
        }

        protected override void AjouterAppareil(AppareilLumiere appareil)
        {
            if (appareil == null) throw new ArgumentNullException(nameof(appareil));
            _inventaireAppareils.Add(appareil);
        }

        protected override void SupprimerAppareil(AppareilLumiere appareil)
        {
            if (appareil == null) throw new ArgumentNullException(nameof(appareil));
            _inventaireAppareils.Remove(appareil);
        }

        public override IEnumerator<AppareilLumiere> GetEnumerator()
        {
            return _inventaireAppareils.GetEnumerator();
        }
    }
}

Couche de Communication Réseau

L'infrastructure réseau est encapsulée dans des classes dédiées pour une réutilisation et une testabilité accrues.

Client de Messagerie TCP

namespace DeviceProjectionSample.Concretion
{
    public class ClientMessagerie : IDisposable
    {
        private readonly TcpClient _clientTCP;
        private readonly NetworkStream _fluxReseau;
        private readonly Thread _threadLecture;
        private readonly SynchronizationContext _contexteSynchro;
        private bool _estLibere = false;

        public ClientMessagerie(SynchronizationContext contexte, TcpClient client)
        {
            _contexteSynchro = contexte ?? throw new ArgumentNullException(nameof(contexte));
            _clientTCP = client ?? throw new ArgumentNullException(nameof(client));
            _fluxReseau = client.GetStream();
            _threadLecture = new Thread(LireCommandes);
        }

        public void Demarrer()
        {
            _threadLecture.Start();
        }

        public void EnvoyerCommande(byte codeCommande)
        {
            if (_clientTCP.Connected)
                _fluxReseau.WriteByte(codeCommande);
        }

        private void LireCommandes(object etat)
        {
            try
            {
                int code;
                do
                {
                    code = _fluxReseau.ReadByte();
                    if (code >= 0)
                        DeclencherCommandeRecue((byte)code);
                } while (code >= 0);
            }
            finally
            {
                DeclencherDeconnecte(this);
            }
        }

        public IPEndPoint PointAccesDistant
        {
            get { return (IPEndPoint)_clientTCP.Client.RemoteEndPoint; }
        }

        public event Action<byte> CommandeRecue;
        private void DeclencherCommandeRecue(byte code)
        {
            var gestionnaire = CommandeRecue;
            if (gestionnaire != null)
                _contexteSynchro.Post(s => gestionnaire(code), null);
        }

        public event Action<ClientMessagerie> Deconnecte;
        private void DeclencherDeconnecte(ClientMessagerie client)
        {
            var gestionnaire = Deconnecte;
            if (gestionnaire != null)
                _contexteSynchro.Post(s => gestionnaire(client), null);
        }

        public void Dispose() { /* Libération des ressources */ }
    }
}

Écouteur de Messagerie TCP

namespace DeviceProjectionSample.Concretion
{
    public class EcouteurMessagerie : IDisposable
    {
        private readonly TcpListener _ecouteurTCP;
        private readonly Thread _threadEcoute;
        private readonly List<ClientMessagerie> _clientsConnectes = new List<ClientMessagerie>();
        private readonly object _verrouListe = new object();

        public EcouteurMessagerie(SynchronizationContext contexte, IPEndPoint pointLocal)
        {
            _ecouteurTCP = new TcpListener(pointLocal);
            _threadEcoute = new Thread(EcouterConnexions);
        }

        private void EcouterConnexions(object etat)
        {
            try
            {
                while (true)
                {
                    TcpClient client = _ecouteurTCP.AcceptTcpClient();
                    var clientMsg = new ClientMessagerie(SynchronizationContext.Current, client);
                    lock (_verrouListe)
                    {
                        clientMsg.Deconnecte += ClientDeconnecte;
                        _clientsConnectes.Add(clientMsg);
                    }
                    DeclencherClientArrive(clientMsg);
                }
            }
            catch { /* Gestion d'arrêt */ }
        }

        public ClientMessagerie TrouverClient(IPEndPoint point)
        {
            lock (_verrouListe)
            {
                return _clientsConnectes.Find(c => c.PointAccesDistant.ToString() == point.ToString());
            }
        }

        public event Action<ClientMessagerie> ClientArrive;
        private void DeclencherClientArrive(ClientMessagerie client)
        {
            var gestionnaire = ClientArrive;
            gestionnaire?.Invoke(client);
        }

        private void ClientDeconnecte(ClientMessagerie client)
        {
            lock (_verrouListe) { _clientsConnectes.Remove(client); }
            DeclencherClientParti(client);
        }

        public event Action<ClientMessagerie> ClientParti;
        private void DeclencherClientParti(ClientMessagerie client)
        {
            var gestionnaire = ClientParti;
            gestionnaire?.Invoke(client);
        }

        public void Demarrer() { _ecouteurTCP.Start(); _threadEcoute.Start(); }
        public void Dispose() { /* Libération des ressources */ }
    }
}

Implémentation des Composants Spécifiques

Contrôleur d'Appareil Lumineux

namespace DeviceProjectionSample.Concretion
{
    public class ControleurLumiere : IControleurAppareil, INotifyPropertyChanged
    {
        private readonly ClientMessagerie _client;
        private bool _lampeAllumee;

        public ControleurLumiere(ClientMessagerie client)
        {
            _client = client ?? throw new ArgumentNullException(nameof(client));
            _client.CommandeRecue += TraiterCommandeReseau;
            _client.Demarrer();
        }

        public bool EtatLumiere
        {
            get => _lampeAllumee;
            set { _lampeAllumee = value; NotifierChangement(nameof(EtatLumiere)); }
        }

        public void ActiverLumiere() => _client.EnvoyerCommande(0x01);
        public void DesactiverLumiere() => _client.EnvoyerCommande(0x00);

        private void TraiterCommandeReseau(byte code)
        {
            switch (code)
            {
                case 0x00: EtatLumiere = false; break;
                case 0x01: EtatLumiere = true; break;
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
        private void NotifierChangement(string propriete)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propriete));
        }
    }
}

Gestionnaire Principle des Appareils

namespace DeviceProjectionSample
{
    public class GestionnaireAppareilsLumiere : DeviceManager<AppareilLumiere, IAppareilEsquisse, IControleurAppareil>
    {
        public GestionnaireAppareilsLumiere(
            IDeviceFactory<AppareilLumiere, IAppareilEsquisse, IControleurAppareil> fabrique,
            IDeviceSketchExplorer<IAppareilEsquisse> explorateur)
            : base(new CollectionAppareilsLumiere(), fabrique, explorateur)
        {
            this.CollectionAppareils = new CollectionAppareilsLumiere();
        }

        public CollectionAppareilsLumiere CollectionAppareils { get; }
    }
}

Intégration dans une Interface Utilisateur Windows Forms

namespace LightMaster
{
    public partial class FenetrePrincipale : Form
    {
        private readonly GestionnaireAppareilsLumiere _gestionnaire;
        private readonly BindingList<AppareilLumiere> _listeLiaison = new BindingList<AppareilLumiere>();

        public FenetrePrincipale()
        {
            InitializeComponent();

            var pointLocal = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 1234);
            var ecouteur = new EcouteurMessagerie(SynchronizationContext.Current, pointLocal);

            var fabrique = new FabriqueAppareilLumiere(ecouteur);
            var explorateur = new ExplorateurEsquissesLumiere(ecouteur);

            _gestionnaire = new GestionnaireAppareilsLumiere(fabrique, explorateur);

            _gestionnaire.AppareilArrive += (s, e) => _listeLiaison.Add(e.Appareil);
            _gestionnaire.AppareilParti += (s, e) => _listeLiaison.Remove(e.Appareil);

            grilleAppareils.DataSource = _listeLiaison;
        }

        private void BoutonAllumer_Click(object sender, EventArgs e)
        {
            var appareilSelectionne = grilleAppareils.CurrentRow?.DataBoundItem as AppareilLumiere;
            appareilSelectionne?.ActiverLumiere();
        }

        private void BoutonEteindre_Click(object sender, EventArgs e)
        {
            var appareilSelectionne = grilleAppareils.CurrentRow?.DataBoundItem as AppareilLumiere;
            appareilSelectionne?.DesactiverLumiere();
        }

        protected override void OnLoad(EventArgs e)
        {
            base.OnLoad(e);
            _gestionnaire.Demarrer();
        }

        protected override void OnFormClosing(FormClosingEventArgs e)
        {
            base.OnFormClosing(e);
            _gestionnaire.Dispose();
        }
    }
}

Cette impléstration illustre comment le modèle de projection d'appareil fournit une abstraction efficace pour gérer des périphériques réseau, séparant clairement les préoccupations métier, de communication et d'interface utilisateur.

Étiquettes: C# TCP Architecture Logicielle Modèle de Conception Systèmes distribués

Publié le 24 septembre à 14h02