原文前端
隨着Phoenix web 框架進入你們的視野, 許多人驚訝於它對 websockets 優秀的支持, 以及用它建立一個"hello world" 聊天應用是多麼簡單. 鑑於 websockets 在 Phoenix 中的一等公民地位, 我想能夠用它來解決一些比簡單的聊天應用更難的問題. 在本文中, 咱們將瞭解如何使用Phoenix 建立一個包含邀請功能的遊戲大廳.git
因爲Phoenix和Elixir 仍然處於開發中, 本篇中的代碼可能會過期. 本文的代碼使用 Elixir 1.2.0 和 Phoenix 1.1.3.github
首先咱們須要一些登陸了的用戶. 我依照這篇文章設置了用戶以及基本的驗證. 爲了在 websockets 裏處理驗證, 我將使用 Phoenix.Token
.web
咱們須要給用戶一個token使他們可以證實本身的身份. 我使用<meta> 標籤來存放token. 在你的應用的layout中, 添加:後端
<head> ... <%= if @current_user do %> <%= tag :meta, name: "channel_token", content: Phoenix.Token.sign(@conn, "user", @current_user.id) %> <% end %> ... </head>
爲驗證用戶鏈接時傳來的token是否合法, 咱們須要修改 web/channels/user_socket.ex
文件中的 connect
方法:瀏覽器
alias MyApp.{Repo, User} def connect(%{"token" => token}, socket) do case Phoenix.Token.verify(socket, "user", token, max_age: 86400) do {:ok, user_id} -> socket = assign(socket, :current_user, Repo.get!(User, user_id)) {:error, _} -> :error end end
這能夠從token中解析出用戶的ID, 並將用戶賦值到socket中, 使得咱們以後能夠調用它.服務器
爲了從前端發起websocket鏈接, 咱們須要從meta 標籤中獲取token, 並使用它在鏈接到後端時創建一個Socket鏈接. 將如下代碼添加到 socket.js
文件中:websocket
var token = $('meta[name=channel_token]').attr('content'); var socket = new Socket('/socket', {params: {token: token}}); socket.connect();
在咱們的服務器上, 一旦用戶鏈接到了網站, 咱們但願他們能夠看見其餘在線的玩家. 讓咱們開始建立一個大廳channel, 在這裏全部用戶能夠加入並相互交談.cookie
channel "game:lobby", MyApp.LobbyChannel
defmodule MyApp.LobbyChannel do use MyApp.Web, :channel def join("game:lobby", _payload, socket) do {:ok, socket} end end
和前端鏈接:app
var lobby = socket.channel('game:lobby'); lobby.join().receive('ok', function() { console.log('Connected to lobby!'); });
如今用戶已經登陸了, 並經過websockt 鏈接到了大廳, 他們應當可以查看其餘在線的用戶並邀請他們. 因爲Elixir是一門函數式語言且不能保存state, 因此實現起來會頗有挑戰性. 咱們將在一個獨立的進程中使用 GenServer 來模擬保存state. 將來Phoenix可能會實現相似的功能, 可是如今咱們須要本身實現它. (譯者注: 如今已經有了 Phoenix.Presence) 感謝Phoenix Trello project 的做者, 我從他那裏學到了這些.
這裏是讓咱們的大廳運做所需的代碼. 我不會深刻探討GenServer 是如何運做的, 從高級層面上來看咱們能夠將他當成是一個持久的映射.
defmodule MyApp.ChannelMonitor do use GenServer def start_link(initial_state) do GenServer.start_link(__MODULE__, initial_state, name: __MODULE__) end def user_joined(channel, user) do GenServer.call(__MODULE__, {:user_joined, channel, user}) end def user_left(channel, user_id) do GenServer.call(__MODULE__, {:user_left, channel, user_id}) end def handle_call({:user_joined, channel, user}, _from, state) do new_state = case Map.get(state, channel) do nil -> Map.put(state, channel, [user]) users -> Map.put(state, channel, Enum.uniq([user | users])) end {:reply, new_state, new_state} end def handle_call({:users_in_channel, channel}, _from, state) do new_users = state |> Map.get(channel) |> Enum.reject(&(&1.id == user_id)) new_state = Map.update!(state, channel, fn(_) -> new_users end) {:reply, new_state, new_state} end end
而後咱們須要將 ChannelMonitor 添加到 start 函數中, 這樣Phoenix啓動時就會自動啓動它. 修改好以後, 重啓你的服務器.
def start(_type, _args) do ... children = [ ... worker(MyApp.ChannelMonitor, [%{}]), ] end
如今, 咱們能夠在channels 裏使用 ChannelMonitor 了. 在LobbyChannel裏, 做以下修改:
defmodule MyApp.LobbyChannel do use MyApp.Web, :channel alias MyApp.ChannelMonitor def join("game:lobby", current_user) do current_user = socket.assigns.current_user users = ChannelMonitor.user_joined("game:lobby", current_user)["game:lobby"] send self, {:after_join, users} {:ok, socket} end def terminate(_reason, socket) do user_id = socket.assigns.current_user.id users = ChannelMonitor.user_left("game:lobby", user_id)["game:lobby"] lobby_update(socket, users) :ok end def handle_info({:after_join, users}, socket) do lobby_update(socket, users) {:noreply, socket} end defp lobby_update(socket, users) do broadcast! socket, "lobby_update", %{users: get_usernames(users)} end defp get_usernames(nil), do: [] defp get_usernames(users) do Enum.map users, &(&1.username) end end
這段代碼作了什麼? ChannelMonitor 是一個映射, 以 channel 名做爲key, 以用戶列表做爲value. 每次咱們更新 ChannelMonitor 時都會返回那個映射, 咱們能夠在其中查找對應channel 的用戶. 因爲value 是用戶列表, 咱們須要提取每一個用戶的用戶名, 再發送到前端. 咱們須要在鏈接開始和終止時更新 ChannelMonitor, 經過 join
和 terminate
方法. 注意咱們仍然能夠獲取 socket.assigns.current_user
.
當咱們想要從服務器經過channel 發送消息給每一個用戶, 咱們使用 broadcast! socket, name_of_event, data
. 這裏咱們發送了一個 "lobby_update" 事件, 並將新的列表發送給每一個在線的用戶. 若是你試圖在join 函數中使用broadcast! , Phoenix會報錯, 由於在socket中join尚未完成. 使用send self, {args}
可讓你在join過程當中發送消息, 而後咱們能夠在 handle_info
中進行模式匹配, 再廣播給全部用戶.
前端的接收很是簡單. 修改大廳的代碼, 監聽"lobby_update" 事件, 並獲取咱們從後端發來的數據:
var lobby = socket.channel('game:lobby'); lobby.on('lobby_update', function(response) { console.log(JSON.stringfy(response.users)); }); lobby.join().receive('ok', function() { console.log('Connected to lobby!'); });
如今當用戶鏈接/斷線時全部用戶都能看到. 你能夠在兩個瀏覽器標籤登陸不一樣的帳號, 由於新標籤會有不一樣的cookies.
咱們已經獲得了在線玩家列表. 如今咱們想要和他們中的一個進行遊戲. 咱們的遊戲大廳將會實現一個 邀請/接收 的流來從大廳裏開始遊戲. 咱們須要在後端監聽邀請事件, 並將其分發到正確的人. 咱們能夠這樣實現:
def handle_in("game_invite", %{"username" => username}, socket) do data = %{"username" => username, "sender" => socket.assigns.current_user.username} broadcast! socket, "game_invite", data {:noreply, socket} end
你會發現到這裏有些錯誤. 咱們想要將邀請發送給特定的用戶, 而不是廣播出去. 這裏的問題在於發送消息給前端的方法只有 send 和 broadcast. send
方法須要目標socket, 然而咱們只有發送者的 socket. 因此, 咱們使用 broadcast 並定義了一個 handle_out
使得消息只發送給咱們想要發給的人.
intercept ["game_invite"] def handle_out("game_invite", %{"username" => username, "sender" => sender}, socket) do if socket.assigns.current_user.username == username do push socket, "game_invite", %{username: sender} end {:noreply, socket} end
intercept 告訴Phoenix爲特定的事件的廣播使用咱們定義的 handle_out. 在這裏咱們是和鏈接到channel裏的每一個玩家對話, 並執行咱們想要的操做. 直到找到那個被邀請的玩家, 咱們發送一個誰邀請了他的信號給他. 要從前端邀請, 咱們要添加以下代碼:
lobby.on('game_invite', function(response) { console.log('You were invited to join a game by', response.username); }); window.invitePlayer = function(username) { lobby.push('game_invite', {username: username}); };
如今你可使用 invitePlayer('other_user')
來試着給在線的玩家發送邀請. 消息應當只發送給目標.
本文中, 咱們建立了一個大廳, 能夠看到當前在線的玩家, 並向他們發送開始遊戲的邀請. 咱們藉助Phoenix 對websockets方便的操控搭建了這個系統, 並將 state 保存在了一個單獨的進程. 以後, 你能夠建立額外的channel 給用戶, 讓他們在邀請玩家以後能夠進行遊戲. Happy coding!