Adapter for Phoenix Connections

Jonathan Hockman
Brightergy Engineering
4 min readJul 29, 2016

A while back we decided to switch our old service implementation from NodeJS to Elixir. With Elixir came the wonderful web framework Phoenix which has a great websocket implementation using their own communication method called Channels. This transition led to the natural choice to move our device software from using a REST method to using websockets to talk to our service. Phoenix has its own Javascript implementation using Channels, but our device software is written in Python, so we underwent the task of writing our own Channel implementation for Python, but this can be applied to any language that you can use to connect to a websocket.

Phoenix Channels

A very quick overview of Phoenix Channels. Channels use a pub/sub pattern in which you can join (or subscribe) to a topic or topics and send messages to a topic or topics. There are 4 parts to each message:

  • topic: the topic is simply a unique name for a given pub/sub relationship
  • event: string name used to separate how to handle messages
  • payload: message data to be acted on as a JSON object
  • ref: unique id of message (optional)

We’re not going to get into how Phoenix handles Channels on the server, so for more information on that you can click here for more in-depth information on Phoenix Channels.

Joining a Topic

Before you can do anything you need to be part of a Channel which is defined by the topic. Once your websocket connection has been opened you need to send a JSON payload telling Phoenix you want to join a topic. In this example we are going to use the topic called chat:room1. To join chat:room1 it is as simple as sending:

{
'topic': 'chat:room1',
'event': 'phx_join',
'payload': {},
'ref': 'chat:room1:join'
}

Upon success Phoenix will respond with:

{
'topic': 'chat:room1',
'event': 'phx_reply',
'payload': {
'status': 'ok',
'response': {'alerts': []}
},
'ref': 'chat:room1:join'
}

If, for whatever reason, you fail to join you will receive payload status of error which will be a variation of the following based on the reason for failure:

{
'topic': 'chat:room1',
'event': 'phx_reply',
'payload': {
'status': 'error',
'response': {'reason': 'unmatched_topic'}
},
'ref': 'chat:room1:join'
}

Because the response to a join request is so generic we found it was a good idea to use the ref field, so we could look for a specific response with that ref so we could check the status for that specific join request then act accordingly. You can use whatever value you want to for the ref, but if you are going to be joining multiple channels it is a very good idea to define an easily identifiable ref. In this case you could use something uniquely generated and track in a list or intentionally specific like ‘chat:room1:join’. Phoenix doesn’t care one way or the other, so it’s really up to you.

Sending and Receiving Messages

Sending and receiving messages are simply a matter of pushing a JSON encoded string out to the websocket or parsing a JSON encoded string received by your websocket. Your own individual implementation will be determined by what language you used and the websocket implementation you are using.

To send a message simply push out a JSON encoded string with the proper topic, and event. Your payload just needs to be a a JSON object with whatever data you want to send.

For instance to send a chat message to the previously joined topic you could push this:

{
'topic': 'chat:room1',
'event': 'new_msg',
'payload': {'message': 'Hello World!'},
'ref': ''
}

The the receiving client would simply receive the same message just the way you sent it and parse the JSON. Assuming you’re using Python and the websocket calls an on_message callback when data is received it might look something like this:

def on_message(data):
topic_msg = json.loads(data)
if topic_msg['event'] == 'new_msg':
message = topic_msg['payload']['message']
...do something with the message here...

Error Handling

One of the most common errors we’ve received is the previously mentioned ‘unmatched topic’. This error can occur if the data sent has been corrupted or if you send invalid data that causes Phoenix to raise an exception. If you receive an ‘unmatched topic’ you will need to join the channel again or you will continue to receive the ‘unmatched topic’ error with each message sent until you do. Again, assuming you’re using Python and your websocket calls an on_message method when it receives data:

def on_message(data):
topic_msg = json.loads(data)
payload = topic_msg['payload']
if 'status' in payload and payload['status'] == 'error':
if payload['status']['reason'] == 'unmatched topic':
ref = "{}:join".format(topic_msg['topic'])
join_msg = {'topic': topic_msg['topic'],
'event': 'phx_join',
'payload': {},
'ref': ref}
websocket.send(json.dumps(join_msg))
else:
...handle normal message here...

That’s It

It really is that simple to work with Phoenix Channels. Simply join the topics you want to subscribe to and send and receive to your heart’s content. You can join as many topics as you want. The handling of messages is entirely up to you.

--

--