Jak wspomniałem w jednym z wcześniejszych już wpisów, nie ma znaczenia, gdzie aktor jest zlokalizowany. Dzięki AKKA.NET jest to szczegół konfiguracyjny. Jeśli pewnego dnia, stwierdzimy, że wykonywanie obliczeń na jednym komputerze nie wystarcza, wtedy po prostu zmieniamy konfigurację, aby hostować danego aktora gdzieś indziej. Framework zadba o komunikację (TCP) między węzłami znajdującymi się w innych sieciach. W ten sposób, bardzo łatwo jest skalować logikę w następujący sposób: wątek->proces->komputer->sieć komputerów.
W dokumentacji znajdziemy szczegóły, ale moim zdaniem brakuje tam prostego przykładu polegającego na wysłaniu wiadomości z węzła A do B.
Zacznijmy od stworzenia struktury projektu. Warto stworzyć jedną bibliotekę, która będzie zawierać wyłącznie kod aktorów. W naszym przypadku będzie to EchoServerActor:
public class EchoServerActor : ReceiveActor { public EchoServerActor() { Receive<EchoMsg>(msg => { Sender.Tell($"Server:{AppDomain.CurrentDomain.FriendlyName},{msg.Text}"); }); } } public class EchoMsg { public string Text { get; set; } }
Jak widać, kod nie różni się niczym od implementacji “lokalnych” aktorów. Po odebraniu wiadomości, wyświetlamy na ekranie nazwę domeny oraz przesłaną wiadomość. Wyświetlenie aktualnej domeny będzie pomocne, w analizie gdzie kod został tak naprawdę wykonany. EchoServeActor będzie służył nam jako “serwer”. W praktyce, komunikacja odbywa się klient-klient i nie należy projektować systemów w sposób scentralizowany. Awaria jednego aktora nie powinna spowodować paraliżu całego systemu. W poście jednak, chcemy napisać jak najprostszy fragment kodu, stąd te uproszczenie.
Jako “klient” posłuży nam następująca klasa:
public class EchoReceiverActor : ReceiveActor { public EchoReceiverActor() { Receive<string>(msg => { Console.WriteLine($"Received on {AppDomain.CurrentDomain.FriendlyName}:{msg}"); }); Receive<EchoMsg>(msg => { var remoteActor = Context.ActorSelection("akka.tcp://Server@localhost:8081/user/EchoServerActor"); remoteActor.Tell(msg); }); } }
Po odebraniu wiadomości “EchoMsg” uzyskujemy referencję za pomocą ścieżki (więcej szczegółów tutaj). W praktyce, nie chcemy umieszczać w kodzie aktora, informacji o jego lokalizacji – powinno to mieć miejsce np. w pliku konfiguracyjnym. Powyższy kod jest zatem złapaniem bardzo ważnej zasady o neutralności fizycznej lokalizacji aktora. W ramach wpisu chcę napisać kod jednak jak najprościej tylko można. W każdym razie, po odebraniu EchoMsg, EchoReceiverActor wyśle wiadomość do zdalnego aktora.
Z kolei jeśli przyjdzie wiadomość w formie czystego tekstu (string), wtedy wyświetlamy ją. Nie trudno domyślić się, że w naszym przykładzie taka wiadomość będzie pochodzić od zdalnego aktora.
Innymi słowy, najpierw po stronie klienta wysyłamy EchoMsg do EchoReceiverActor. Aktor z kolei prześle tą wiadomość zdalnie do EchoServerActor, który z kolei odpowie tekstem do EchoReceiverActor.
Przejdźmy teraz do konfiguracji “serwera”:
var config = ConfigurationFactory.ParseString(@" akka { actor { provider = ""Akka.Remote.RemoteActorRefProvider, Akka.Remote"" } remote { helios.tcp { transport-class = ""Akka.Remote.Transport.Helios.HeliosTcpTransport, Akka.Remote"" applied-adapters = [] transport-protocol = tcp port = 8081 hostname = localhost } } } "); using (var system = ActorSystem.Create("Server", config)) { system.ActorOf<EchoServerActor>("EchoServerActor"); Console.ReadLine(); }
Konfiguracja w prawdziwych projektach umieszczana jest w App\Web.Config, tak aby mnożna ją było zmienić bez potrzeby rekompilacji. Widzimy, że serwer będzie nasłuchiwać na porcie 8081. Zostanie również stworzony system o nazwie “Server” oraz pojedynczy aktor o nazwie “EchoServerActor”.
Klient z kolei wygląda następująco:
Console.ReadLine(); var config = ConfigurationFactory.ParseString(@" akka { actor { provider = ""Akka.Remote.RemoteActorRefProvider, Akka.Remote"" } remote { helios.tcp { transport-class = ""Akka.Remote.Transport.Helios.HeliosTcpTransport, Akka.Remote"" applied-adapters = [] transport-protocol = tcp port = 0 hostname = localhost } } } "); using (var system = ActorSystem.Create("Client", config)) { var receiver = system.ActorOf(Props.Create<EchoReceiverActor>()); receiver.Tell(new EchoMsg { Text = "Hello world" }); Console.ReadLine(); }
W przypadku “klienta” nie musimy określać portu. Jak wiemy, wszystko działa na zasadzie klient-klient, ale w kodzie sami nie musimy nic samemu wysyłać do klienta – stąd nie ma to znaczenia. Wartość zero oznacza, że port zostanie wybrany automatycznie. Następnie wysyłamy wiadomośc EchoMsg do aktora EchoReceiverActor. Jak wiemy z powyższego kodu, EchoReceiverActor jest hostowany w systemie “Client”. Następnie wyśle on wiadomość do zdalnego systemu “Server”.
Po uruchomieniu serwera, zobaczymy, że faktycznie system Server nasłuchuje na 8081:
Z kolei po uruchomieniu klienta, zobaczymy, że nasłuchuje on na automatycznie wybranym porcie 8591:
Z powyższego screenu również widać, że wiadomość została z powrotem przesłana do klienta. Dzięki wyświetleniu nazwy domeny, widzimy kiedy wiadomość została odebrana przez serwer (echo) i przesłana do kienta.
Error: An unhandled exception of type ‘Akka.Configuration.ConfigurationException’ occurred in Akka.dll Additional information: ‘akka.actor.provider’ is not a valid type name : ‘Akka.Remote.RemoteActorRefProvider, Akka.Remote’
Solution: PS> Install-Package Akka.Remote http://getakka.net/docs/remoting/