当我开始学习 Elixir 和 Phoenix 时, 让我最震惊的部分是它对实时网络连接的一流支持.一旦你掌握了 Elixir , 并习惯于函数式编程, 你就可以愉快地使用Phoenix 为我们设置好的 socket 和 channels 抽象.
我想用一个项目来测试这些特性. 而且, 我想我应该全部使用函数式编程来完成. Elm 语言正在改变我们编写客户端代码的方式, 我们很享受这种体验.
在本系列中, 我想强调 Elixir, Phoenix 和 Elm 提供的有趣的抽象, 它们使得构建应用比过去更加愉快.
系列中的第一篇将探讨如何使用 Elixir 和 Phoenix 编写一个在线音乐制作应用”Loops With Friends”的后端. 该应用在一个给定的”Jam”上最多支持七个用户, 每个用户拥有自己的音乐循环. 每个用户可以开始和停止自己的循环, 与其它用户在Jam中进行实时演奏. 这个应用会在用户加入和离开时自动创建和平衡额外的卡带.
在我们编写的过程中,我将重点介绍这一切是如何连接在一起的,逐步向现有函数添加行。如果你想看看最终的源代码和测试,请务必通过链接检查完整的代码。我们不会花费太多时间在数据转换的细节,所以如果你是Elixir的新手,可以先去看看 ELixir Guides.
加入一个Jam
当玩家访问应用时,服务器向下发送客户端代码以及Jam标识符。 然后客户端代码立即请求针对该标识符的Phoenix Channel的WebSocket连接。 服务器应用的Endpoint模块将/ socket路径绑定到UserSocket模块。
# lib/loops_with_friends/endpoint.ex
defmodule LoopsWithFriends.Endpoint do
use Phoenix.Endpoint, otp_app: :loops_with_friends
socket "/socket", LoopsWithFriends.UserSocket
# ...
end
UserSocket 模块声明由 socket 支持的 channels. 模式jams:*
指定了客户端请求的哪些 channel 会被匹配. 同时, connect 函数为套接字分配了一个用户 ID, 这样我们就可以知道在初始连接点后的所有点上我们正在与哪个用户通信.
# web/channels/user_socket.ex
defmodule LoopsWithFriends.UserSocket do
use Phoenix.Socket
channel "jams:*", LoopsWithFriends.JamChannel
def connect(_params, socket) do
{:ok, assign(socket, :user_id, UUID.uuid4())}
end
end
最后, JamChannel 模块实现 join 函数, 它匹配客户端请求的话题, 回复用户 ID, 并将 jam ID分配给 socket.
# web/channels/jam_channel.ex
defmodule LoopsWithFriends.JamChannel do
use LoopsWithFriends.Web, :channel
def join("jams:" <> jam_id, _params, socket) do
# ...
{:ok,
%{user_id: socket.assigns.user_id},
assign(socket, :jam_id, jam_id)}
end
end
到这一步, 用户已经成功加入了 jam, 但是只有一个用户的 jam 是很寂寥的.
跟踪用户
Phoenix 的 Presence 功能可以使我们在应用中轻松管理连接了的实体. 如果应用是分布式的,它甚至可以跨节点工作。 虽然Loops With Friends没有大到需要分布式,Presence 仍然可以非常方便地使我们的应用中的多个客户端进行交互。
向我们的应用程序添加 presence 支持非常简单,只需创建一个 presence 样板模块,它将使用Phoenix的Presence模块。 我们将其它模块添加到应用的监督树,然后与这个模块交互。
在我们的Channel中,当用户加入时,我们将socket,用户的id和任何希望存储的元数据传递给Presence.track函数。
# web/channels/jam_channel.ex
defmodule LoopsWithFriends.JamChannel do
# ...
alias LoopsWithFriends.Presence
def join("jams:" <> jam_id, _params, socket) do
Presence.track(socket, socket.assigns.user_id, %{
user_id: socket.assigns.user_id
})
# ...
end
end
这些就是我们从服务器端追踪用户所需要的一切. Phoenix Presence 通过 socket 实现了客户端与服务器通信.
循环回路
我们想要确保每个用户在加入时获得不同的音乐循环。 为了实现这一点,我们需要从一个 jam 里的用户还没有选取的任何循环中选择一个循环。 LoopCycler模块的next_loop函数(为了简洁在此省略)负责这个工作。 在我们的channel 的 join函数中,我们传递已经采用过循环给 next_loop ,并将结果包括在由Presence跟踪的元数据中。 从当前 presence 列表中确定已经获取的循环, 是由添加到 Presence模块的辅助函数完成的。
# web/channels/jam_channel.ex
defmodule LoopsWithFriends.JamChannel do
# ...
def join("jams:" <> jam_id, _params, socket) do
Presence.track(socket, socket.assigns.user_id, %{
user_id: socket.assigns.user_id,
loop_name: LoopCycler.next_loop(present_loops(socket))
})
# ...
end
defp present_loops(socket) do
socket
|> Presence.list
|> Presence.extract_loops
end
end
将循环名称添加到用户的 presence, 客户端也能由此获得它们的循环.
处理事件
当用户加入, 离开, 播放自己的循环, 或停止自己的循环, 同一个 Jam 里的其他用户应该知道这些. Phoenix Channels 提供了 Elixir OTP 风格的回调, 来处理客户端事件. 此外, 我们在 JamChannel 模块中使用 handle_info
和 handle_in
回调来分别通知用户的加入和播放/停止. 我们通过向 channel 进程发送: after_join
消息来调用 handle_info
回调, 以便我们异步推出 presence_state
通知, 以允许我们的用户完成他们的加入.
# web/channels/jam_channel.ex
defmodule LoopsWithFriends.JamChannel do
# ...
def join("jams:" <> jam_id, _params, socket) do
# ...
send self(), :after_join
# ...
end
def handle_info(:after_join, socket) do
push socket, "presence_state", Presence.list(socket)
{:noreply, socket}
end
def handle_in("loop:" <> event, %{"user_id" => user_id}, socket) do
broadcast! socket, "loop:#{event}", %{user_id: user_id}
{:noreply, socket}
end
# ...
end
离开 jam 的用户通过 presence_diff
消息由 Phoenix 自动传播到 channel 里的所有客户端.
下一期
我们已经设置了服务器应用, 用户可以加入 jam, 查看其他用户, 获取他们的循环, 以及在整个 jam 中发送和接受循环事件.
一旦我们的小应用变得流行, 在同一个 jam 里就会有一百万的用户, 这会产生问题. 在下一篇文章中, 我们将处理超过七个用户时所产生的干扰.