LoginSignup
3

More than 5 years have passed since last update.

Elixirで遠隔PCに侵入#2風「GenServerでサーバ/クライアントを気軽に作る」

Last updated at Posted at 2017-08-16

2ヶ月ぶりのご無沙汰です:bow_tone1:

前回の fukuoka.ex#1 の後くらいから、福岡と東京を行ったり来たりしつつ、以下登壇/勉強会をこなし、最新技術をプロダクション採用している種々企業のスーパーエンジニアの方々にお話を聞きに行く、というハードな2ヶ月で、このElixirコラムもすっかり止まってしまいました…:sweat:

その一方で、こうした活動の中で出会った方々と、Elixirの良さを共有する機会もあり、次に覚えるプログラミング言語としてElixirを多くの方に選んでいただけた、良質な布教活動(笑)の2ヶ月でもありました :wine_glass:

少しでもElixirがプロダクション採用されるよう、より本番運用やデータ分析みたいな領域にアプローチする内容にてリスタートしたいと思いますので、どうぞよろしく :blush:

さて今回の連載は、Elixirで遠隔PCに侵入ハッカー気分 の続きをやると見せかけ、途中でOTPを使ったリファクタリングをしたり、「耐障害性」に分岐するようなパラレルワールド的展開をしてみようかな、と :ocean:

image.png

前準備

スライドP6にある通り、mixで「Pass」というElixirプロジェクトを作成済みのところからスタートします

iexを起動しておいてください

# iex -S mix
iex>

また、スライドでは、PC間での通信をする例としていますが、説明を分かりやすくするため、サーバーもクライアントも1台のPC内で行うこととします

サーバとクライアントをGenServerで1つにまとめる

P25で定義している「pwdサーバ」は、以下の通り、File.cwd()でカレントフォルダを取得して返すだけのサーバーです

lib/pass.ex
defmodule Pass do
 …
    def start_pwd_server() do
        pid = spawn( Pass, :pwd_server, [] )
        :global.register_name( :pwd, pid )
    end
    def pwd_server() do
        receive do
            { sender_pid, _ } -> 
                { :ok, result } = File.cwd()
                send( sender_pid, { true, result } )
        end
        pwd_server()
    end
 …

P28で定義している「pwdクライアント」は、以下の通り、pwdサーバプロセスにメッセージを投げて、フォルダパスを待ち受けるだけのクライアントです

lib/pass.ex
defmodule Pass do
 …
    def pwd() do
        send( :global.whereis_name( :pwd ), { self(), "" } )
        listen()
    end
 …

まず、pwdサーバを起動します


iex> Pass.start_pwd_server

このサーバに問合せをすると、カレントフォルダが取得できます

iex> Pass.pwd
/code/pass

こういったサーバ/クライアントのアプリは、機能の違いはあれど、構成やインタフェースはほぼ同じため、Elixirでは、「GenServer」という名前で汎用モジュール化されています :aerial_tramway:

以下のように「use GenServer」をモジュール内に記述し、handle_call()でサーバ側処理を書けば終わりです

lib/pass_genserver.ex
defmodule PassGenServer do
    use GenServer

    def handle_call( :pwd, _from, state ) do
        { :ok, result } = File.cwd()
        { :reply, result, state }
    end
end

GenServerでのpwdを試してみましょう

GenServer.start_link()でサーバ起動し、GenServer.call()でhandle_call()に定義した処理が呼び出せます

iex> recompile()
iex> { :ok, pid } = GenServer.start_link( PassGenServer, "" )
{:ok, #PID<0.297.0>}
iex> GenServer.call( pid, :pwd )
"/code/pass"

うまくいきました :thumbsup:

GenServerで書く効果

まず、GenServer版と、そうで無い版のコードを見比べて欲しいのですが、コード量が1/3程度にスリム化されています :dancer_tone1:

次に、GenServer版のコードには、本質的な処理(ここではpwd実行)しか書いておらず、プロセス起動やメッセージパッシングのような処理は一切書かなくて良くなっています

サーバとクライアントを別々に定義する必要が無くなり、1つにまとめられるので、メンテナンス性も向上しています

そして、ここからが凄いのですが、関数名と引数/返却を少し変えるだけで、非同期化対応できてしまうのです! :flushed:

GenServerだと非同期化もラクラク

ここまで作ったものは、同期処理のため、スリープを入れると返ってこなくなることを、まず確認しておきます

lib/pass_genserver.ex
defmodule PassGenServer do
    use GenServer

    def handle_call( :pwd, _from, state ) do
        { :ok, result } = File.cwd()
        Process.sleep( 3000 )
        { :reply, result, state }
    end
end

実行すると、処理呼び出し後、スリープ分、待たされます :hourglass_flowing_sand:

iex> recompile()
iex> { :ok, pid } = GenServer.start_link( PassGenServer, "" )
{:ok, #PID<0.297.0>}
iex> GenServer.call( pid, :pwd )
 …
 (3秒、待たされる)
 …
"/code/pass"

非同期化するには、handle_call() を handle_cast() に変更して、引数_fromを削除し、返却を:noreplyに変える… たったこれだけ :thinking:

:lib/pass_genserver.ex
defmodule PassGenServer do
    use GenServer

    def handle_cast( :pwd, state ) do
        { :ok, result } = File.cwd()
        Process.sleep( 3000 )
        IO.puts "on handle_cast(): " <> result
        { :noreply, state }
    end
end

呼出も、GenServer.cast() に変え、試してみると...

iex> recompile()
iex> { :ok, pid } = GenServer.start_link( PassGenServer, "" )
{:ok, #PID<0.406.0>}
iex> GenServer.cast( pid, :pwd )
:ok                   【←呼出後、即座に返ってくる】
iex> on handle_cast(): /code/pass  【←3秒後に表示される】

バッチリ非同期になりました :laughing:

元々のコードもそんなに複雑では無かったのですが、これはそもそもElixirが、サーバ/クライアントのような、マルチプロセスを書きやすい言語だからな訳ですが、GenServerを使うと、もはや本質的な処理以外は、書かなくて良いレベルまで昇華されます、OTP恐るべし :scream:

次回に続く)


p.s.

:stars: :stars: 【お知らせ】「fukuoka.ex #2」、今月末8/24(金)に開催です :stars: :stars:

ElixirとPhoenix(高速Web・APIフレームワーク)のマルチプロセスと耐障害性をテーマに、セッションを行います

ちょっと難しそうに聞こえるテーマですが、分かりやすく噛み砕いてセッションしますし、懇親会でのご質問もドシドシ受け付けますので、ご安心ください。

https://techjin.connpass.com/event/63493/
image.png

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3