sockets 和 channels 是Phoenix中用来实现实时效果的两大工具。
Sockets
socket是用来连接客户端与服务器的,它使用endpoint来声明:
defmodule GenPoker.Endpoint do use Phoenix.Endpoint, otp_app: :gen_poker socket "/socket", GenPoker.PlayerSocketend
Channels
客户端只有加入了channel之后才能发送消息。
defmodule GenPoker.PlayerSocket do use Phoenix.Socket channel "tables:*", GenPoker.TableChannelend
创建socket
defmodule GenPoker.PlayerSocket do use Phoenix.Socket transport :websocket, Phoenix.Transports.WebSocket def connect(%{"playerId" => player_id}, socket) do {:ok, assign(socket, :player_id, player_id)} end def id(socket) do "players_socket:#{socket.assigns.player_id}" endend
注册进程
defmodule Poker.Table do use GenServer def start_link(table_name, sup, storage, num_seats) do GenServer.start_link( __MODULE__, [table_name, sup, storage, num_seats], name: via_tuple(table_name) ) end defp via_tuple(table) do {:via, :gproc, {:n, :l, {:table, table}}} end def whereis(table) do :gproc.whereis_name({:n, :l, {:table, table}}) endend
我们使用了gproc库来注册进程,这样就可以使用一个term而不仅仅是atom作为名字。让我们来定义Channel:
module GenPoker.TableChannel do use GenPoker.Web, :channel alias Poker.Table def join("tables:" <> table, _payload, socket) do {:ok, assign(socket, :table, table)} end def handle_in(command, payload, socket) when command in ~w(sit leave buy_in cash_out deal) do table = Table.whereis(socket.assigns.table) arguments = [table, socket.assigns.player_id] ++ payload result = apply(Table, String.to_atom(command), arguments) if result == :ok do broadcast! socket, "update", Table.get_state(table) end {:reply, result, socket} endend
对于客户端的join请求,我们有不同的回复。在JavaScript中可以这样写:
channel.push("message", arguments) .receive("ok", (msg) => console.log("Got OK!")) .receive("error", (msg) => console.log("Oops!"))
发送初始的state
def join("tables:" <> table, _payload, socket) do state = table |> Table.whereis |> Table.get_state push socket, "update", state {:ok, assign(socket, :table, table)}end
使用handle_info
def join("tables:" <> table, _payload, socket) do send self, :after_join {:ok, assign(socket, :table, table)}enddef handle_info(:after_join, socket) do state = socket.assigns.table |> Table.whereis |> Table.get_state push socket, "update", state {:noreply, socket}enddef handle_info(_, socket) do {:noreply, socket}end
拦截消息
def handle_out("update", state, socket) do push socket, "update", hide_other_hands(state, socket) {:noreply, socket}enddefp hide_other_hands(state, socket) do player_id = socket.assigns.player_id hide_hand_if_current_player = fn %{id: ^player_id} = player -> player player -> Map.delete(player, :hand) end update_in(state.players, fn players -> Enum.map(players, hide_hand_if_current_player) end)end