トピック
Qiitaのアドベントカレンダーの執筆したり、業務が忙しすぎたり、PID
の闇にハマっていたりで、少し間が空いてしまいましたが第15回のレポートをまとめました
今回も前回同様に、kogaさんにお声がけ頂きましてfukuoka.exさんと合同でもくもく勉強会を開催しました
fukuokaex.connpass.com
僕はQiitaのアドベントカレンダーでElixir
でNode
間チャットアプリを作るってのを書こうと思って、前からやりたかったNode
間通信に挑戦したので当日の内容と後日の作業をプラスでまとめたものを共有します
清流elixir-infomation
開催場所: 丸の内(愛知)
参加人数: 12 -> 4 コミュニティ参加人数 : 36 -> 37 update!
2019/12/07現在
第15回の勉強会について
Nodeの基本的な操作
Elixir
でのNode
間通信を行う基本的な操作はプログラミングElixirの第15章にまとめられている。しかし、ただまねるだけでは面白くないので以下の記事を参考にNode
間でチャットを作る簡易的なアプリケーションを作ってみようと思う
その前に基本的な Node
に関するコマンド処理を覚えておこう。まず複数のNode
を識別する必要があるためNode
に名前を付けてあげよう。名前を付ける際はiex
の起動時に--sname
というオプションを付与して以下のように起動する。すると、立ち上がったプロンプトに付与したNode
名が表示されていることが確認できる
$ iex --sname node1 : iex(node1@okabe-y)1>
自身のNode
名を確認したい時にはNode.self()
とすることでアトム形式で自身のNode
名を取得することが出来る
iex(node1@okabe-y)1> Node.self() :"node1@okabe-y"
これでNode
への名前付けと自身の名前の確認が出来るようになった。次にもう1つターミナルを用意して、別のNode
を立ち上げてみる。こちらにはnode2という名前を付与する
$ iex --sname node2 : iex(node2@okabe-y)1>
次に、この用意した2つのNode
を実際に繋げてみる。といっても、やり方は凄く簡単で自身のNode
に接続させてやりたいNode
の情報をNode.connect()/1
を通して渡してやるだけだ。今回はnode1に対してnode2を接続させてみよう。まずはnode2のターミナルで自身のNode
情報を確認する
iex(node2@okabe-y)1> Node.self() :"node2@okabe-y"
次に、node1のターミナルで先ほど確認したnode2の情報を用いて、実際にNode
を接続させる。接続に無事に成功するとtrue
が返ってくる
iex(node1@okabe-y)2> Node.connect(:"node2@okabe-y") true
ついでに自身のNode
に今、どのNode
が接続しているのかを確認しておこう。Node.list()
とすることで接続されているNode
の一覧をリスト形式で取得する事が出来る
iex(node1@okabe-y)3> Node.list() [:"node2@okabe-y"]
無事に、先ほどの接続が完了していることが確認出来た
チャットサーバーの用意
ベースに使用しているのは先ほど紹介させて頂いた記事だが、せっかくなのでチャットルームの概念をサーバーに用意してみた。少しコードが長くなってしまったので順に解説はしていく
defmodule ClientInfo do defstruct name: "GUEST", room: "roomA", pid: nil end defmodule ChatServer do def start() do pid = spawn(__MODULE__, :receiver, [[]]) :global.register_name(:server, pid) end def receiver(clients) do receive do # 全体通知のメッセージ {sender, {:announce, body}} -> IO.puts("[ANNOUNCE] #{body}") send(sender, {:ok}) receiver(clients) # ルーム単位のメッセージ {sender, {:message, room, body}} -> Enum.filter(clients, fn client -> client.room == room end) |> Enum.map(fn client -> IO.puts("[MESSAGE]: #{client.room}@#{client.name} #{body}") end) send(sender, {:ok}) receiver(clients) # ルームへの新規参加 {sender, {:join, room, name}} -> client = %ClientInfo{name: name, room: room, pid: sender} add = clients ++ [client] IO.puts("[JOIN]: #{name} joined the #{room}") send(sender, {:ok}) receiver(add) # ルームから退室 {sender, {:leave, room, name}} -> IO.puts("[LEAVE]: #{name} left the #{room}") send(sender, {:ok}) receiver(Enum.filter(clients, fn client -> !(client.room == room and client.name == name) end)) {sender, {:status}} -> IO.puts("[STATUS] send server status to client") send(sender, {:ok, :status, clients}) receiver(clients) end end end defmodule Client do def server_pid(), do: :global.whereis_name(:server) def announce(body) do send(server_pid(), {self(), {:announce, body}}) receiver() end def message(room, body) do send(server_pid(), {self(), {:message, room, body}}) receiver() end def join(room, name) do send(server_pid(), {self(), {:join, room, name}}) receiver() end def leave(room, name) do send(server_pid(), {self(), {:leave, room, name}}) receiver() end def status() do send(server_pid(), {self(), {:status}}) receiver() end def receiver() do receive do {:ok} -> IO.puts("success send message to server") {:ok, :status, data} -> Enum.map(data, fn d -> IO.puts("[INFO]: #{d.name} joined #{d.room}") end) _ -> IO.puts("invalid message") after 3000 -> IO.puts("not reply...") end end end
まずはチャットサーバーで扱うクライアントのデータの構造をstruct
を用いて定義している。今回のチャットサーバーは凄くシンプルなもので、それぞれ以下を参照している
- name -> クライアントの名前
- room -> クライアントが入室している部屋(今回は入室は1室のみ)
- pid -> クライアントの
PID
defmodule ClientInfo do defstruct name: "GUEST", room: "roomA", pid: nil end
次にメインとなるチャットサーバーの実装がChatServer
モジュールに記述してある。基本的にやっていることは凄くシンプルでElixir
のメッセージパッシングの構文receive
をパターンマッチングを使って複数種類のメッセージを受け取るようにして、それぞれのメッセージを受け取った際の処理を順に記述しているだけになる
- announce -> 接続しているクライアント全員にメッセージを通知
- message -> 指定したルームにいるクライアント全員にメッセージを通知
- join -> 指定したルームに新規にクライアントを参加させる
- leave -> 指定したルームからクライアントを退室(削除)させる
- status(デバッグ用) -> 現在チャットサーバーに接続しているクライアントの情報(誰がどこのルームに参加しているか)を返す
また、start()
を呼び出すことで、メッセージ受信用のプロセスを新規で立ち上げて、:global.register_name
を使って、接続しているNode
間で立ち上げた新規のプロセス情報を共有する。これはメッセージをメッセージ受信用のプロセスに送信するために行なっている(send
はPID
を指定する必要があるため)
そして、最後にクライアントがメッセージを送信するためのモジュールがClient
になる。それぞれの関数はsend
構文をwrapして、関数単位で処理が出来るようにしているだけなので、実装しなくても同様の処理は行えるが、楽にメッセージを送信するために実装した
メッセージを送ってみる
先ほど実装したコードを適当なファイル名を付けてiex
を実行したパスに配置しよう。僕は適当にmessage_server.ex
という名前を付けた。このファイルをそれぞれのiex
上からコンパイルしてモジュールを実行可能にする
iex(node1@okabe-y)4> c("message_server.ex") [Client, ChatServer, ClientInfo]
iex(node2@okabe-y)2> c("message_server.ex") [Client, ChatServer, ClientInfo]
node1をメッセージ受信に用いて、node2からメッセージを送ってみる。まず、node1でチャットサーバーを起動する。その前にNode.list()
を確認して、node1にnode2が接続していることを確認しておこう。Node
が接続されていないと、:global.whereis_name
が値(チャットサーバーのPID
)が取得出来ないので、メッセージを送る事が出来ない
無事に、チャットサーバーが立ち上がると:yes
というアトムが返ってくる
iex(node1@okabe-y)5> Node.list() [:"node2@okabe-y"] iex(node1@okabe-y)6> ChatServer.start() :yes
次に、node2の方からメッセージを送ってみる
iex(node2@okabe-y)3> Client.join("Elixirを朝まで語る部屋", "okb") success send message to server :ok
node1のターミナルを確認すると受信したメッセージを元に作成したログを確認することが出来るだろう
[JOIN]: okb joined the Elixirを朝まで語る部屋
せっかくなのでもう1つNode
を追加して、よりチャットっぽさを出してみよう。新たにターミナルを立ち上げてnode3という名前でiexを用意して、node1に接続させる
$ iex --sname node3 : iex(node3@okabe-y)1> c("message_server.ex") [Client, ChatServer, ClientInfo]
node1のターミナル
iex(node1@okabe-y)7> Node.connect(:"node3@okabe-y") true
無事に成功したようなので、node3からnode2と同じチャットルームにjoinさせて、適当にチャットしてみる
iex(node3@okabe-y)3> Client.join("Elixirを朝まで語る部屋", "にゃーん") success send message to server :ok
node1
[JOIN]: にゃーん joined the Elixirを朝まで語る部屋
チャットサーバーに接続しているクライアントの確認
node2
iex(node2@okabe-y)5> Client.status() [INFO]: okb joined Elixirを朝まで語る部屋 [INFO]: にゃーん joined Elixirを朝まで語る部屋 [:ok, :ok]
では、メッセージを送ってみよう node2
iex(node2@okabe-y)6> Client.message("Elixirを朝まで語る部屋", "こんにちは、初めまして!") success send message to server :ok
node1
# 送信されたというログ(フロントを実装していないため、サーバーログで確認) [MESSAGE]: Elixirを朝まで語る部屋@okb こんにちは、初めまして! [MESSAGE]: Elixirを朝まで語る部屋@にゃーん こんにちは、初めまして!
node3
iex(node3@okabe-y)5> Client.message("Elixirを朝まで語る部屋", "こんにちは、okbさん") success send message to server :ok
node1
[MESSAGE]: Elixirを朝まで語る部屋@okb こんにちは、okbさん [MESSAGE]: Elixirを朝まで語る部屋@にゃーん こんにちは、okbさん
nod2
iex(node2@okabe-y)12> Client.leave("Elixirを朝まで語る部屋", "okb") success send message to server :ok
node1
[LEAVE]: okb left the Elixirを朝まで語る部屋
node3
iex(node3@okabe-y)6> Client.message("Elixirを朝まで語る部屋", "まだ朝じゃないのにいなくなったww") success send message to server :ok
node1
[MESSAGE]: Elixirを朝まで語る部屋@にゃーん まだ朝じゃないのにいなくなったww
無事にやり取りが出来ていることが確認出来た
今後の発展
今回のNode
間通信は同一のホストで行なっただけで、まだNode
間通信の真髄を味わったわけではない。それにチャットサーバーもクライアント管理が適当であったり、認証がなかったり、データ構造がいまいちだったりと改善の余地が山ほどある。しかしながら、Elixir
でのNode
間通信の基礎を楽しく抑えるには十分だろう
次はDocker
を使って別ホストとのNode
間通信を行なったり、cloudのVMと接続したりということをやってみようと思う