Lesson 18

Channels & websockets

5

Although Phoenix is great at many things, handling real-time events is where the framework really shines! Dealing with websockets has traditionally been quite cumbersome for web developers, but Erlang's runtime environment combined with Elixir's ease of use and Phoenix's abstractions make it a breeze. Without further ado, let's dive into it!


In order to be able to test this functionality, we will open a new browser application (so if you're already logged-in on Chrome, use Firefox for example), and create a brand new user. Personally I'm going to use Safari and call the new user "alice". You should then log in and create a new room where both of your users can communicate with each other.


Here's my setup with Chrome on the left side and Safari on the right side.


We now want to be able to send a message from our user to "alice", and have that message instantly appear in Alice's window, without her having to manually fetch it!

Creating and connecting to a channel

Phoenix introduces the concept of channels – a websocket endpoint that clients can connect to, in order to send and receive messages. The best way to understand it is through an example. Let's generate a new channel for our rooms!

You should now have a file at lib/messengyr_web/channels/room_channel.ex. Let's open it and take a look. As you can see, there's a default room called lobby that anyone can join:

lib/messengyr/web/channels/room_channel.ex

If you scroll down the file, you'll see that authorized?/1 is a private function that always returns true, so joining this room will currently always work without any problems.


We also list all of our "channel routes" in the user_socket.ex-file. Here we can specify what the endpoint that connects to the RoomChannel should be. In this case, we'll just un-comment the line that Phoenix already has for us near the top of the file, which says that all routes that start with the prefix room: should go to the room channel:

lib/messengyr_web/channels/user_socket.ex

Let's now try to connect to our "lobby" room from React. Phoenix has actually already generated a JavaScript file that helps us connect to our rooms, at assets/js/socket.js. If you open this file, you'll find plenty of comments explaining how to use it. Delete those comments so that we can get a better overview of what the file is doing. We'll also delete the part that connects to the channel "topic:subtopic", since that endpoint doesn't exist. You final file should look like this:

assets/js/socket.js

Next, we'll go into menu-container.js and import this socket file at the top. Using the socket object, we can create a channel variable, which specifies which Phoenix channel we want to connect to (in this case, "room.lobby"), and finally we'll join the channel:

assets/js/components/menu-container.js

And that's all you need! If you refresh the browser, you should now see this message in your JavaScript log...



...and this in your Phoenix log:



Sending a socket message

Now that we're connected to the socket, we can try to send a message to it! If we open room_channel.ex again, you'll see that it already has some default handle_in/3-functions. One of them pattern matches against the string "shout" – that function takes whatever message it receives and broadcasts it to all the users that are connected to the room.


If invoke this "shout"-function from JavaScript, we simply use the channel variable that we created earlier and push the string "shout". We also want to listen to the channel so that we can log something when we receive the "shout":

assets/js/components/menu-container.js

Again, refresh the page and you should see the message in your JavaScript console:


As you probably expected.


This might not seem very advanced, but the cool part is that the messages aren't necessarily tied to your own user. Any user connected to the "room:lobby" channel can broadcast the "shout" message, and it will still be intercepted by you!


We can try this out by opening the other browser where Alice is logged-in (Safari in my case), and hit refresh multiple times. That way, Alice will send out multiple shouts, and you'll notice that your own user will log every single one!

Authenticating sockets

Before we start sending and receiving more messages, we want to make sure that our websocket endpoint requires authentication, so that only logged-in users can use it. For this, we'll use Guardian and our JWT-token again.


If you re-open the autogenerated socket.js-file, you'll see that Phoenix is trying to guide us in the right direction by sending a window.userToken variable under the key "token" when it connects to the socket endpoint. This userToken is undefined however, but we do have the global variable jwtToken which we used in our Fetch requests earlier. Let's alter the file so that it uses that instead:

assets/js/socket.js

Now, back in user_socket.ex, we're going to use the resource_from_claims/1-function from our Messengyr.Auth.Guardian module to decode the token and retrieve the user that belongs to it. We'll do this in the user socket's connect/2-function, which is called as soon as the user attempts to connect to the socket. After we've retrieved the user, we'll assign it to the socket variable so that we can easily read it later. If the user isn't logged in, or we cannot retrieve the user for whatever reason, we'll return an :error.

lib/messengyr_web/channels/user_socket.ex

Here you can also clearly see the benefit of using Elixir's with – it allows us to easily pattern match multiple conditionals one after the other. If we wanted to write the same logic using `case` statements, things would get deeply nested:

We'll also define a second connect/2-function that doesn't pattern match against guardianToken. If the user tries to connect without that parameter, it should return an :error instantly:

lib/messengyr_web/channels/user_socket.ex

If you refresh the page, you should be able to use the socket just like before. However, you can try experimenting with the socket.js-file and comment out the guardianToken line. You'll notice that, if you don't send the token, or if you change it to something that's not valid, you won't be able to connect to the socket (which is the expected behaviour)!


If we send a valid "guardianToken" everything is fine...



...but if we don't, we get an error!


Connecting to a specific room

Alright, enough with the "shouting" in the "lobby"! What we really want to do is to connect to each room where the user is a member. Instead of connecting to "room:lobby", a user could then connect to the channels "room:1" and "room:2", provided that they are a member of the rooms with ID 1 and 2.


Let's start with the room_channel.ex-file. We'll remove all the functions that are currently in it, and instead have a single join/3-function that connects to a room with a specific ID:

lib/messengyr_web/channels/room_channel.ex

Next, we'll go to menu-container.js and delete the code that's related to joining the "lobby" channel. Instead, we'll create a getRoomChannel-function that takes a roomId, connects to the channel of that room, and returns the channel:

assets/js/components/menu-container.js

Finally, we'll loop through all of our rooms (right after fetching them with AJAX in componentDidMount) and add a channel to each room object before putting them in our Redux store:

assets/js/components/menu-container.js

Refresh the page, and you should now see your user join only the rooms that you're a member of in the console:



Handling errors

What we have is pretty good, however, we should handle cases where the user tries to connect to a room that doesn't exist. Also, what happens if another logged-in user manually connects to the channel "room:10" to spy on the conversation I'm having with Alice? We obviously can't let that happen, so we need the join/3-function in room_channel.ex to make sure that the room exists, and only let in the members of that specific room.


To do that, we'll first need two new functions in our chat context: get_room/1 and room_has_user?/2:

lib/messengyr/chat/chat.ex

Using these new functions, we can now update the room channel's join/3-function:

lib/messengyr_web/channels/room_channel.ex

There we go! Now it should be safe from eavesdroppers. To test that we cannot connect to just any random room, you can try to call the getRoomChannel-function from React with a room ID that doesn't exist (like 999):

assets/js/components/menu-container.js

You should then see an error message for that room in your log:



Now you know how to connect to a websocket in Phoenix and perform some basic authentication in your channels! In the next chapter, we'll make our two users communicate with each other through this channel.

Comments

profile/avatar/default
Vasileios Ntarlagiannis

"Messengyr.RoomChannel" must be "Messengyr.Web.RoomChannel"

Tristan Edwards

@bdarla: Fixed πŸ‘

Peter Marreck

The code to import that file is not included below. It is:

Peter Marreck

import socket from '../socket';