webForumDet fria alternativet

Remoting/Tcp socket eller vad?

.NETur .NET

5 svar · 659 visningar · startad av cok

cokMedlem sedan dec. 2005664 inlägg
#1

Hallå,

Jag har följande uppbyggnad av applikation:

Flera klienter ute i nätverk som körs. I dagsläget kan man skicka kommando till dem med hjälp utav socket:s.

Detta görs genom ett com-object (skapat i .NET 3.5), som körs i VB6. När jag fårn min dll skickar kommando så går det helt utmärkt. Mitt problem är att jag vill kunna ta emot resultat från kommandona på ett snyggt sätt. Vet inte om jag kan göra det på ett snyggt sätt med hjälp utav sockets. Skulle kanske kunna skicka med ett guid eller liknande i kommandot och returnera detta som svar och ha en lyssnare i klienten. Detta gör dock att jag inte kan få resultatet direkt i samma metod (rätta mig om jag har fel).

Testade med remoting, inga prblem att skicka kommando till ett object på klientsidan ("serversidan"), men hur får jag det vidare "upp" till klienten.

Tips, ideér?

GladhMedlem sedan maj 20012 812 inlägg
#2

Om du kan så bör du titta på WCF. Vilket underlättar din kommunikation då du slipper själv hålla reda på sockets och dylikt.

Du kan få svar i samma metod som du skickar din kommando i, men det kräver ju så fall att den tråden står kvar och väntar på svaret, och om det tar långtid för svaret att komma så blir ju den tråden låst, vilket inte är så smart.

Ett bättre alternativ är som du förslagit en helt asynchron kommunikations system, men en kommunikationsklient och kommunikationsserver på både din server och på alla dina klienter. Det betyder att din server först är en kommunikationklient och skickar ett kommand till en av dina klienter (som då är kommunikationserver) kommunikationservern får meddelandet och för att få det vidare upp i klienten så reser den ett event som din klient måste lyssna på. Klienten bearbetar kommandot och blir sedan en kommunikationsklient som skickar svaret till din server (som nu är kommunikationsserver) den reser även den ett event om att ett nytt meddelande har kommit som din server lyssnar på.

För att klara av en massa olika kommand som kommer in till din server/klient så skall kommunikationsservern köras i sin egen tråd som hela tiden väntar på ett meddelande skall komma in till den, när detta meddelande kommer så skall den bara lägga ner den på en internkö, och sedan är den tråden redo för att ta emot nästa meddelande. Du har sedan en annan tråd som hela tiden ligger och väntar på att något skall komma ner i din kö, och så fort något placeras i kön, så kommer denna tråden plocka meddelandet, kontrollera vad som skall göras, och start en ny tråd för att utföra detta kommandot.

Med en sådan lösning så kommer din kommunikationsserver klara av att hantera riktigt många samtidiga användare, risken är om din kommando som skall utföras tar långtid så kommer det ätas en del trådar, vilket påverkar prestandan negativt, men samtidigt så kommer inte din server "låsas" för att du har kanske en operation som tar långtid, medans alla andra går snabbt.

Om du måste para ihop ditt sendmeddelande med ditt receivemeddlande så gör du med en GUID precis som du förslagit.

Jag tror jag har någon kod liggande för hantering av kommunikationsservern om du är nyfiken på hur jag menar :)

- M

cokMedlem sedan dec. 2005664 inlägg
#3

Tack för ett utförligt svar.

Min server, är olika klienter ute i nätverket och data som skickas är inte så stor. Det är även väldigt få kommandon som kommer skickas ut till alla klienter (man kan nog mer kalla det klient-klient, än klient-server).

Jag tittar gärna på din kod. Vi har dock redan en hantering av kommandon som inte behöver svar, men som sagt, du kanske har något som man kan bryta ut. :)

GladhMedlem sedan maj 20012 812 inlägg
#4

Okej jag har rotat lite bland koden och hittat följande, hoppas jag kommer ihåg hur det fungerade :)

Jag använder mig av WCF och NetTcpBinding (ungefär som remoting) för att få det här att fungera.

Först börjar vi med Intefacet för min WCFServer:

    [ServiceContract(Name="controllerservices", Namespace="http://services.ie.gladh.org/")]
    public interface IControllerServices
    {
        #region -- Events
        event ProcessCommandEventHandler ProcessCommandEvent;
        event CommandProcessedEventHandler CommandProcessedEvent;
        event ErrorOccuredEventHandler ErrorOccuredEvent;
        #endregion

        #region -- Methods
        [OperationContract(IsOneWay=true)]
        void ProcessCommand(ProcessCommandMessage processCommandMessage);
        [OperationContract(IsOneWay = true)]
        void CommandProcessed(CommandProcessedMessage commandProcessedMessage);
        #endregion
    }

De viktiga här är de 2 metoderna som man kan anropa från klienterna. ProcessCommand, och CommandProcessed. Jag skall säga att min Server(Controller) är en middleware server, så jag har ett(flera) Frontends som kan skicka commands till servern som sedan skickar det vidare till en(flera) Executers(kallar de för klienter) som finns på en(flera) olika maskiner i nätverket. Så ProcessCommand skickar meddelande "neråt", och CommandProcessed skickar svar tillbaka "uppåt".

Sedan så har jag en BaseServices (eftersom jag har 3 olika typer av server, en för frontendens, en för min controllers och en för mina executers. Och den ser ut så här.

    /// <summary>
    /// The base class that all our services should inherit from.
    /// </summary>
    /// <remarks>
    /// <para>
    /// Contains an queue that all incomming message are stored in, another thread the reads all 
    /// message in queue and starts a new thread raiseing the event that a message are read to be processed.
    /// </para>
    /// </remarks>
    public abstract class baseServices
    {
        #region -- Declaration
        protected Log _Log;

        private object _IncommingMessageQueueSyncLock = new object();
        private Queue _IncommingMessageQueue;
        private AutoResetEvent _IncommingMessageQueueEvent;
        private Thread _IncommingMessageQueueThread;

        protected event ErrorOccuredEventHandler _ErrorOccuredEvent;
        #endregion

        #region -- Private Methods
        private void Initialize(Type typeOfInheirter)
        {
            _Log = LogManager.Instance.GetLog(typeOfInheirter);

            _IncommingMessageQueue = new Queue();
            _IncommingMessageQueueEvent = new AutoResetEvent(false);

            _IncommingMessageQueueThread = new Thread(new ThreadStart(ReadIncommingMessageFromQueue));
            _IncommingMessageQueueThread.IsBackground = true;
            _IncommingMessageQueueThread.Name = string.Format("ReadIncommingMessageFromQueue");
            _IncommingMessageQueueThread.Start();            
        }
        private void ReadIncommingMessageFromQueue() 
        {
            _Log.TraceEnter();

            //-- Declaration
            object message = null;

            try
            {
                while (true)
                {
                    try
                    {
                        //-- Wait the thread until it's signal that a messages has been added to queue
                        _IncommingMessageQueueEvent.WaitOne();

                        while (_IncommingMessageQueue.Count > 0)
                        {
                            //-- Read the message from the queue
                            lock (_IncommingMessageQueueSyncLock)
                                message = _IncommingMessageQueue.Dequeue();

                            ThreadPool.QueueUserWorkItem(new WaitCallback(IncomingMessageDequeueFromQueue), message);
                        }
                    }
                    catch (Exception exception)
                    {
                        _Log.Error<object>("An error occured while reading messages from queue", exception, message);
                        //-- Raise this error so it's shown in frontend
                        OnErrorOccuredEvent(new ErrorOccuredEventArgs(string.Format("An error occured while reading messages from queue:: {0}", exception.Message)));
                    }
                }
            }
            finally
            {
                _Log.TraceLeave();
            }
        }
        protected void OnErrorOccuredEvent(ErrorOccuredEventArgs errorOccuredEventArgs)
        {
            if (_ErrorOccuredEvent != null)
                _ErrorOccuredEvent(errorOccuredEventArgs);
        }
        #endregion

        #region -- Virtual methods
        protected virtual void AddIncomingMessageToQueue(object message)
        {
            _Log.TraceEnter();
            try
            {
                _Log.Debug<object>("A messages is about to be added to IncommingMessageQueue", message);

                lock (_IncommingMessageQueueSyncLock)
                    _IncommingMessageQueue.Enqueue(message);

                _IncommingMessageQueueEvent.Set();
            }
            catch (Exception exception)
            {
                _Log.Error<object>("Error while adding incommingmessage to queue", exception, message);
                //-- Raise this error so it's shown in frontend
                OnErrorOccuredEvent(new ErrorOccuredEventArgs(string.Format("Error while adding incommingmessage to queue: {0}", exception.Message)));
            }
            finally
            {
                _Log.TraceLeave();
            }
        }
        #endregion

        #region -- Pure virtual method
        protected abstract void IncomingMessageDequeueFromQueue(object message);
        #endregion

        #region -- Constructor
        public baseServices(Type typeOfInheirter)
        {
            Initialize(typeOfInheirter);
        }
        #endregion
    }

Det finns 3 metoder som är intressanta här: Dels AddIncomingMessageToQueue som lägger ner ett meddelande på vår kö och metoden ReadIncommingMessageFromQueue som läser meddelande från kön så fort något meddelande har lagts till kön. Som sedan startar en ny tråd för att hantera meddelande genom att anropa metoden IncomingMessageDequeueFromQueue, denna method är virtual eftersom vår olika server skall göra olika saker med meddelanden som kommer in.

Fortsättning följer, se nedan

- M

GladhMedlem sedan maj 20012 812 inlägg
#5

Sedan har vi själva ControllerServices alltså den klass som ärver från vår baseServices.

    /// <summary>
    /// Controller services
    /// </summary>
    /// <remarks>
    /// <para>
    /// The controller services that handles incomming messages for the controller
    /// </para>
    /// </remarks>
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, Name = "controllerservices", Namespace = "http://services.ie.gladh.org/")]
    public class ControllerServices : baseServices, IControllerServices
    {
        #region -- Declaration
        private event CommandProcessedEventHandler _CommandProcessedEvent;
        private event ProcessCommandEventHandler _ProcessCommandEvent;
        #endregion

        #region -- Override methods
        protected override void IncomingMessageDequeueFromQueue(object message)
        {
            _Log.TraceEnter();
            try
            {
                if(message.GetType() == typeof(CommandProcessedMessage))
                    if (_CommandProcessedEvent != null)
                        _CommandProcessedEvent(new CommandProcessedEventArgs(message as CommandProcessedMessage));

                if (message.GetType() == typeof(ProcessCommandMessage))
                    if (_ProcessCommandEvent != null)
                        _ProcessCommandEvent(new ProcessCommandEventArgs(message as ProcessCommandMessage));
            }
            catch (Exception exception)
            {
                _Log.Error<object>("An error occured trying to raise event that a messages has been read from queue", exception, message);
                OnErrorOccuredEvent(new ErrorOccuredEventArgs(string.Format("An error occured trying to raise event that a messages has been read from queue: {0}", exception.Message)));
            }
            finally
            {
                _Log.TraceLeave();
            }
        }
        #endregion

        #region -- Constructor
        public ControllerServices() : base(typeof(ControllerServices)) { }
        #endregion

        #region IControllerServices Members
        event ProcessCommandEventHandler IControllerServices.ProcessCommandEvent
        {
            add { _ProcessCommandEvent += value; }
            remove { _ProcessCommandEvent -= value; }
        }
        event CommandProcessedEventHandler IControllerServices.CommandProcessedEvent
        {
            add { _CommandProcessedEvent += value; }
            remove { _CommandProcessedEvent -= value; }
        }
        event ErrorOccuredEventHandler IControllerServices.ErrorOccuredEvent
        {
            add { base._ErrorOccuredEvent += value; }
            remove { base._ErrorOccuredEvent -= value; }
        }
        void IControllerServices.ProcessCommand(ProcessCommandMessage processCommandMessage)
        {
            base.AddIncomingMessageToQueue(processCommandMessage);
        }
        void IControllerServices.CommandProcessed(CommandProcessedMessage commandProcessedMessage)
        {
            base.AddIncomingMessageToQueue(commandProcessedMessage);
        }
        #endregion
    }

Här ser vi de två WCF interfacen som våra klienter använder sig av när de vill anropa till servern. IControllerServices.ProcessCommand och IControllerServices.CommandProcessed så när klienten anropa servern på någon av dessa metoder, så kommer vi ta meddelandet och lägga ner det i kön, och sedan får en annan tråd ta hand om själva uppläsningen och ytterligare en annan tråd tar hand om exekveringen av meddelandet.

Metoden IncomingMessageDequeueFromQueue är den metod som blir anropad när ett meddelande har blivit läst från kön och skall exekveras vidare av vår server, så vår controllerServices reser ett event till Controllern att ett meddelande har kommit in och Controllern tar nu hand om det med hjälp av följande metoder:

        private void ControllerServices_ProcessCommandEvent(ProcessCommandEventArgs processCommandEventArgs)
        {
            ProcessCommandMessage processCommandMessage = processCommandEventArgs.ProcessCommandMessage;

            this.GetType().InvokeMember(processCommandMessage.ProcessCommand.ToString()
                , System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.InvokeMethod
                , null, this, new object[] { processCommandMessage });
        }
        private void ControllerServices_CommandProcessedEvent(CommandProcessedEventArgs commandProcessedEventArgs)
        {
            CommandProcessedMessage commandProcessedMessage = commandProcessedEventArgs.CommandProcessedMessage;

            this.GetType().InvokeMember(commandProcessedMessage.CommandProcessed.ToString()
                , System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.InvokeMethod
                , null, this, new object[] { commandProcessedMessage });
        }

Anledningen till att jag använder mig av InvokeMember() som är lite långsammar än att köra en stor switch-sats där man kontrollerar vilken commandProcessed det är och sedan anropar metoden, är att jag vill ha mindre kod att underhålla, om jag lägger till ett nytt CommandProcessed så slipper jag underhålla koden i dessa metoder, och skall "bara" kommaihåg att skriva den kod som skall processas för den nya CommandProcessed typen.

Så säg att jag skickar ett meddelande om att att en av mina Executer har startat, då vill jag att min Controller skall veta det. Executern kallar på controller.CommandProcessed(ExecuterStarted). ControllerServices tar emot meddelandet och lägger ner det på kö, en anna tråd läser meddelandet från kön, och startar en ny tråd som reser ett event om att ett commandProcessed meddelande har inkommit. Metoden ControllerServices_CommandProcessedEvent ser att det är av typen ExecuterStarted och anropar därför metoden ExecuterStarted som finns i min Controller-klass, som utför det som skall utföras när en Executer har startat.

Och så här ser metoden ut (i controllern) som körs när en Executer har startas

        private void ExecuterStarted(CommandProcessedMessage commandProcessedMessage)
        {
            _Log.TraceEnter();
            try
            {
                ExecuterDataEntity executerData = commandProcessedMessage.Body.ExecuterData;
                _Log.Information<CommandProcessedMessage>(
			string.Format("Executer Id:{0} Uri:{1} has started"
				, executerData.FriendlyName, executerData.EndpointAdressURI), commandProcessedMessage);

                //-- Change the status for this executer to started
                ExecuterDataEntityWrapper executerDataEntityWrapper = FindExecuterInList(executerData.Identifier);
                lock (executerDataEntityWrapper.SyncLock)
                    executerDataEntityWrapper.ExecuterData.Status = ExecuterStatusEnum.Started;

                //-- Add this executer to the activeexecuter list so it can host and run adapters.
                lock (_ActiveExecuterList.SyncLock)
                    _ActiveExecuterList.Add(executerDataEntityWrapper);

                //-- Send CommandProcessed to frontend.
                SendCommandProcessedMessage(commandProcessedMessage);
            }
            finally
            {
                _Log.TraceLeave();
            }
        }

Som du märker i den sista metoden så har jag lite Lock()'s och det är ju viktigt för att jag skall accessa variabler som är delade mellan alla trådar som körs, och då vill jag vara säker på att 2 trådar inte kommer in samtidigt och skriver till variablen, det leder bara till problem....

Där har du en snabb genomgång av hur det är tänkt med köerna som skall hantera inkommande meddeland.

cokMedlem sedan dec. 2005664 inlägg
#6

Tackar så mycket för en liten "tutorial". Ska helt klart testa detta, men det får bli ett kvällsprojekt. Tittade en del på WCF precis innan det släptes, kommer tyvärr inte ihåg så mycket mer än att det fanns cola stöd för t ex lastbalancerade servers etc.

Återkommer säkerligen med frågor...

142 ms totalt · 3 externa anrop · v20260731065814-full.29ac60f6
0 ms — hämta forumlista (cache)
0 ms — hämta statistik (cache)
139 ms — hämta tråd, inlägg och bilagor (db)