Phoenix.socket - How to support wildcard topic

Is it possible to listen wildcard topic in a channel

# my_socket.ex
  use Phoenix.Socket

  ## Channels
  channel "*", AbcWeb.MyChannel  #something of this sort?

Welcome to the forum!

Yes this is definitely supported by Phoenix Socket/Channel and is the same format you use. You do need to ensure that the wildcard is at the end of the string. So "users:*" is supported but "users:*:widgets" is not supported. Here’s a toy example of how you might do this.

socket.ex

  channel "wild:*", HelloSocketsWeb.WildcardChannel
wildcard_channel.ex

  def join("wild:" <> numbers, _payload, socket) do
    if numbers_correct?(numbers) do
      {:ok, socket}
    else
      {:error, %{}}
    end
  end
2 Likes

Also, from the official docs:

https://hexdocs.pm/phoenix/channels.html#channel-routes

The star character * acts as a wildcard matcher, so in the following example route, requests for room:lobby and room:123 would both be dispatched to the RoomChannel .

channel "room:*", HelloWeb.RoomChannel

In the above cases

"topic:*"

only wildcard subtopic is supported right?
Is it possible to not consider the topic at all? All the socket connection at an endpoint handled by MyChannel without considering what topic/subtopic.

It would be like having only one channel… Which could be equivalent to any:* if You use only this topic.

The first part of topic acts like the routing part.

What usecase do You want to solve?

You can do "*". I think it’s useful to think of this in terms of Elixir pattern matching. You can write a string pattern match like:

my_string = "test:1"
# my_string == "test:1"

"test:" <> number = "test:1"
# number == "1"

Either approach works. You can not do id <> ":test" = "1:test" in Elixir—the compiler will throw an error. Channels uses pattern matching and follows these same rules.

1 Like

I have an IOT device that would open a socket connection and start sending data. I really dont have control over how this device send data.

I even have a feeling that Phoenix.Socket is not the right solution for this use case. Is there a different way to allow IOT devices to establish socket connection with elixir applications and let them start pushing data?

Yeah Phoenix Sockets are not the right answer here, even if you have a wildcard topic there is still a protocol channel clients are expected to follow. It sounds like you want to do a custom raw websocket connection. I found Phx_raws - Raw websocket on top of Phoenix not sure if it’s still up to date though.

3 Likes

Thanks, let me look into it. I also found GitHub - meh/elixir-socket: Socket wrapping for Elixir.

I would suggest trying to stick to cowboy as the WebSocket server if possible. It’s super stable and you’ll get more support for it if you run into issues (because Phoenix uses it). Ben’s link to phx_raws looks promising.

3 Likes

This might help You if You go the cowboy way…

https://marcelog.github.io/articles/erlang_websocket_server_cowboy_tutorial.html

3 Likes

update: phx_raw doesnt wotk. The Author himself have told that in one of the github issues.

I also tried it, just to make sure. Phoenix tries to handle all the requests and mess up with phx_raw.

Planning to look at custom dispatch…

elixir-socket is for TCP (or UDP) sockets, not websockets. Which protocol does your device talk ? TCP or websockets ?

1 Like

:wave:

I have an IOT device that would open a socket connection and start sending data. I really dont have control over how this device send data.

I used ranch for an iot project in the past. The devices were sending data over tcp, it worked roughly like this:

lib/iot_device_server.ex

defmodule IOTDeviceServer do
  @behaviour :ranch_protocol

  @impl true
  def start_link(ref, _socket, transport, opts) do
    pid = :proc_lib.spawn_link(__MODULE__, :init, [ref, transport, opts])
    {:ok, pid}
  end

  @doc false
  def init(ref, transport, _opts) do
    {:ok, socket} = :ranch.handshake(ref)
    __MODULE__.loop(socket, transport)
  end

  @doc false
  def loop(socket, transport) do
    case transport.recv(socket, 0, 5000) do
      {:ok, data} ->
        case handle_in(data) do
          {:reply, reply} ->
            transport.send(socket, reply)

          :noreply ->
            :ok
        end

        loop(socket, transport)

      _ ->
        :ok = transport.close(socket)
    end
  end

  @spec handle_in(binary) :: {:reply, iodata} | :noreply
  defp handle_in(<<...some markers..., _rest::bytes>> = binary_packet) do
    {:ok, packet, _rest} = IOTPacket.decode(binary_packet)
    # :ok = publish(packet) then it was published to a queue
    :noreply
  end
end

lib/application.ex

defmodule IOTDeviceServer.Application do
  @moduledoc false
  use Application

  def start(_type, _args) do
    config = Application.get_all_env(:iot_device_server)

    _children = [
        :ranch.child_spec(IOTDeviceServer, :ranch_tcp, [port: config[:port]], IOTDeviceServer, [])
    ]

    opts = [strategy: :one_for_one, name: IOTDeviceServer.Supervisor]
    Supervisor.start_link(children, opts)
  end
end
1 Like

You are right. My device use ws. What do you think about custom dispatch as a solution?

Well if you need to use Phoenix then you have no choice but use custom dispatch.

Now, you could also just use cowboy and not Phoenix at all.