Eric Oestrich
20 Aug 2018
•
2 min read
For my side project Gossip I wanted to have a websocket connection for non-Phoenix connections. I did this by going straight to Cowboy and using a handler at that level. This explains how I did this for Gossip.
Gossip is a cross game chat service for MUDs, check it out.
The full websocket handler is here on GitHub.
defmodule Web.SocketHandler do
@behaviour :cowboy_websocket_handler
def init(_, _req, _opts) do
{:upgrade, :protocol, :cowboy_websocket}
end
def websocket_init(_type, req, _opts) do
Logger.info("Socket starting")
{:ok, req, %State{status: "inactive"}}
end
def websocket_handle({:text, message}, req, state) do
with {:ok, message} <- Poison.decode(message),
{:ok, response, state} <- Implementation.receive(state, message) do
{:reply, {:text, Poison.encode!(response)}, req, state}
else
{:ok, state} ->
{:ok, req, state}
_ ->
{:reply, {:text, Poison.encode!(%{status: "unknown"})}, req, state}
end
end
end
This is a snipped version of the full file, but the basics are here. This shows the websocket upgrading, a cowboy and websockets requirement. The init
function upgrades to websockets and the websocket_init function is called after the upgrade.
The other function shown is when a new message is received. The message is JSON (or should be), so it gets parsed and then run through an implementation module elsewhere. Depending on the response from that submodule, different responses will get send back.
There are a few other cool things in the real module, so I encourage you to check it out.
One thing I had to add that I thought was included as part of the cowboy handler was a pong response to a client side ping. This actually crashed the websocket process a few times so I needed to add this as per the websocket spec:
def websocket_handle({:ping, message}, req, state) do
{:reply, {:pong, message}, req, state}
end
Since we're using a lower level websocket (than Phoenix) we have to manually set up the cowboy dispatcher. This configuration shows the cowboy websocket handler along with a separate Phoenix channel, since I want to have both options.
If you go this route, you need to manually specify any Phoenix channels from here on out.
config :gossip, Web.Endpoint,
http: [dispatch: [
{:_, [
{"/socket", Web.SocketHandler, []},
{"/chat/websocket", Phoenix.Endpoint.CowboyWebSocket, {Phoenix.Transports.WebSocket, {Web.Endpoint, Web.UserSocket, :websocket}}},
{:_, Plug.Adapters.Cowboy.Handler, {Web.Endpoint, []}}
]}
]]
Setting up your own lower level websocket in Elixir/Phoenix turned out to be pretty simple. I am happy I went this route so I didn't have to worry about forcing the higher level Phoenix channels protocol on top of external clients.
If you're curious about more of the events Gossip sends, the docs are available here.
Ground Floor, Verse Building, 18 Brunswick Place, London, N1 6DZ
108 E 16th Street, New York, NY 10003
Join over 111,000 others and get access to exclusive content, job opportunities and more!