Co za tydzień, newsletter nazwany 12b i teraz jeszcze post ;) A wszystko po to by nie było 1X tak jak to jest przyjęte w Europie ;) Choć dostałem dzisiaj info, że w chinach nie ma numeru 4, gdyż czyta się go jako “SY”, a to brzmi tak samo jak śmierć. Czego to człowiek się nie uczy na starość :) Ale wracajmy na ziemie, to znaczy, do naszego elixira. Dziś w elixir rozwiniemy to co zaczęliśmy w zeszłym tygodniu. Czyli na bazie spawn
, send
, receive
, zbudujemy sobie mikro serwer działający w innym procesie, do którego będziemy słali ping
i oczekiwali pong
:) Zaczynajmy
Własny Serwer
Na początku założenia naszego mini serwera. Chcemy wysłać ping
do kodu działającego w innym procesie, odszczekać sekundę i uzyskać odpowiedź pong
. To odczekanie jest potrzebne by zademonstrować bottleneck o którym wspomniałem w zeszłym tygodniu.
Stwórzmy sobie najpierw plik PingPongServer.exs
w jakimś tam katalogu, i odpalmy iex
za pomocą komendy:
iex PingPongServer.exs
Teraz, mamy iex
i możemy działać, brakuje nam tylko kodu ;) a więc do działa. Co potrzebujemy by zrobić mini serwer?
- Serwer musimy jakoś wystartować tak by działał on na innym procesie
- By coś działało na innym procesie wykorzystujemy
spawn
- By móc coś wysłać na ten serwer, potrzebujemy znać jego
PID
, na szczęściespawn
nam go zwraca - By serwer działał cały czas w tle, to znaczy, że po otrzymaniu wiadomości, musi na nowo zacząć nasłuchiwać
- Możemy sobie ułatwić i stworzyć metodę do wysyłania wiadomośći
Ok, to od czego zaczynamy? Od stworzenia części serwerowej, która będzie działała w innym procesie, zwracała nam PID
jak i działała w kółko:
defmodule PingPongServer do # startujemy server def start do spawn(fn -> loop end) end # działamy w kółko defp loop do loop end end
Metoda PingPongServer.start
zwróci nam PID
utworzonego procesu, jak i wystartuje pętle loop
która będzie działała w kółko (rekurencja ogonowa, nic nam się w elixir nie powiesi).
Teraz, przydałoby się jakoś móc odbierać wiadomości wysłane do tego świeżo utworzonego procesu. W tym celu musimy zmodyfikować loop
tak by czekało ono na wiadomość – jak wiemy, czekamy aż do skutku, blokując wykonywanie kodu aplikacji, a więc do loop
wystarczy dodać:
defp loop do receive do msg -> IO.puts "Received: #{msg}" end loop end
Gdzie czekamy na wiadomość, która będzie zawierała treść przez nas wysłaną.
Możemy to przetestować w iex
:
r(PingPongServer) server = PingPongServer.start send(server, "Hello1") send(server, "Hello2") send(server, "Hello3")
Otrzymamy trzy wiadomości:
Received: Hello1 Received: Hello2 Received: Hello3
Teraz serwer działa tak jak chcieliśmy, w pętli odbiera wiadomość i zaczyna pracę od nowa. Komenda r(PingPongServer)
, ładuje nam na nowo moduł który został załadowany podczas ładowania iex
– więc jak tylko zmodyfikujecie PingPongServer
to można ją odpalić.
Dobrze, mamy już większość rzeczy zrobione, możemy teraz dodać czekanie 1
sekundę na odpowiedź serwera. Do odbioru wiadomości msg
wystarczy dodać :timer.sleep(1000)
, co powoduje, że nasz loop
wygląda następująco:
defp loop do receive do msg -> :timer.sleep(1000) IO.puts "Received: #{msg}" end loop end
Jako, że tworzymy serwer PingPong, to znaczy, że na pytanie ping
, powinniśmy odpowiedzieć pong
. Aktualnie, nie odpowiadamy niczym, jedynie wypisujemy, że otrzymaliśmy wiadomość. By móc odpowiedzieć, musimy znać PID
procesu który do nas wysłał wiadomość. Jak się okazuje, PID
to tylko identyfikator, więc można go przekazywać jako argument. Możemy tak zmodyfikować naszą pętlę by otrzymywała zarówno informację kto wysłał wiadomość jak i jej treść:
defp loop do receive do {caller, msg} -> :timer.sleep(1000) IO.puts "Received: #{msg} from #{inspect(caller)}" end loop end
Teraz by wysłać wiadomość, musimy przekazać PID
procesu wysyłającego wiadomość. Jest to dość proste, wystarczy podać na przykład self
:
r(PingPongServer) server = PingPongServer.start send(server, {self, "Hello1"})
Wysyłając wiadomość do procesu serwera, o treści {self, "Hello1"}
gdzie, self
to PID
naszego procesu, zaś "Hello1"
to wiadomość, powinniśmy otrzymać coś takiego (numer PID
może się różnić):
Received: Hello1 from #PID<0.61.0>
Super, pozostaje nam więc odesłanie ponga
– dodajemy wywołanie send
:
send(caller, {:pong, "...pong"})
Pozwoliłem sobie jednak trochę zmodyfikować nasz loop
by miał on licznik odebranych pingów
. Przypominam, że \\
ustawia domyślną wartość argumentu.
defp loop i \\ 0 do receive do {caller, msg} -> :timer.sleep(1000) IO.puts "##{i} Received: #{msg} from #{inspect(caller)}" send(caller, {:pong, "...pong ##{i}"}) end i = i + 1 loop i end
Dzięki czemu, wiemy którą wiadomość otrzymujemy i którą odsyłamy.
Dobrze, mamy prawie wszystko, mamy serwer działający w tle w nieskończoność. Odbiera on wiadomości, wykonuje jakąś długą operację i zwraca pong
do procesu który przesłał wiadomość do serwera. Teraz możemy ciutkę sobie ułatwić i napisać mały kod, który przyspieszy nam wysyłanie pingów
. Więc zamiast w iex
:
send(server, {self, "Hello1"})
Moglibyśmy wywołać funkcję
PingPongServer.ping(server)
W tym celu, musimy utworzyć funkcję ping
:
def ping server do send(server, {self, "ping"}) end
Jak chcemy, możemy to przetestować, wszystko powinno śmigać. Jednak wciąż nie otrzymujemy naszej wiadomości pong
. By ją otrzymać musimy nasłuchiwać na wiadomości. Najlepszym do tego miejscem będzie metoda ping
:
def ping server do send(server, {self, "ping"}) receive do {:pong, msg} -> IO.puts "Caller Received: #{msg}" msg end end
Teraz możemy to przetestować, jednak by nie mieć problemu z zakolejkowanymi wiadomościami do tej pory (a trochę mogliśmy ich mieć bawiąc się w jednej sesji iex
), ubijmy iex
i odpalmy go na nowo. Teraz jak wykonamy nasz ping
powinniśmy otrzymać wynik:
#0 Received: ping from #PID<0.61.0> Caller Received: ...pong #0 "...pong #0"
Ten ostatni pong
to jest wartość zwrócona z receive
-> ten msg
. On nie jest potrzebny, ale może się przydać w przykładzie z bottleneck. Na wszelki wypadek, pełny kod serwera:
defmodule PingPongServer do # startujemy server def start do spawn(fn -> loop end) end # wysyłamy ping def ping server do send(server, {self, "ping"}) receive do {:pong, msg} -> IO.puts "Caller Received: #{msg}" msg end end # działamy w kółko defp loop i \\ 0 do receive do {caller, msg} -> :timer.sleep(1000) IO.puts "##{i} Received: #{msg} from #{inspect(caller)}" send(caller, {:pong, "...pong ##{i}"}) end i = i + 1 loop i end end
Bottleneck
Mając nasz serwer gotowy i załadowany do iex
, wykonajmy sobie następujący kod:
Enum.each(1..100, fn(i) -> spawn(fn -> IO.puts "Call ##{i}" ret = PingPongServer.ping(server) # możemy to wypisać: IO.puts "Got #{ret} from call ##{i}" end) end)
Teraz, łatwo zaobserwować, że jak już wykonywanie kodu trwa sekundę, to na ostatnią odpowiedź na ping
będziemy musieli trochę poczekać. Lepiej nie czekajmy… ale widać, że łatwo coś co niby trwa mikrosekundy, zamienić w coś co będzie trwać baaardzo długo. Sprawdźcie zresztą sobie sami. Wywalcie timeout z serwera. I zobaczcie jak to będzie zapierniczało :)
Podsumowanie
To co jest najważniejsze, to to byście zobaczyli na jakiej zasadzie działa ten serwer. Byście go zrozumieli, i byli wstanie sami napisać taki bez ściągawki. Dlaczego? Dlatego, że to jest wzorzec najczęściej chyba w elixir wykorzystywany. Wszystkie jego implementacja są podobne. Wiedząc jak to działa, będziecie mogli na swoje potrzeby tworzyć podobne serwery.
Jednak, jest to na tyle popularny wzorzec elixir, że aż jest dostępny moduł powodujący, że pisanie takiego serwera, jest jeszcze prostsze. Myślałem, żę się dziś z tym tematem też uwinię, ale się nie udało. A więc, pozostawiam go do następnego tygodnia :)
PS.: Naprawdę, tego typu serwer każdy kto się uczy lub pracuje z elixir powinien móc napisać samemu. Jest to podstawą! :)
PS2.: Zauważyliście, że właśnie stworzyliście obliczanie równoległe bez konieczności robienia żadnych synców? ;)
[…] Elixir #12b – Ping Pong Server […]
[…] Elixir #12b – Ping Pong Server […]
Comments are closed.