どうも、靖宗です。
今回はPresence
ということであまりなじみのない概念・・・
どうやらChannnel絡みの機能だそうですので、前回のサンプルの続き、ということになりそうです。
Presence
Presenceはトピックのプロセス情報を取得したり、クラスタ上にこの情報を複製したりする機能のようです。
幸いサーバー側もクライアント側も実装がシンプルになっているそうなのでそこまで苦労することはないでしょう。
ごちゃごちゃ書いてますが要はCRDTであるといった感じです。
CRDTに関しては下記を参考にしました。
ぶっちゃけよく分かってませんが、要はチャットとかで時間の整合性が若干ズレても構わんけど最終的に全員の発言をきっちり表示したいとかそんなんだと思います。
Presenceモジュール作成
Presenceを利用するにはPresenceモジュールを作成する必要があるそうです。mixで作成していきます。
PS \hello> mix phx.gen.presence * creating lib/hello_web/channels/presence.ex Add your new module to your supervision tree, in lib/hello/application.ex: children = [ ... HelloWeb.Presence ] You're all set! See the Phoenix.Presence docs for more details: http://hexdocs.pm/phoenix/Phoenix.Presence.html
mix phx.gen.presence
するとlib/hello_web/channels/presence.ex
が作成されます。
該当するファイルはほとんどがコメントで中身はほとんどないです。
defmodule HelloWeb.Presence do ... use Phoenix.Presence, otp_app: :hello, pubsub_server: Hello.PubSub end
ひとまずこのままにしておきます。
生成した際にでていた指定通りにlib/hello/application.ex
にHelloWeb.Presence
を追記します。
... def start(_type, _args) do # List all child processes to be supervised children = [ # Start the Ecto repository Hello.Repo, # Start the endpoint when the application starts HelloWeb.Endpoint, # Starts a worker by calling: Hello.Worker.start_link(arg) # {Hello.Worker, arg}, HelloWeb.Presence, ] ...
mixを利用したChannel作成
次にRoomChannel
を作成します。前回作成したんですが、どうやらmix
でも生成できるみたいなので作り直します。
PS \hello> mix phx.gen.channel Room * creating lib/hello_web/channels/room_channel.ex * creating test/hello_web/channels/room_channel_test.exs Add the channel to your `lib/hello_web/channels/user_socket.ex` handler, for example: channel "room:lobby", HelloWeb.RoomChannel
lib/hello_web/channels/user_socket.ex
に上記の情報を追加し、connect
関数を書き換えます。
defmodule HelloWeb.UserSocket do use Phoenix.Socket channel "room:lobby", HelloWeb.RoomChannel def connect(params, socket) do {:ok, assign(socket, :user_id, params["user_id"])} end def id(_socket), do: nil end
channel
のルーティングの方は前回通りです。
connect
はパラメータからユーザーIDを取り出し、ソケット情報にアサイン(登録)しておきます。
おそらく接続ユーザー一覧とかを作成したい意図でしょう。とりあえず先に進みます。
最後のdef id(_socket), do: nil
ですが、サンプルではとくに記載されていませんがコレが無いとUserSocket
の生成の際にエラーが発生するようです。
Channel編集
RoomChannel
でPresenceを利用するように編集していきます。
defmodule HelloWeb.RoomChannel do use HelloWeb, :channel alias HelloWeb.Presence def join("room:lobby", payload, socket) do send(self(), :after_join) {:ok, socket} end def handle_info(:after_join, socket) do push(socket, "presence_state", Presence.list(socket)) {:ok, _} = Presence.track(socket, socket.assigns.user_id, %{ online_at: inspect(System.system_time(:second)) }) {:noreply, socket} end end
接続を確立した際にsend/2
で:after_join
というメッセージを送っています。
これはChannelの内部実装がGenServer
になっていて、send/2
でメッセージを送信しているようなものでしょう。なのでhandle_info
が発火してdef handle_info(:after_join, socket) do
の処理が始まるといったところです。
肝心のhandle_info
では"presence_state"
というイベントにPresence.list(socket)
というメッセージを載せてプッシュ(サブスクライブ)しています。
Presenceはどうやら"presence_state"
や"presence_diff"
といったイベントを監視して状態を共有しているのだと思います。
Presence.track
の箇所はユーザーID毎にどのタイミングまでオンラインであったかをトラッキングできるように情報を更新しているようなものでしょうか。inspect(System.system_time(:second))
で現在のシステムの時間がUNIX時間で格納されるようです。
クライアントサイドを実装する
phoenix.js
を使っているクライアント側でPresenceを取得したりするよう実装します。
どうやら"presence_state"
や"presence_diff"
といったイベントが発生した際にpresence.onSync
でその時に実行されるコールバック関数を登録できるようです。
現在接続しているユーザーを表示している際などは、レンダリング関数などもこのコールバックの中に入れておくのが望ましいでしょう。
Presenceに登録されている情報を取得するにはpresence.list
を利用するようです。
presences.list()
のコールバックには引数が二つ用意されており、第一引数はPresence id(今回でいうユーザーID)と、第二引数は登録してあるメタ情報(今回で言うonline_at
のマップ)が利用できます。
サンプルではassets/js/app.js
を利用してますが、ここでは前回同様assets/js/socket.js
を編集します。
import {Socket, Presence} from "phoenix" let socket = new Socket("/socket", { params: {user_id: window.location.search.split("=")[1]} }) let channel = socket.channel("room:lobby", {}); let presence = new Presence(channel); function renderOnlineUsers(presence) { let response = ""; presence.list((id, {metas: [first, ...rest]}) => { let count = rest.length + 1; response += `<br>${id} (count: ${count})</br>`; }); document.querySelector("#main").innerHTML = response; } socket.connect(); presence.onSync(() => renderOnlineUsers(presence)); channel.join(); export default socket
上記で#main
を利用するような記述に変更しましたのでlib/hello_web/templates/page/index.html.eex
を編集しておきます。
<div id="main"></div>
今回は接続しているクライアントを表示するだけにします。
これで実装完了です。実際にブラウザで見てみましょう。
動作確認
実際にブラウザで見てみます。
http://localhost:4000/?name=Alice
を2個、http://localhost:4000/?name=Bob
を1個のブラウザで開きます。
ここでは写し忘れましたが、ブラウザを閉じるときちんと名前が消えたりカウントが減ったりします。
厳密にはnameというクエリを取得しているわけではないんですが、とりあえずそれっぽいものはできました。
実際の実装ではクエリ文字からユーザーIDを取得するよりかはユーザートークンから取得することになりそうです。