Servidor de jogos no MS Orleans - Parte 1: O que são atores



Oi Habr! Então, no décimo quarto dia da lança, decidi começar a criar um servidor de jogo simples para fotografar online simples. Para um tópico da computação distribuída tocar. Neste artigo introdutório do ciclo, quero dizer o que são os atores (em Orleans são chamados de grãos) e o princípio de seu trabalho. Por isso, ainda sou um exemplo de uma aplicação simples com atores caseiros sem Orleans. Como se costuma dizer antes de construir um navio, vamos ver como ele flutua e por que um barquinho de papel comum flutua. Para detalhes, bem-vindo ao gato.

Conteúdo


  • Servidor de jogos no MS Orleans - Parte 1: O que são atores


Atores


Um ator é uma entidade computacional que, em resposta a uma mensagem recebida, pode simultaneamente:
enviar um número finito de mensagens para outros atores;
criar um número finito de novos atores;
Selecione o comportamento que será usado ao processar a próxima mensagem recebida.
Não se supõe que exista uma certa sequência das ações acima, e todas elas podem ser executadas em paralelo.
Enviamos ao ator algumas mensagens (equipes ou eventos) e ele próprio pode enviar aos outros as mesmas mensagens. As corotinas com canais trabalham com um princípio semelhante. Eles recebem alguns dados do canal e enviam alguns dados para o canal enquanto trabalham de forma assíncrona.

Exemplo


Na verdade, o código para dois atores muito 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. A interface pela qual marcamos as mensagens que o ator recebe ou envia:
    interface IMessage { }
    

  2. O comando com o qual dizemos ao ator para aumentar seu contador (estado interno):
    class IncrementCommand : IMessage { }
    

  3. O comando com o qual solicitamos ao ator que informe seu estado atual (contador) para os outros:
    class TellCountCommand : IMessage { }
    

  4. Um evento que informa aos outros atores que o ator disse a todos o seu estado atual (contador). Gerado quando o comando TellCountCommand é processado:
    class SaidCountEvent : IMessage
    {
        public int Count { get; }
        public int ActorId { get; }
    
        public SaidCountEvent(int count, int actorId)
        {
            Count = count;
            ActorId = actorId;
        }
    }
    

    Contagem é quanto agora é discado no contador de um ator com um identificador igual a ActorId
  5. Este comando diz ao ator para imprimir esta mensagem no console:
    class WriteMessageCommand : IMessage
    {
        public string Message { get; }
    
        public WriteMessageCommand(string message)
        {
            Message = message;
        }
    }
    

  6. Inicia uma instância de ator que controla o estado atual do 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. Cria um ator que simplesmente grava mensagens no console:
            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();
            }
    



Bem, para que seja absolutamente lindo e pareça com o sombrio (ator) de Orleans, vamos fazer uma embalagem em nosso balcão:
    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());
        }
    }

Aqui foi utilizado um modelo reativo. Nossos atores não retornam valores, mas apenas geram novas mensagens com os valores desejados. Aqui temos um ator apenas em seu próprio segmento, o segmento seguro de todos os outros, aumenta seu contador. Outro ator apenas escreve no console. No entanto, vamos imaginar que, para nós, cada ator, por exemplo, calcule as coordenadas atuais, a saúde e o maná de um dos dez jogadores que estão atualmente jogando em nosso servidor. Cada um com seu próprio estado em seu próprio fluxo. Enviamos mensagens a outras pessoas ou recebemos mensagens sobre os danos recebidos, a habilidade usada ou a alteração de nossas coordenadas.

All Articles