Skip to content

karlseguin/exws_channels

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

An ETS-based (in memory) Channel (aka room) implementation for ExWs.

Example

Below is a sample Handler that accepts JSON messages and allows users to join, leave and send messages to any channel.

Note that the pid_socket/0 function is a special function available within your handler. Yes, a nicer API would be great (like just being able to call join(channel_id)), but this way the ExWs library is fully decoupled from the ExWsChannels library. (pid_socket/0 currently returns a tuple of the handler pid, and the socket, though this could change in the future).

defmodule YourApp.YourWSHandler do
  use ExWs.Handler

  defp message(data, state) do
    case Jason.decode(data) do
      {:ok, data} -> process(data)
      _ -> close(3000, "invalid payload")
    end
  end
  
  defp process(%{"join" => channel}) do
    ExWsChannels.join(channel, pid_socket())
    # optionally, ack the join
    # write(Jason.encode!(%{joined: channel}))
  end
  
  defp process(%{"leave" => channel}) do
    ExWsChannels.leave(channel, pid_socket())
    # optionally, ack the leave
    # write(Jason.encode!(%{left: channel}))
  end

  defp process(%{"say" => message, "to" => channel}) do
    case ExWsChannels.member?(channel, pid_socket()) do
      true -> 
        # broadcast/2 can be called from anywhere, not just your handler
        ExWsChannels.broadcast(channel, message)
      false -> 
        # or maybe just respond with an error message
        close(3000, "cannot send to channel")  
    end
  end
end

Usage

Follow the directions for installing and configuring ExWs as usual. In addition, add the following dependency to your project:

{:exms_channels, "~> 0.0.1"}

And add ExWsChannels.Supervisor to your supervisor tree. This should likely be added BEFORE the ExWs.Supervisor:

children = [
  # ...
  ExWsChannels.Supervisor,
  {ExWs.Supervisor, [port: 4545, handler: YourApp.YourWSHandler]}
]

## Joining a Channel
From within your handler, call `ExWsChannels.join(CHANNEL_ID, pid_socket())` to have the user join a channel named `CHANNEL_ID`. In most cases `CHANNEL_ID` will be a string or maybe an atom (it can be anything, including a mix of types).

The special `pid_socket()` function is available from within your handler.

Joining the same channel multiple times is a no-op.

When joining a channel, you can specify a 3rd optional parameter which is arbitrary data to associate with the user. This data should be kept small (like a user_id and user_name, perhaps.) It is exposed in the reduce/3 function.

## Leave one or all Channels
From within your handler, call `ExWsChannels.leave/1` to leave all channels, or `ExWsChannels.leave/2` to leave a specific channel:

```elixir
ExWsChannels.leave(pid_socket())
ExWsChannels.leave(CHANNEL_ID, pid_socket())

Leaving a channel that the user isn't in is safe.

Channel Membership

Use ExWsChannels.member?(CHANNEL_ID, pid_socket()) from within your handler to determine if the user is in the channel. Not that you can safely join the same channel multiple times without having to call member?/2 first.

User List

You can reduce over a list of channel members using ExWsChannels.reduce/3:

ExWsChannels.reduce(channel, %{}, fn {pid, socket, meta},  ->
  #TODO
end)

pid is the handler process id. socket is the tcp socket (remember, it's safe to write to the socket from different processes!). meta is the data, if any, that was used when join/3 was called (or nil if join/2 was used).

Write to a Channel

You can call ExWsChannels.broadcast(CHANNEL_ID, MESSAGE) from anywhere in your code. This does not have to be called from within your handler (but it can be).

The broadcast/2 function can be optimized like the write/1 function.

Advanced Configuration

A pool or writers is created on startup and used when broadcasting messages to channels. There are a few ways this can be tweaked.

The first is by controlling the size of the pool and how much it can grow to. By default, the pool size is 10, and it can grow by 5x.

When starting ExWsChannels.Supervisor, you can specify the :writers and :max_writers options:

children = [
  # ...
  {ExWsChannels.Supervisor, [writers: 20, max_writers: 100]},
  {ExWs.Supervisor, [port: 4545, handler: YourApp.YourWSHandler]}
]

Having more writers means that separate broadcasts (either to the same channel or to different channels) aren't going to block waiting for a writer to become available. However, what about individual broadcasts to very large channels? By default, a single writer will iterate through all users and send the message.

To help deal with very large channels, channels can be "sharded". Internally, a channel with 4 shards will be split into 4 groups of users, and broadcasts to the channel will be able to use 4 writers. To enable sharding, you need to creating a sharding module:

defmodule YourWSHandler.ChannelSharder do
  # the "general" channel is very popular, so we want more shards for it
  def shards("general"), do: 16
  def shards(_), do: 1
end

Next add the configuration to point to your sharder (in dev.exs and/or prod.exs):

config :exws_channels,
  sharder: YourWSHandler.ChannelSharder

The built-in default sharder always returns a shard count of 1. While the implementation of your shards/1 can be anything, do note that it can be called often, so don't make it too expensive.

Besides defining the sharding logic, there should be no other changes to your code. You still broadcast and join the channel using the same CHANNEL_ID.

Misc

Since this uses ETS, it might not seem to fit within an HA/multi-server setup. It's true that, on its own, this won't be enough, but it can act as a foundation. There are a lot of ways to implement channels over a cluster of servers and for keeping them in sync, and it really depends on what you're needs are. Are you talking about 2 or 200 servers? Will you let clients connect to any server, or will you direct them to specific ones? Do you need other consistent data (like a chat history)?

Also, while I like the ETS implementation for most cases, I do believe that in some cases, GenServers with a DynamicSupervisor might work best. Specifically, if you have complex channel logic or a lot of channel state (like a history), GenServers might be a better fit.

Broadcasting to large channels (say 50K+) will probably also necessitate more consideration, such as sharded GenServers, to scale broadcasts and minimize copying of data.

About

Channel (room) implementation for ExWs (elixir websocket server)

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published