Phoenix websocket: how to send message to one user

channels
phoenix

#1

Hello,
I’m new to Phoenix and I’m following this tutorial https://hexdocs.pm/phoenix/channels.html to implement something similar to chat application as described in that tutorial.
That article talks about how to broadcast a message, I want to send a message to a particular user. And I couldn’t find something similar to send. How I can do that.
Also, to do this, do I have to save all the connected socket objects in another process’s state, maybe using a map which maps username -> socket.


#2

One way to do it would be to subscribe your users to a particular topic individual to each one of them. Then you would have a way of sending particular messages to them using that topic and simply broadcasting to it, effectively working as if you were “sending” them a message. Of course this only accounts for when the receiving user is online and subscribed to his own topic.


#3

Thanks, this should work.
How costly/cheap is creating a topic?


#4

AFAIK the phoenix team is not yet charging for creating topics, so it’s free.

(j/k)

The cost will probably be one entry with one term in an ETS table? How that translates into “really” useful information I’m not sure.


#5

The trick is to broadcast to a specific user with

socket.endpoint.broadcast!(“user:#{id}”, message, payload)


#6

Quite cheap, I wouldn’t worry about it at this point.


#7

video tutorial for user to user private messages


#8

I might be able to do this if you’re still interested.


#9

I just wonder if there’s a way to do it without having to create additional channels per user/use specific user IDs. In my use case there will be no registration and there will be no preexisting IDs unless I generate a random one for each new user who connects, which is doable but doesn’t feel exactly natural.

I wonder if there’s a way for Presence to actually fetch specific processes/sockets, e.g. to fetch the first socket connection of the first user that joined, and pair her with the second user as soon as the second user joins. It doesn’t seem exactly easy though so far.

I meant to make a new post but the tag selection seems broken at the moment.


#10

You can access the underlying websocket like in Broadcast to socket without channel and communicate with the user directly via it, if that’s what you need.


#11

I guess my problem is how I can keep track of the socket IDs/PIDs. I think I’m still a bit confused over all the terminologies and their relations between each other.

In short, my use case:

  • There is a lobby.
  • Whenever enough number (e.g. 3) of users joined the lobby, they will be teamed up, each assigned a role to start a conversation.
  • They should then be removed from the lobby.
  • The next 3 users to join repeat this process, etc.

My question is, when the third user joins, how can I:

  • know that he’s the third user
  • locate the first two users (i.e. find the PIDs of their sockets I guess)
  • send a customized message to each user (because each has a different role)
  • remove all of them from whatever tracking mechanism I’m using

The problem with normal Channel and Presence mechanisms is that they don’t seem to be able to hold any global state.

One mechanism that I’ve thought of with Presence (likely to be incorrect):

def handle_info(:after_lobby_join, socket) do
  Presence.track(socket, "lobby", %{
    pid: self()
  })

  {:noreply, socket}
end

def handle_info(:start, socket) do
  pid1 = hd(Presence.list(socket)["lobby"][:metas])[:pid]
  # Start the conversation by sending messages individually to pid1 and pid2
  ...
  untrack(pid1, "my_app:lobby", "lobby")
  {:ok, socket}
end

In the example you linked there is the use of Registry. Are you suggesting to record the PIDs of the first two users in Registry under keys such as first_user, second_user, and then look them up and send messages to them via send(socket_pid, {:socket_push, :text, Poison.encode!(%{"oh" => "my"})})?


#12

If I didn’t need distribution (although it would be possible to add later), I’d probably have a genserver tracking the new users.

def handle_info({:new_user, socket}, [{us1, ref1}, {us2, ref2}]) do
  # we had 2 users, a new one joined, so that's 3
  user_sockets = [socket, us1, us2]
  # we can create a room for them now
  create_new_room(user_sockets)
  # and clean the state
  Enum.each([ref1, ref2], fn ref ->
    Process.demonitor(ref)
  end)
  {:noreply, []}
end

def handle_info({:new_user, socket}, waiting_user_sockets) do
  # otherwise just add the new users to the waiting users list
  ref = Process.monitor(socket)
  {:noreply, [{socket, ref} | waiting_user_sockets]}
end

def handle_info({:DOWN, ref, :process, _object, _reason}, waiting_user_sockets) do
  # remove the disconnected socket
end