Django and real time applications
Channels allows you to reduce complexity, learn more
The web has gone under a series of transformations and the good old request response mechanism is no longer present as the applications get more complex. This means that more tools are required to develop them, and it can become interesting for developers learning how to use them, but on the other hand, it makes everything more complex.
Channels - Concepts
There are many frameworks to develop real time applications. In my talk “Building real time applications” at DjangoCon Europe I talked about how to do it with Django and in particular with Channels, a framework to build up asynchronous applications that allows to go beyond the typical request-response mechanism of HTTP.
Channels allows to use various asynchronous protocols. In this blog post, we will refer to websockets, a web technology which provides full duplex communication channels, that send and receive messages bidirectionally in a simultaneous way. It’s one of the most widely used technologies because the server does not have to be requested continuously to provide contents to the browser.
Compared to the previous version, 2.0 Channels release brings a big change. Channels 1.0 allowed people to write synchronous code only, hiding the asynchronous one, while the new release has got an asynchronous interface as well so that the developer is free to decide which approach to use.
Channels uses Asynchronous Server Gateway Interface protocol (ASGI) and it implements it at each level: each part of Channels is an ASGI application that is capable of operating autonomously. This structure makes the software modular, allowing the developer to build his/her own pipeline.
The first element that have to be set is the protocol server, a part of the software which interacts with the network, as it intercepts calls and translates everything into ASGI to the application. With websocket and HTTP we can use Daphne. Once the connection is established, a scope is created. It collects all the data about the connection itself and it has to be routed through the routing. While scope makes sure the connection and the application instance communicate, the routing maps the messages to the consumer.
In Channels, consumers are high-level abstractions, classes which manage the events. Generally speaking, consumers are protocol independent, while websocket consumers, which are a specialization linked to websockets, are an exception. In this post blog we will dive into what can be done with websocket consumers.
Where does the app name come from? Channels is, ironically, a “minor” element, since it’s a mechanism that has the function of conveying messages through the instances of various consumers.
THE DEMO I DEVELOPED
To demonstrate the potential of Channels I developed a web application which allows to perform these actions:
- Counting active users
users have the possibility to know if there are other users that accessed the dashboard - Concurrency checking
users can check if other users accessed or are modifying a given resource - Have all useful notifications on the browser
all the actions performed by users are notified to the browser
WHICH ARE THE ASGI APPS THAT YOU NEED TO BUILD UP
To create this application I worked on three levels:
- Channel layers configuration
- routing
- consumers
CHANNEL LAYERS
ASGI_APPLICATION = 'dashboard.routing.application'
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
'hosts': [('localhost', 6379)],
},
},
}
Channel layers configuration allows the communication between the application instances and its function is to communicate to the server where to send messages. Channels provides a default storage that has to be configured. The only compulsory requirement to configure within channel layers is which ASGI application to use. In this specific case, the application needed is the routing.
ROUTING
For this application I set three routing that run in series.
# equivalent to my_project.urls
# tipically used to include application routing
application = ProtocolTypeRouter({
'websocket': AuthMiddlewareStack(
URLRouter([
path('status/', documents_routing),
])
),
})
The first given element is protocol routing and, in this specific case, we’ll use websocket only. And after that the middleware, which “migrates” Django authentication data on the scope.
Middleware is the place where to input the routers, which will map websocket call paths to consumers. Routers are specific for the protocol you choose to use and among them there is URL router, specific for websocket, which routes messages according to the given path.
Finally, the application routing has been set, this too specific for websocket, which has the function of connecting the given path to a single consumer.
channel_routing = URLRouter([
path('users/', UserCounterConsumer),
path('documents/', DocumentListConsumer),
path('document/<str:slug>/<str:phase>/', DocumentDetailConsum
])
CONSUMER
Consumers are organized in a class hierarchy that manages the application on more levels.
Taking in exam a websocket consumer there are three main events that have to be set and managed:
• connection
• disconnection
• reception (of a message)
In the example we are analyzing I created a consumer that counts the number of users connecting to the application. How does it work?
class UserCounterConsumer(JsonWebsocketConsumer):
groups = 'users',
def connect(self):
""" Increment users on connect and notify other consumers"""
super().connect()
if self.scope['user'].is_authenticated:
increment_users(message.user)
msg = {'users': count_users(),
'type': 'users.count'}
async_to_sync(self.channel_layer.group_send)('users', msg
In order for the other users, connected to the same group, to receive the message of a new connection, routing has to route the connection to the connect method, which manages the event connect.
def users_count(self, event):
""" Notify connected user """
self.send_json(content=event['message'])
After sending the message, the application calls users.count method, which is managed by user_count. In short what is created is a custom event managed by a function with a specific name.
Within concurrency monitoring, groups are used more widely.
class DocumentListConsumer(JsonWebsocketConsumer):
@property
def groups(self):
return Document.Status.list,
def connect(self):
super(DocumentListConsumer, self).connect()
async_to_sync(self.channel_layer.group_send)(
self.slug, {
'type': 'document.status',
'message': self.get_status_packet()
})
def document_status(self, event):
self.send_json(content=event['message'])
Each document, represented by a slug, is linked to a group. When a user connects to a path his/her instance is recorded on the group with that specific slug and the counter of active users is updated.
Inshort the core of this app is represented by these lines of code
def connect():
...
async_to_sync(self.channel_layer.group_send)(
self.slug, {
...
})
def connect():
...
async_to_sync(self.channel_layer.group_send)(
self.slug, {
...
})
def connect():
...
async_to_sync(self.channel_layer.group_send)(
self.slug, {
...
})
def connect():
...
async_to_sync(self.channel_layer.group_send)(
self.slug, {
...
})
This method allows to send messages, create events that are managed by the consumers and it permits a synchronous function to interact with an asynchronous one. Connect is the item that allows to send a message on the group. In this specific case what is sent is a dictionary but you can send different kinds of messages. The only compulsory element to set is the type of message because it determines the events that can be created.
In order to make the application work an event document.status was set.
All the consumers that are connected on the slug group have to implement the method explained above.
We hope we were able to inspire you and that you’ll give Channels a chance. Stay tuned for more news about Django applications and development tips!