Game Server auf MS Orleans - Teil 1: Was sind Schauspieler?



Hallo Habr! Und so beschloss ich am vierzehnten Tag des Speers, einen einfachen Spieleserver für einfaches Online-Schießen zu entwickeln. Damit sich ein Thema des verteilten Rechnens berührt. In diesem Einführungsartikel des Zyklus möchte ich erklären, was Schauspieler sind (in Orleans werden sie Körner genannt) und welches Prinzip sie haben. Dafür bin ich immer noch ein Beispiel für eine einfache Anwendung mit hausgemachten Schauspielern ohne Orleans. Wie sie sagen, bevor sie ein Schiff bauen, wollen wir sehen, wie es schwimmt und warum ein gewöhnliches Papierboot schwimmt. Für Details willkommen bei Katze.

Inhalt


  • Game Server auf MS Orleans - Teil 1: Was sind Schauspieler?


Schauspieler


Ein Akteur ist eine Recheneinheit, die als Antwort auf eine empfangene Nachricht gleichzeitig:
eine endliche Anzahl von Nachrichten an andere Akteure senden kann ;
eine endliche Anzahl neuer Akteure schaffen;
Wählen Sie das Verhalten aus, das bei der Verarbeitung der nächsten empfangenen Nachricht verwendet werden soll.
Es wird nicht angenommen, dass eine bestimmte Abfolge der oben genannten Aktionen vorhanden ist, und sie können alle parallel ausgeführt werden.
Wir senden dem Schauspieler einige Nachrichten (Teams oder Events) und er selbst kann anderen die gleichen Nachrichten senden. Coroutinen mit Kanälen arbeiten nach einem ähnlichen Prinzip. Sie empfangen einige Daten vom Kanal und senden einige Daten an den Kanal, während sie asynchron arbeiten.

Beispiel


Eigentlich der Code für zwei sehr einfache Schauspieler:
    class Program
    {
        interface IMessage { }

        class IncrementCommand : IMessage { }

        class TellCountCommand : IMessage { }

        class SaidCountEvent : IMessage
        {
            public int Count { get; }
            public int ActorId { get; }

            public SaidCountEvent(int count, int actorId)
            {
                Count = count;
                ActorId = actorId;
            }
        }

        class WriteMessageCommand : IMessage
        {
            public string Message { get; }

            public WriteMessageCommand(string message)
            {
                Message = message;
            }
        }

        static Task CreateCounterActor(
            BlockingCollection<IMessage> output,
            BlockingCollection<IMessage> input,
            int id
            )
        {
            return Task.Run(() =>
             {
                 var count = 0;

                 while (true)
                 {
                     var m = input.Take();
                     if (m is IncrementCommand)
                         count++;
                     if (m is TellCountCommand)
                         output.Add(new SaidCountEvent(count, id));
                 }
             });
        }

        static Task CreateWriterActor(BlockingCollection<IMessage> input)
        {
            return Task.Run(() =>
             {
                 while (true)
                 {
                     var m = input.Take();
                     if (m is WriteMessageCommand write)
                         Console.WriteLine(write.Message);
                     if (m is SaidCountEvent sc)
                         Console.WriteLine(
                             $"Counter   {sc.Count}   {sc.ActorId}"
                             );
                 }
             });
        }

        static void Main(string[] args)
        {
            var writerInput = new BlockingCollection<IMessage>();
            var firstInput = new BlockingCollection<IMessage>();
            var secondInput = new BlockingCollection<IMessage>();
            var writer = CreateWriterActor(writerInput);
            var firstCounter = CreateCounterActor(writerInput, firstInput, 1);
            var secondCounter = CreateCounterActor(writerInput, secondInput, 2);
            for (int i = 0; i < 5; i++)
            {
                firstInput.Add(new IncrementCommand());
            }
            for (int i = 0; i < 9; i++)
            {
                secondInput.Add(new IncrementCommand());
            }
            firstInput.Add(new TellCountCommand());
            secondInput.Add(new TellCountCommand());
            writerInput.Add(new WriteMessageCommand("  Main"));
            Console.ReadLine();
        }
    }


  1. Die Schnittstelle, über die wir Nachrichten markieren, die der Akteur empfängt oder sendet:
    interface IMessage { }
    

  2. Der Befehl, mit dem wir den Schauspieler anweisen, seinen Zähler zu erhöhen (interner Zustand):
    class IncrementCommand : IMessage { }
    

  3. Der Befehl, mit dessen Hilfe wir dem Schauspieler sagen, dass er anderen seinen aktuellen Zustand (Zähler) mitteilen soll:
    class TellCountCommand : IMessage { }
    

  4. Ein Ereignis, das anderen Schauspielern mitteilt, dass der Schauspieler jedem seinen aktuellen Status (Zähler) mitgeteilt hat. Wird generiert, wenn der Befehl TellCountCommand verarbeitet wird:
    class SaidCountEvent : IMessage
    {
        public int Count { get; }
        public int ActorId { get; }
    
        public SaidCountEvent(int count, int actorId)
        {
            Count = count;
            ActorId = actorId;
        }
    }
    

    Anzahl ist, wie viel jetzt auf dem Zähler eines Akteurs mit einer Kennung gleich ActorId gewählt wird
  5. Dieser Befehl weist den Schauspieler an, diese Nachricht an die Konsole zu drucken:
    class WriteMessageCommand : IMessage
    {
        public string Message { get; }
    
        public WriteMessageCommand(string message)
        {
            Message = message;
        }
    }
    

  6. Startet eine Akteurinstanz, die den aktuellen Status des Zählers steuert:
            static Task CreateCounterActor(
             //   
                BlockingCollection<IMessage> output,
              // 
                BlockingCollection<IMessage> input,
             //              
                int id
                )
            {
                return Task.Run(() =>
                 { 
                     //  .    
                     var count = 0;
    
                     while (true)
                     {
                       //     
                      //             
                         var m = input.Take();
                         if (m is IncrementCommand)
                             count++;
                         if (m is TellCountCommand)
                             output.Add(new SaidCountEvent(count, id));
                     }
                 });
            }
    

  7. Erstellt einen Schauspieler, der einfach Nachrichten an die Konsole schreibt:
            static Task CreateWriterActor(BlockingCollection<IMessage> input)
            {
                return Task.Run(() =>
                 {
                     while (true)
                     {
                         var m = input.Take();
                         if (m is WriteMessageCommand write)
                             Console.WriteLine(write.Message);
                         if (m is SaidCountEvent sc)
                             Console.WriteLine(
                                 $"Counter   {sc.Count}   {sc.ActorId}"
                                 );
                     }
                 });
            }
    

  8.         static void Main(string[] args)
            {
                var writerInput = new BlockingCollection<IMessage>();
                var firstInput = new BlockingCollection<IMessage>();
                var secondInput = new BlockingCollection<IMessage>();
                var writer = CreateWriterActor(writerInput);
              //       
                var firstCounter = CreateCounterActor(writerInput, firstInput, 1);
                var secondCounter = CreateCounterActor(writerInput, secondInput, 2);
                //       
                for (int i = 0; i < 5; i++)
                {
                    firstInput.Add(new IncrementCommand());
                }
                //       .
                for (int i = 0; i < 9; i++)
                {
                    secondInput.Add(new IncrementCommand());
                }
                //         .
                //      writer      
                firstInput.Add(new TellCountCommand());
                secondInput.Add(new TellCountCommand());
               // writer     .
                writerInput.Add(new WriteMessageCommand("  Main"));
                Console.ReadLine();
            }
    



Nun, damit es absolut schön ist und wie der Grale (Schauspieler) aus Orleans aussieht, machen wir eine Verpackung auf unserer Theke:
    public interface ICounterActor
    {
        void Increment();
        void TellCount();
    }

    public class CounterActor : ICounterActor
    {
        private readonly Task _taks;
        private readonly BlockingCollection<IMessage> _input;
        private readonly BlockingCollection<IMessage> _output;

        public CounterActor(BlockingCollection<IMessage> output)
        {
            _input = new BlockingCollection<IMessage>(); ;
            _output = output;
            _taks = Task.Run(() =>
            {
                //State
                var count = 0;

                while (true)
                {
                    var m = _input.Take();
                    if (m is IncrementCommand)
                        count++;
                    if (m is TellCountCommand)
                        _output.Add(new SaidCountEvent(count, _taks.Id));
                }
            });
        }

        public void Increment()
        {
            _input.Add(new IncrementCommand());
        }

        public void TellCount()
        {
            _input.Add(new TellCountCommand());
        }
    }

Hier wurde ein reaktives Modell verwendet. Unsere Akteure geben keine Werte zurück, sondern generieren nur neue Nachrichten mit den gewünschten Werten. Hier haben wir einen Schauspieler nur in seinem eigenen Thread, Thread sicher vor allen anderen, erhöht seinen Zähler. Ein anderer Schauspieler schreibt einfach auf die Konsole. Stellen wir uns jedoch vor, dass jeder Schauspieler für uns beispielsweise die aktuellen Koordinaten, den Gesundheitszustand und das Manna eines der zehn Spieler berechnet, die derzeit auf unserem Server spielen. Jeder mit seinem eigenen Zustand in seinem eigenen Fluss. Wir senden Nachrichten an andere oder empfangen Nachrichten von anderen über den erlittenen Schaden, die verwendeten Fähigkeiten oder die Änderung unserer Koordinaten.

All Articles