Back
Mar 12, 2017

Creating a chat with Django Channels

Mikhail Andreev

Introduction

Nowadays, when all sorts of chat rooms have become extremely popular, when every second large company has launched or developed its own instant messenger, when an increase of smiles and change in the text size is considered as innovation, in the era of iMessages, Slack, Hipchat, Messager, Google Allo, Zulip, etc. I will tell you how to keep up with the trend and write your own chat, using django-channels 0.17.3, django 1.10.x, python 3.5.x.

We will take multichat as an example. This article can be considered as step-by-step instructions on creating such a chat.

Django-channels review

This package allows our application to interact with a user not only using HTTP 1.1 (request-response), but also using HTTP/2 and WebSocket.

WebSocket is designed for exchanging messages between the client and the web server in real time. You should consider it as an open channel between the client and the server, with the ability to subscribe to the events sent to it.

Chat functionality

Let's define the main functions, which our chat app will have.

User authorization will be done on the basis of the built-in django.contrib.auth. Django-channels support cookies-based user authentication, this is what we need.

Separate chat rooms with the possibility of adding, deleting and editing via standard Django admin control panel.

Message delivery in real time, which we will do using django-channels and websocket.

Installation and configuration of the environment

We will use python 3.5.x, but python 2.7 is also supported.

Virtualenv and django project

false

Redis

- Hey, wait a minute! Why do we need Redis? We came here to read about websockets. - The typical reader.

- Django-channels are not only about websockets. - Anonymous

Django-channels consist of three components:

  • Interface servers - which handle requests (WSGI and ASGI) and put them in a queue.
  • Channels- is a first-in first-out queue for messages that need to be stored in data structures such as Redis, IPC etc. The message can be delivered only to one listener, message delivery order depends only on its getting into the queue.
  • Workers - monitor channels and handle requests.

For more detailed information, see documentation;

Note that such architecture can scale horizontally the app. You can vary the number of processes for each component and run them on separate servers.

Let’s configure django-channels so that it will use the above-mentioned redis as the channel storage.

false

We will add the following lines to multichat/settings.py:

false

We will create multichat/routing.py (next to settings.py) and add a basic message handler. For example we will display text of a received message in the console. The file will look like this:

false

Also, it was needed to initialize the django-channels routing.

Now let’s make a digression and check whether it all works.

For this we will need any websocket client.

false

Now let’s run the Django web server ./manage.py runserver, as well as open the interactive session interpreter in a parallel window:

First of all, let’s join our websocket, django-channels runs it by default on the same the url, but the protocol ws/wss is used depending on whether encryption is used or not, so in our case the address of the connection is ws://localhost:8000. We are connecting:

false

If we take a look at our web server logs, we'll see something like this:

false

So it means we have successfully connected. Now, let’s try to send a message:

false

As we can see, the message has been duplicated in our server log:

false

Profit!

Authorization

Let’s create a template for our future application:

false

Now let’s add it to the INSTALLED_APPS:

false

Then, add a base template and some styles.

Create static/main.css style file with the following content.

It is now up to the basic template templates/base.html:

false

We will expand it later.

Let’s work with a homepage, we will create a template templates/index.html for it:

false

But so that we could get to it, let's add a corresponding view in the chat/views.py, here's what you should get in the end:

false

Now let's add it to the multichat/urls.py:

false

We have a homepage. For logout and login we use the built-in view from contrib.auth:

false

By default, django.contrib.auth.views.login view uses a template registration/login.html, which we have not set yet. We’ll create templates/registration/login.html with the following content:

false

 

Now, to enable the user to get on the home page after logging in, we need to add the following line in the multichat/settings.py:

LOGIN_REDIRECT_URL = "/"

The same for logout:

LOGOUT_REDIRECT_URL = "/"

That’s all. Let's go to the main page and verify our authorization, enter your user data which was created earlier:

django-channels-multichat-login.png

Voila:

django-channels-multichat-rooms.png

Rooms

Let's talk about rooms, each room can have its own name, also it would be nice to be able to create separate rooms where only superusers can enter. Let’s add a ‘Room’ class to chat/models.py:

false

Create and run the migrations:

false

It would be nice to provide the possibility of adding them, for this we can certainly create a separate page, provide access rights etc., but it is easier to just use django admin control panel. Add the following line in the chat/admin.py:

false

Voila, now we can create, edit and delete via django standard admin control panel.

Let’s add displaying of registered rooms list on our home page, for this we add a queryset with rooms to the home page context:

false

Then we can display them in the templates/index.html (do not forget to envisage the absence of rooms, in this case you can simply ask the user to add them with reference to django):

false

Event handlers

The handlers may include such events as: a user connection or disconnection from the websocket, data sending to the websocket. We also need to keep track of when the user logs into the chat, disconnects from it and sends a message.

User connects to the websocket

When a user connects to the websocket, first of all we have to identify it. In this case there is a decorator channel_session_user_from_http in the django-channels arsenal, but how does it work? This decorator takes a user from http session and inserts it into the channel-base session (which uses the django standard SESSION_ENGINE documentation), after which it will be available in the message.user attribute.

We must also take care of creating a channel_session rooms for the user. It is necessary to be able to subscribe the user to the events in a particular room.

To do this, we’ll create a chat/consumers.py file and declare ws_connect function in it:

false

User disconnects from the websocket

When a user disconnects from the websocket, we need to clean up his opening session. To identify the user, we’ll need the channel_session_user which adds user instance to the received message, based on the user ID from the channel session. Unlike channel_session_user_from_http, it turns on channel session implicitly.

In order to provide a possibility to send a message to all users who are in a particular room, django-channels provides an excellent opportunity to group channels (more details here).

Let’s add the models Room method websocket_group, which will return a unique channels.Group for each room through id:

false

Now let’s add the following lines into chat/consumers.py:

false

Message Processing

For comfortable work we will send the data in the form of json, so when you receive a message, we will parse json, and send the received data into the channel.

Let’s add the following lines into chat/consumers.py:

false

User logged into the chat

One of the important features of our app is the ability to enter a specific chat room, but what will happen if an unauthorized user tries to do it? and what if the requested room does not exist? or if the user has no rights to enter this room at all? That's right, an error will occur! And in the first two cases, it will clearly be thrown at the system level, and in the third case the user will likely get to the secret room. To prevent such a trouble from happening, you need to add error handling.

Let's create chat/exceptions.py file and declare ClientError class in it:

false

In the future, if any error occurs, when using this class we will notify the client-side. Now let's create chat/utils.py and declare decorators needed for error handling there.

false

Since we want to notify users about something, let's add some gradation of messages by a level of importance. To do this, let's create a chat/settings.py, where we define the types of messages:

false

Also, we will need a simple way to deliver a message of any content to all users connected to this room. To do this, let’s add the send_message method the Room class, after that it will be as follows:

false

Add the following lines into chat/consumers.py:

false

User left the chat

Let’s add the following lines into chat/consumers.py:

false

User sent a message

Let’s add the following lines to chat/consumers.py:

false

Connecting handlers

After previous actions our chat/consumers.py should look as follows:

false

Let’s create chat/routing.py and add declared signals there:

false

And now we’ll add routing that we have created in the main multichat/routing.py, after which it will look as follows multichat/routing.py:

false

Chat!

We have written a lot of things on the backend, but where is the chat itself? So, let's create a front end part.

Firstly, we’ll create static/main.js, where we will be coding. Let’s connect it, also we will need jquery and any library to reconnect to websocket. In this example we will use ReconnectingWebSocket. We open our templates/index.html and append the following lines:

false

And also do not forget to create several rooms, I'm sure that if you are reading this article, then you know how to use Django admin panel for sure :)

Now it is possible to code a chat, as we know, only the authorized user can get into this page, so we will not make any further checks in this regard (but if you decide to use this code in production, the additional check on the front end will be useful).

Connecting to the socket

Why do we use ReconnectingWebSocket? Everything is very simple. With this library, WebSocket will be automatically re-connected after calling the onclose event. I just want to note that reconnection does not start immediately, but with a small delay that can be adjusted by transmitting value in milliseconds to the designer of the reconnectInterval class. If you want, you can specify timeoutInterval. But will it work if we lose the connection? I don’t think so. :)

Let’s connect to our socket:

false

Stop, stop, stop. This code will be difficult to debug, let's display in the event console, such as the attempt to connect, the client is successfully connected and disconnected from the socket. WebSocket provides the ability to define onopen and onclose methods, which will be called when the connection is successful, or when there is disconnection from the server. As a result, we get something like this:

false

Let's open the console, reload the page and see what we've got. If you see the following lines, then life is good:

false

Connecting and disconnecting from the rooms

It's time to add the possibility to connect and disconnect from a particular room. We do not want to bother with page making, this article has purely training nature, so we will add the necessary elements directly from js. Is it good or bad? Within this training material - it is acceptable, especially when reading these lines, you understand that we are sorry for what we’ve done.

And so, as the WebSocket provides duplex communication, we can send data to a server, socket.send method is used for it. This method accepts any string and sends it through the WebSocket to the server. As you can remember, our message handler expects to receive JSON - let’s not disappoint it, we will wrap a dictionary with the data in JSON.stringify.

Let’s add the following code to the static/main.js:

false

Let's take a look at our Code carefully and think what we are lack of. There is no processing of responses from the server. It will help us in the onmessage event, whenever any kind of information is received, the socket.onmessage method will be called.

As a parameter it receives a message, and as you can remember, we expect to get valid json which we have to parse. Also, we should take into account the processing errors from the server, in this training material we will just show an error as an alert for a user. So, let’s add the following code:

false

You have the right to fully control the data returned in the response, see message.reply_channel.send in chat/consumers.py.

Let's check what we've got. Make sure that the console is open, refresh the page and try clicking once on the room button to connect to it and disconnect from it. Everything is fine, if you see the following lines in the console:

false

Sending and receiving messages

Our work with the chat proceeds, there is not much left to do - we will add the possibility to send and receive messages. We’ll begin with sending of course.

As you can see in the previous chapter, when we get the information from the server about the successful connection to the room, we add a form with an input field of the message text and a button to send it in our template. Now let's hung the submit event handler on it, with which we will send the text input field entered in WebSocket. Add the following code:

false

I think that if you've read up to this point, then the code seems rather obvious and there is no need in further explanation.

We need to display messages received from users who are with us in the same room. Let’s renew our onmessage:

false

The final static/main.js will look like this:

false

In it we are trying to define a user’s message, and depending on the type, display the message.

Well, that's all, our chat is ready and it even can be used. :)

All code shown in this tutorial material was taken from here, it has only minor cosmetic changes.

Also you can find it in our repository.

Conclusion

The django-channels framework is designed to simplify the developer's life, bringing the WebSockets and HTTP2 support in the world of simple and ordinary HTTP 1.1. Using this library allows to standardize the communication with WebSocket, and standardization of our work is very important. Among other things, this library allows horizontal scaling, as well as does not create any problems associated with asynchrony.

More thoughts