Game Server en MS Orleans - Parte 1: ¿Qué son los actores?



Hola habr Y así, en el decimocuarto día de la lanza, decidí comenzar a hacer un servidor de juegos simple para disparos en línea simples. Para un tema de computación distribuida al tacto. En este artículo introductorio del ciclo quiero decir qué son los actores (en Orleans se les llama granos) y el principio de su trabajo. Para esto, sigo siendo un ejemplo de una aplicación simple con actores caseros sin Orleans. Como dicen antes de construir un barco, veamos cómo flota y por qué flota un barco de papel común. Para más detalles, bienvenido a cat.

Contenido


  • Game Server en MS Orleans - Parte 1: ¿Qué son los actores?


Actores


Un actor es una entidad computacional que, en respuesta a un mensaje recibido, puede simultáneamente:
enviar un número finito de mensajes a otros actores;
crear un número finito de nuevos actores;
Seleccione el comportamiento que se utilizará al procesar el siguiente mensaje recibido.
No se supone que existe una cierta secuencia de las acciones anteriores, y todas pueden realizarse en paralelo.
Le enviamos al actor algunos mensajes (Equipos o Eventos) y él mismo puede enviar a otros los mismos mensajes. Las corutinas con canales funcionan según un principio similar. Reciben algunos datos del canal y envían algunos datos al canal mientras trabajan de forma asíncrona.

Ejemplo


En realidad, el código para dos actores muy simples:
    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. La interfaz por la cual marcamos los mensajes que el actor recibe o envía:
    interface IMessage { }
    

  2. El comando con el que le decimos al actor que aumente su contador (estado interno):
    class IncrementCommand : IMessage { }
    

  3. El comando con la ayuda del cual le decimos al actor que le diga su estado actual (contador) a otros:
    class TellCountCommand : IMessage { }
    

  4. Un evento que le dice a otros actores que el actor les ha dicho a todos su estado actual (contador). Generado cuando se procesa el comando TellCountCommand:
    class SaidCountEvent : IMessage
    {
        public int Count { get; }
        public int ActorId { get; }
    
        public SaidCountEvent(int count, int actorId)
        {
            Count = count;
            ActorId = actorId;
        }
    }
    

    El recuento es cuánto se marca ahora en el mostrador de un actor con un identificador igual a ActorId
  5. Este comando le dice al actor que imprima este mensaje en la consola:
    class WriteMessageCommand : IMessage
    {
        public string Message { get; }
    
        public WriteMessageCommand(string message)
        {
            Message = message;
        }
    }
    

  6. Inicia una instancia de actor que controla el estado actual del contador:
            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. Crea un actor que simplemente escribe mensajes en la consola:
            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();
            }
    



Bueno, para que sea absolutamente hermoso y se vea como el Grale (actor) de Orleans, hagamos un envoltorio en nuestro mostrador:
    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());
        }
    }

Aquí se utilizó un modelo reactivo. Nuestros actores no devuelven valores, sino que solo generan nuevos mensajes con los valores deseados. Aquí tenemos un actor solo en su propio hilo, hilo a salvo de todos los demás, aumenta su contador. Otro actor simplemente escribe en la consola. Sin embargo, imaginemos que para nosotros cada actor, por ejemplo, calcula las coordenadas actuales, la salud y el maná de uno de los diez jugadores que actualmente juegan en nuestro servidor. Cada uno con su propio estado en su propio flujo. Enviamos mensajes a otros o recibimos mensajes de otros sobre el daño recibido, la habilidad utilizada o el cambio de nuestras coordenadas.

All Articles