mikejm

mikejm

How do you manually send a client a message through their established Websock?

This is an Elixir question, not a Phoenix question. I am not interested in any PubSub or Channels solution, but rather how to do this with basic Elixir.

If you have a user who has established a WebSock connection as shown here in the EchoServer example or as shown in the MyAppWeb.ConnectionTimer example, how can you utilize this websocket to send them a message from a distinct Elixir process/service?

Both the conn and the WebSock behavior are not processes. They have no process ID. Thus they have no capacity to receive messages. They are just looping on state in reaction to user triggered handle_in stimulus.

1) ELIXIR SERVICE TO CLIENT MESSAGING (EASY)

One can envision a way the conn/Websock could be made to send out messages into Elixir in response to incoming triggers easily enough:

  • Create a Genserver on each new connection made and set the PID for this into the state of conn/WebSock on its initialization.
  • When a specific handle_in request is received, you can use Process.send to send that GenServer a message which it will then receive it under handle_info

Thus we can easily allow client → separate Elixir process system communication through the WebSock.

2) ELIXIR SERVICE TO CLIENT MESSAGING (UNCLEAR?)

But what about separate Elixir process → client WebSock communication? Where such communication is not triggered by the user’s initiation, but rather our system’s?

Let’s say for whatever reason that GenServer wants to send the client a message through their websock. How can it do so? How can any process do so?

My current guess based on the fact that in this example he is running:

  defp schedule_alert, do: Process.send_after(self(), :alert, :timer.seconds(15))

This suggests to me each running conn/WebSock while not a process itself is being run by and is associated with a parent process (direct Supervisor?) that is uniquely associated with it (one-to-one). So somewhere here there is a possible solution.

EXPERIMENT

I just tested debugging out the PID of the main supervisor that starts my router in Application.ex and also debugging out self() in the router itself under get / for simple localhost:port/ accesses. On every reload, a new PID is shown there, which is not the PID of the main application supervisor that started the router.

This suggests a new process is initiated on every new web request to the router. But what is this process, then, and how do we control it? Presumably it has the conn/Websock within its own state (or the conn/WebSock functions are augmenting or overriding its base functions) when the connection is upgraded so if we can control that or override its functions we can perhaps accomplish the goal?

Simpler: If we assume all conn/WebSock’s have their own unique one-to-one Supervisor PID running them given by self() within the WebSock code (which seems to be true), then we can message that PID just like in this timer websock to trigger the handle_info of the WebSock. Then based on the handle_info manually run inside that handle_in which has the capacity to message the user directly through the websock.

I am thinking that is the solution now as I work it through. I will try that next.

GOAL

I am looking for the basic Elixir solution to this. To pre-empt, yes I have tried to solve this already but can’t. I have four books on Elixir sitting on the couch next to me but none explain this. ChatGPT can’t seem to answer without telling me about Phoenix (which I’m not interested in and is not magic in any case and must follow the same laws as base Elixir).

There is obviously some way to do this that systems like Phoenix are utilizing. That’s what I want to understand.

Thanks for any help.

Marked As Solved

mikejm

mikejm

Ah got it!

Working

  def handle_info(msg, state) do
   {:push,  {:text, "HELLO"}, state}
end

Thanks! And thanks to all in this thread! That’s the end. :joy:
:beers:

Also Liked

LostKobrakai

LostKobrakai

This is not correct as stated. conn is a datastructure and indeed therefore not a process.

But code on the beam cannot run outside a process. There’s always a process running the code. It could be that that you want a distinct process to the one you start out on in certain places for reasons like failure isolation and more, but you cannot run code without there being a process.

You can find the handle_info callback for the WebSock behaviour here: WebSock — WebSock v0.5.3

You can return {:push, messages(), state()} from handle_info to send a message to the websocket client in response to having handled a message. WebSock — WebSock v0.5.3

I’d strongly suggest looking at Registry to register the websock processes under a more descriptive key, which you can then use to send messages to it. This is also what is used within phoenix pubsub and further their channels implementation as described here: Matrjoschka of phoenix communication | Benjamin Milde

benwilson512

benwilson512

Author of Craft GraphQL APIs in Elixir with Absinthe

Yeah to elaborate on what @LostKobrakai said, the loop I described in the other thread is happening in a process, and that process is NOT the supervisor. It’s the process spawned by the supervisor but it is it’s own process. When you do self() you get the process ID of that specific process.

Notably you’re still using an abstraction here. If you want to actually do this with 100% built in Elixir you’d want something like Playing with Sockets and Processes in Elixir which is using a manual loop and direct management of the :gen_tcp library.

And this is where the history that @LostKobrakai provided I think should explain a lot of the educational material you’re finding. Phoenix was first published in 2013, built on the more “lower level” websocket support provided by the erlang library cowboy. As essentially “first to the field” for websocket support in Elixir, Phoenix has been the way to do websockets for a decade now. If people needed a more raw websocket handling experience Phoenix simply grew to support more control within its framework.

More recently as more modern and Elixir influenced alternatives to cowboy have been developed there’s also been an effort to create some more “mid level” websocket abstractions, but these are actually the new comers to the field.

Process Registration and PubSub

Switching topics, the main architectural piece that you’re missing here is the idea of a process registry, and the pattern of using this registry to “publish” messages out to clients. At a high level, your websocket process should be using eg Registry — Elixir v1.19.5 to give your process a “name” like a user or client ID. Then from some other part of the system if you want to send that user or client a message, you use the registry to lookup the PID of any processes associated with that user / client ID and then send the message to those processes.

If you are not sure about how to “name” your processes I want to note that this is a problem you’d face no matter what system or language you’re using. If you have 10 websocket clients connected and you want to send one of them a message from the command line or from one client to another, you need SOME way to identify and therefore distinguish one client from another.

sodapopcan

sodapopcan

It really seems as though you are still conflating modules and processes. Modules are literally just static bags of functions (this isn’t strictly true as they can also carry metadata in the form of persisted attributes, but these are also static). use GenServer doesn’t turn a module into a process, it just adds some callbacks that the GenServer module is aware of. self() returns the pid of whatever process a function is being run in, it has nothing to do with the module itself.

This might make it a bit clearer (if not, sorry I’m trying my best, lol):

# m.ex

defmodule A do
  def hi, do: self()
end

defmodule B do
  def hi, do: self()
end

defmodule Server do
  def start_link, do: spawn_link(&loop/0)

  defp loop do
    receive do
      mod -> apply(mod, :hi, []) |> IO.inspect()
    end

    loop()
  end
end

Here we have a Server that doesn’t hold any state. It receives a module name as a message and assumes it has a function called hi/0 and calls it. A and B both implement hi/0 which just returns self(). Both A.hi() and B.hi() will return the same result when called in the process running the Server.start_link function. Notably, A and B don’t implement any sort of “process behaviour.”

iex(1)> c "m.ex"
[A, B, Server]
iex(2)> pid = Server.start_link()
#PID<0.122.0>
iex(3)> send(pid, A)
#PID<0.122.0>
A
iex(4)> send(pid, B)
#PID<0.122.0>
B

As you can see, all three pids are identical despite being returned from functions in different modules.

EDIT: This is a reply to @mikejm to be clear… it sort looks like I’m responding to @benwilson512 :sweat_smile:

LostKobrakai

LostKobrakai

I also want to address this as well: Before phoenix 1.7 (the latest release at this point) websocket handling wasn’t abstracted, but handled directly through the lower level integration with cowboy. @mtrudel having built bandit was nice enough to build out all the new WebSock abstractions to make it possible to switch all of phoenix to bandit vs using cowboy as well as allowing for any future webserver implemenation to provide adapters for both http and websockets.

Hence probably all books you have would have been written before those abstractions were in place and also generally the lack of more hands on guides around it. People have been using channels for ages vs the WebSock abstraction being very new.

sodapopcan

sodapopcan

I realize too that this is poor wording. It’s really more “a Server that starts a process.” It’s confusing because people do indeed talk about modules as if they themselves are processes, but it’d be more proper to say they implement functions to be run in a process as a server.

It could also be written like this:

defmodule Server do
  def loop do
    receive do
      mod -> apply(mod, :hi, []) |> IO.inspect()
    end

    loop()
  end
end
iex(2)> pid = spawn_link(&Server.loop/0)
#PID<0.122.0>

This better shows that spawn_link isn’t connected to Server in any way. spawn_link, self, etc all come from Kernel, they aren’t “methods” on each module. The compiler essentially rewrites these calls to Kernel.spawn_link etc.

Where Next?

Popular in Questions Top

dotdotdotPaul
Okay, I'm having a heck of a time trying to figure out how to best handle the validation of belongs_to associations in Ecto. I'm sure I'...
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New
lk-geimfari
What is most correct way to open, read and parse JSON file with poison? For example if we have example.json file in root of some projec...
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
script
If I have a string “1000 cfu/ml” . I want to remove the characters and / and space . So the string is like this "1000" What is the ...
New
makeitrein
Hey all, just started picking up Elixir last week and am writing a scraper as a learning project. Baby step #1 is extracting the number ...
New
chensan
I have a User schema with a :from_id field set to type :string: defmodule TweetBot.Repo.Migrations.CreateUsers do use Ecto.Migration ...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New
joeerl
Hello again - after a longish gap I’ve decided I really must dig into Elixir and see what’s been happening here - so I have a few questio...
New

Other popular topics Top

JakeBecker
TL;DR: I’ve just released an implementation of Microsoft’s IDE-independent Language Server Protocol for Elixir. It adds language support ...
1140 51847 244
New
Jim
As a follow up to my earlier question: I have the code compiling and running but not getting a successful login from the rest server. ...
New
fireproofsocks
I’m working on defining a simple Ecto schema for a table (in PostGres), but I don’t see where I can define a column as NOT NULL. Conside...
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
myronmarston
The Elixir Typespec docs show the following syntax for keyword lists in typespecs: # ... | [key: type] # keyword lis...
New
mgjohns61585
Could someone help me? I'm making my first elixir program, number guessing game. I can't figure out how to convert the user's guess from ...
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
Nvim
Elixir appears to be a superior language to Python. I don’t see any advantage of Python over Elixir. Are there any?
New

We're in Beta

About us Mission Statement