原文前端
随着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!