我正在使用Elixir频道编写应用程序来处理实时事件。我知道每个客户端将打开1个套接字,并且可以在其上复用多个通道。所以我的应用程序是一个聊天应用程序,用户是多个群聊的一部分。我有一个名为MessageChannel的Phoenix Channel,其中join方法将处理动态主题。
def join("groups:" <> group_id, payload, socket) do
....
假设John加入群组/主题A和B而Bob只加入群组/主题B.当john向群组/主题A发送消息时,广播!/ 3也会将该消息发送给Bob太正确了吗?因为handle_in没有发送消息的主题/组的上下文。
我如何处理它以便Bob不会收到发送给A组的事件。我设计得对吗?
答案 0 :(得分:5)
因为
handle_in
没有发送邮件的主题/组的上下文。
当调用Phoenix.Channel.broadcast/3
时,显然 的主题与消息相关联(签名中不明显)。您可以看到以on this line of channel.ex开头的代码:
def broadcast(socket, event, message) do
%{pubsub_server: pubsub_server, topic: topic} = assert_joined!(socket)
Server.broadcast pubsub_server, topic, event, message
end
因此,当使用套接字调用broadcast/3
时,它会模式匹配当前主题,然后调用基础Server.broadcast/4
。
(如果你像我一样好奇,这反过来调用底层的PubSub.broadcast/3
,它会做一些分配魔术来将调用路由到你配置的pubsub实现服务器,很可能使用pg2但是我离题了...)
所以,我发现这种行为在阅读Phoenix.Channel
docs时并不明显,但他们在Incoming Events的phoenixframework频道页面中明确说明了这一点:
broadcast!/3
将通知此套接字主题的所有加入的客户端,并调用他们的handle_out/3
回调。
所以它只是在这个套接字的主题上播出了#34;他们在同一页面上定义主题:
topic - 字符串主题或主题:子主题对名称空间,例如“messages”,“messages:123”
所以在你的例子中,&#34;主题&#34;实际上是主题:子主题对命名空间字符串:"groups:A"
和"groups:B"
。 John必须在客户端上单独订阅这两个主题,因此您实际上会引用两个不同的通道,即使它们使用相同的套接字。因此,假设您正在使用javascript客户端,通道创建看起来像这样:
let channelA = this.socket.channel("groups:A", {});
let channelB = this.socket.channel("groups:B", {});
然后,当您从客户端在频道上发送消息时,您只使用具有在服务器上匹配模式的主题的频道,如上所述。
channelA.push(msgName, msgBody);
答案 1 :(得分:2)
实际上,套接字路由是基于如何使用channel
API在项目Socket模块中定义主题来完成的。对于我的Slack克隆,我使用三个通道。我有一个系统级通道来处理状态更新,用户频道和房间频道。
任何给定用户订阅0或1个频道。但是,用户可能订阅了多个频道。
对于发往特定房间的信息,我通过房间频道进行广播。
当我检测到特定房间的未读消息,通知或徽章时,我使用用户频道。每个用户频道也存储用户订阅的房间列表(它们列在客户端侧栏上)。
所有这一切的诀窍是使用几个渠道API,主要是intercept
,handle_out
,My.Endpoint.subscribe
和handle_info(%Broadcast{},socket)
。
intercept
来捕获我要忽略的广播消息,或者在发送之前进行操作。handle_info
次%Broadcast{}
次调用,其中包含广播消息的主题,事件和有效负载。以下是我的代码的几个部分:
defmodule UcxChat.UserSocket do
use Phoenix.Socket
alias UcxChat.{User, Repo, MessageService, SideNavService}
require UcxChat.ChatConstants, as: CC
## Channels
channel CC.chan_room <> "*", UcxChat.RoomChannel # "ucxchat:"
channel CC.chan_user <> "*", UcxChat.UserChannel # "user:"
channel CC.chan_system <> "*", UcxChat.SystemChannel # "system:"
# ...
end
# user_channel.ex
# ...
intercept ["room:join", "room:leave", "room:mention", "user:state", "direct:new"]
#...
def handle_out("room:join", msg, socket) do
%{room: room} = msg
UserSocket.push_message_box(socket, socket.assigns.channel_id, socket.assigns.user_id)
update_rooms_list(socket)
clear_unreads(room, socket)
{:noreply, subscribe([room], socket)}
end
def handle_out("room:leave" = ev, msg, socket) do
%{room: room} = msg
debug ev, msg, "assigns: #{inspect socket.assigns}"
socket.endpoint.unsubscribe(CC.chan_room <> room)
update_rooms_list(socket)
{:noreply, assign(socket, :subscribed, List.delete(socket.assigns[:subscribed], room))}
end
# ...
defp subscribe(channels, socket) do
# debug inspect(channels), ""
Enum.reduce channels, socket, fn channel, acc ->
subscribed = acc.assigns[:subscribed]
if channel in subscribed do
acc
else
socket.endpoint.subscribe(CC.chan_room <> channel)
assign(acc, :subscribed, [channel | subscribed])
end
end
end
# ...
end
我还将user_channel用于与特定用户相关的所有事件,如客户端状态,错误消息等。
答案 2 :(得分:1)
免责声明:我没有查看频道的内部工作情况,这些信息完全来自我在应用程序中使用频道的第一次体验。
当有人加入另一个组时(基于join/3
中的模式匹配),将建立通过单独通道(套接字)的连接。因此,向A的广播不会向B的成员发送消息,只有A.
在我看来,Channel模块类似于GenServer
,并且连接有点像start_link
,其中新的服务器(进程)被旋转(但是,只有它还没有存在)。
你真的可以忽略模块的内部工作原理,只是理解如果你加入一个名称与现有名称不同的频道,你就加入了一个独特的频道。您还可以相信,如果您向频道广播,则只有该频道的成员才能收到该消息。
例如,在我的应用程序中,我有一个用户频道,我只希望连接一个用户。联接看起来像def join("agent:" <> _agent, payload, socket)
,其中代理只是一个电子邮件地址。当我向此频道广播消息时,只有单个代理接收消息。我还有一个所有代理加入的办公室频道,当我希望所有代理接收消息时,我都会向它广播。
希望这有帮助。