如何检测用户是不是因网络断开而离开凤凰频道?

Posted

技术标签:

【中文标题】如何检测用户是不是因网络断开而离开凤凰频道?【英文标题】:How to detect if a user left a Phoenix channel due to a network disconnect?如何检测用户是否因网络断开而离开凤凰频道? 【发布时间】:2016-02-29 06:39:16 【问题描述】:

我有一个 Elixir/Phoenix 服务器应用程序,客户端通过 websockets 通过内置通道系统连接。现在我想检测用户何时离开频道。

旁注:我在 Google Chrome 扩展程序中使用 javascript 客户端库。为此,我从 Phoenix 中提取了 ES6 代码,将其转换为 javascript,并对其进行了一些调整,使其独立运行。

现在当我关闭弹出窗口时,服务器立即使用reason = :shutdown, :closed 触发terminate/2 函数。扩展方面不涉及关闭回调,所以这很棒!

但是当客户端只是失去网络连接时(我连接了第二台计算机并拔出网络插头)然后terminate/2不会触发。

为什么以及如何解决这个问题?

我尝试了transport :websocket, Phoenix.Transports.WebSockettimeout选项,但这没有成功。

更新: 有了新的很棒的 Phoenix 1.2 Presence 东西,这应该不再需要了。

【问题讨论】:

我刚刚注意到服务器并不总是能够识别弹出窗口何时关闭。所以我希望我的问题的解决方案也能解决这个问题。 请注意,当订阅频道的最后一个用户失去连接时,Presence 将不起作用。请参阅***.com/questions/53986369/… 这可能不是正确的解决方案。 另见elixirforum.com/t/… 显然,即使使用 Presence,您仍然需要一个外部进程来监控频道,所以这里的答案仍然非常相关。 【参考方案1】:

执行此操作的正确方法是不要在您的通道中捕获退出,而是让另一个进程监视您。当您下降时,它可以调用回调。下面是一个帮助您入门的 sn-p:

# lib/my_app.ex

children = [
  ...
  worker(ChannelWatcher, [:rooms])
]

# web/channels/room_channel.ex

def join("rooms:", <> id, params, socket) do
  uid = socket.assigns.user_id]
  :ok = ChannelWatcher.monitor(:rooms, self(), __MODULE__, :leave, [id, uid])

  :ok, socket
end

def leave(room_id, user_id) do
  # handle user leaving
end

# lib/my_app/channel_watcher.ex

defmodule ChannelWatcher do
  use GenServer

  ## Client API

  def monitor(server_name, pid, mfa) do
    GenServer.call(server_name, :monitor, pid, mfa)
  end

  def demonitor(server_name, pid) do
    GenServer.call(server_name, :demonitor, pid)
  end

  ## Server API

  def start_link(name) do
    GenServer.start_link(__MODULE__, [], name: name)
  end

  def init(_) do
    Process.flag(:trap_exit, true)
    :ok, %channels: HashDict.new()
  end

  def handle_call(:monitor, pid, mfa, _from, state) do
    Process.link(pid)
    :reply, :ok, put_channel(state, pid, mfa)
  end

  def handle_call(:demonitor, pid, _from, state) do
    case HashDict.fetch(state.channels, pid) do
      :error       -> :reply, :ok, state
      :ok,  _mfa ->
        Process.unlink(pid)
        :reply, :ok, drop_channel(state, pid)
    end
  end

  def handle_info(:EXIT, pid, _reason, state) do
    case HashDict.fetch(state.channels, pid) do
      :error -> :noreply, state
      :ok, mod, func, args ->
        Task.start_link(fn -> apply(mod, func, args) end)
        :noreply, drop_channel(state, pid)
    end
  end

  defp drop_channel(state, pid) do
    %state | channels: HashDict.delete(state.channels, pid)
  end

  defp put_channel(state, pid, mfa) do
    %state | channels: HashDict.put(state.channels, pid, mfa)
  end
end

在 Elixir/Phoenix HashDict 的较新版本中已将名称更改为 Map。 较新代码库的正确示例是:

# lib/my_app.ex

children = [
  ...
  worker(ChannelWatcher, [:rooms])
]

# web/channels/room_channel.ex

def join("rooms:", <> id, params, socket) do
  uid = socket.assigns.user_id]
  :ok = ChannelWatcher.monitor(:rooms, self(), __MODULE__, :leave, [id, uid])

  :ok, socket
end

def leave(room_id, user_id) do
  # handle user leaving
end

# lib/my_app/channel_watcher.ex

defmodule ChannelWatcher do
  use GenServer

  ## Client API

  def monitor(server_name, pid, mfa) do
    GenServer.call(server_name, :monitor, pid, mfa)
  end

  def demonitor(server_name, pid) do
    GenServer.call(server_name, :demonitor, pid)
  end

  ## Server API

  def start_link(name) do
    GenServer.start_link(__MODULE__, [], name: name)
  end

  def init(_) do
    Process.flag(:trap_exit, true)
    :ok, %channels: Map.new()
  end

  def handle_call(:monitor, pid, mfa, _from, state) do
    Process.link(pid)
    :reply, :ok, put_channel(state, pid, mfa)
  end

  def handle_call(:demonitor, pid, _from, state) do
    case Map.fetch(state.channels, pid) do
      :error       -> :reply, :ok, state
      :ok,  _mfa ->
        Process.unlink(pid)
        :reply, :ok, drop_channel(state, pid)
    end
  end

  def handle_info(:EXIT, pid, _reason, state) do
    case Map.fetch(state.channels, pid) do
      :error -> :noreply, state
      :ok, mod, func, args ->
        Task.start_link(fn -> apply(mod, func, args) end)
        :noreply, drop_channel(state, pid)
    end
  end

  defp drop_channel(state, pid) do
    %state | channels: Map.delete(state.channels, pid)
  end

  defp put_channel(state, pid, mfa) do
    %state | channels: Map.put(state.channels, pid, mfa)
  end
end

【讨论】:

我让它工作了,我有两个后续问题:1. 为什么这不是核心(太好了)? 2. 从网络断开到离开被触发之间的时间大约在 90 秒左右。这可以以任何方式定制吗? (我想将传输超时设置为 20 秒,每 10 秒 ping 一次服务器……但当然会消耗额外的资源) 这个解决方案很棒。唯一的问题是我在leave 函数中执行了一些数据库操作,而这个问题***.com/questions/38335635/… 发生在测试中。 很好,问题终于被 Elixir v1.8.0 和 DBConnection v2.0.4 twitter.com/plataformatec/status/1091300824251285504解决了

以上是关于如何检测用户是不是因网络断开而离开凤凰频道?的主要内容,如果未能解决你的问题,请参考以下文章

从凤凰频道一次订阅多个

凤凰频道的广播如何影响其他节点上的客户端?

玩家 1 邀请玩家 2 与凤凰频道玩游戏

新浪微博AcFun凤凰网的视频要被关了?只因它们没有这个……

如何卸载凤凰os硬盘版?

DevOps凤凰沙盘总结