Сервер Игры на MS Orleans — часть 1: Что такое Акторы

от автора

Привет Хабр! И так, на четырнадцатый день копья решил я значит начать делать простенький игровой сервер для простой онлайн стрелялки. За одно тему распределенных вычислений затронуть. В этой вводной статье цикла хочу рассказать что такое акторы (в Орлеанс их зернами называют) и принцип их работы. Для этого я пока пример простенького приложения с самодельными акторами без Orleans. Как говориться прежде чем строить корабль посмотрим как плавает и почему плавает обычный бумажный кораблик. За подробностями добро пожаловать под кат.

Содержание

  • Сервер Игры на MS Orleans — часть 1: Что такое Акторы

Акторы

Актор является вычислительной сущностью, которая в ответ на полученное сообщение может одновременно:
отправить конечное число сообщений другим акторам;
создать конечное число новых акторов;
выбрать поведение, которое будет использоваться при обработке следующего полученного сообщения.
Не предполагается существования определённой последовательности вышеописанных действий и все они могут выполняться параллельно.
Мы посылаем актору какие-то сообщения (Команды или События) и он сам может посылать другим такие же сообщения. По похожему принципу работаю корутины с каналами. Они принимает какие-то данные из канала и отправляет какие-то данные в канал работая при этом асинхронно.

Пример

Собственно код для двух очень простых акторов:

    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. Интерфейс которым мы помечаем сообщения которые получает или отправляет актор:
    interface IMessage { } 

  2. Команда с помощью которой мы говорим актору увеличить его счетчик (внутренне состояние):
    class IncrementCommand : IMessage { } 

  3. Команда с помощью которой мы говорим актору сказать его текущее состояние (счетчик) другим:
    class TellCountCommand : IMessage { } 

  4. Событие которое говорит другим акторам о том что актор сказал всем свое текущее состояние (счетчик). Генерируется при обработке команды TellCountCommand:
    class SaidCountEvent : IMessage {     public int Count { get; }     public int ActorId { get; }      public SaidCountEvent(int count, int actorId)     {         Count = count;         ActorId = actorId;     } } 

    Count это сколько сейчас набралось на счетчике у актора с идентификатором равным ActorId

  5. Эта команда говорит актору вывести данное сообщение на консоль:
    class WriteMessageCommand : IMessage {     public string Message { get; }      public WriteMessageCommand(string message)     {         Message = message;     } } 

  6. Запускает инстанс актора который управляет текущим состоянием счетчика:
            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. Создает актор который просто пишет сообщения в консоль:
            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();         } 

Тут использовалась реактивная модель. Наши акторы не возвращают значения а только генерируют новые сообщения с нужными значениями. Тут у нас один актор просто в своем отдельном потоке, потокобезопасно от всех остальных, увеличивает свой счетчик. Другой актор просто пишет в консоль. Однако представим что у нас каждый актор например вычисляет текущие координаты, запас здоровья и манны одного из десятка игроков которые сейчас играют на нашем сервере. Каждый со своим собственным состоянием в своем собственном потоке. Отправляем другим сообщения или получает от других сообщения о полученном уроне, использованном навыке или смене своих координат.

ссылка на оригинал статьи https://habr.com/ru/post/498326/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *