WebSocket – Opens the door to a new world

What is a WebSocket?

WebSocket is a protocol for full duplex communication over a single TCP connection. WebSocket allows the server to actively push data to the client. In the WebSocket protocol, the client browser and the server only need to complete a handshake to create a persistent connection and two-way data transfer between the browser and the server.

What does WebSocket do?

One of the most notable features of WebSocket, unlike HTTP, is that WebSocket can initiate messages by the server, which is suitable for scenarios where the browser needs to receive data changes in a timely manner. For example, when we encounter long tasks in Django, we usually use Celery to perform tasks asynchronously. So if the browser wants to get the execution status of this task, in the HTTP protocol, the browser can only continuously send requests to the server through the rotation mode to get the latest status, so sending a lot of useless requests is not only a waste of resources, but also not elegant, if you use WebSokcet to achieve the perfect

Another application scenario of WebSocket is the chat room described below. A message sent by a user (browser) needs to be received by other users (browser) in real time, which is difficult to achieve under THE HTTP protocol, but WebSocket can handle it easily based on the long connection and the feature that can actively send messages to the browser

Now that we have a look at WebSocket, how do we implement WebSocket in Django

Channels

Django doesn’t support Websockets per se, but websockets can be implemented by integrating with the Channels framework

Channels is an enhanced framework for Django projects that enables Django to support not only HTTP, but also WebSocket, MQTT, and other protocols. Channels also integrates Django’s Auth and session system for user management and authentication.

All of my code implementations below use the following Python and Django versions

  • Python = = 3.6.3
  • Django = = 2.2

Integrated Channels

I assume you have created a New Django project called WebApp with the following directory structure

project
    - webapp
        - __init__.py
        - settings.py
        - urls.py
        - wsgi.py
    - manage.py
Copy the code
  1. Install the channels
PIP install channels = = 2.1.7Copy the code
  1. Modify settings.py,
# APPS add Channels
INSTALLED_APPS = [
    'django.contrib.staticfiles'.'channels',]# specify the ASGI routing address
ASGI_APPLICATION = 'webapp.routing.application'
Copy the code

Channels runs on the ASGI protocol, whose full name is Asynchronous Server Gateway Interface. It is an asynchronous service gateway interface protocol that is different from the WSGI protocol used by Django, and it implements WebSocket

ASGI_APPLICATION specifies that the location of the primary route is application in the routing.py file under webApp

  1. Create a routing file named routing.py in the sibling of setting.py, which is similar to url.py in Django and specifies webSocket routes
from channels.routing import ProtocolTypeRouter

application = ProtocolTypeRouter({
    # temporarily empty, fill in below
})
Copy the code
  1. Running a Django project
C:\python36\python.exe D:/demo/tailf/manage.py runserver 0.0.0.0:80
Performing system checks...
Watching forfile changes with StatReloader System check identified no issues (0 silenced). April 12, Django Version 2.2, using Settings'webapp.settings'
Starting ASGI/Channels version 2.1.7 development server at http://0.0.0.0:80/
Quit the server with CTRL-BREAK.
Copy the code

If you look closely at the output above, you will see that Starting Development Server in Django startup has been changed to Starting ASGI/Channels Version 2.1.7 Development Server. This indicates that the project has been converted from the WSGI protocol used by Django to the ASGI protocol used by Channels

At this point Django has basically integrated with the Channels framework

Setting up a chat room

Channels are integrated in the project above, but no application uses it. Next, we take the example of chat room to explain the use of Channels

Suppose you’ve created an app called Chat and added it to INSTALLED_APPS in settings.py. The directory structure of the app looks something like this

chat
    - migrations
        - __init__.py
    - __init__.py
    - admin.py
    - apps.py
    - models.py
    - tests.py
    - views.py
Copy the code

Let’s build a standard Django chat page with the following code

url:

from django.urls import path
from chat.views import chat

urlpatterns = [
    path('chat', chat, name='chat-url')]Copy the code

view:

from django.shortcuts import render

def chat(request):
    return render(request, 'chat/index.html')
Copy the code

template:

{% extends "base.html" %}

{% block content %}
  <textarea class="form-control" id="chat-log" disabled rows="20"></textarea><br/>
  <input class="form-control" id="chat-message-input" type="text"/><br/>
  <input class="btn btn-success btn-block" id="chat-message-submit" type="button" value="Send"/>
{% endblock %}
Copy the code

With the code above, a simple Web chat page is built, which looks like this:

Next, we use the WebSocket protocol of Channels to realize message sending and receiving function

  1. Starting with the routing, we have created the routing file routing.py. Now fill in the contents
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
import chat.routing

application = ProtocolTypeRouter({
    'websocket': AuthMiddlewareStack(
        URLRouter(
            chat.routing.websocket_urlpatterns
        )
    ),
})
Copy the code

ProtocolTypeRouter: ASIG supports multiple protocols. In this section, you can specify routing information of a specific protocol. Only webSocket is used

AuthMiddlewareStack: Django channels encapsulates Django’s Auth module. Using this configuration, we can get user information from the consumer in the following code

def connect(self):
    self.user = self.scope["user"]
Copy the code

Self. scope is similar to a Request in Django. It contains the request’s type, path, header, cookie, session, user, and other useful information

URLRouter: Specifies the path of the routing file, or you can write the routing information directly here. The code configures the path of the routing file, and looks for webSocket_urlpatterns in routeing.py under chat

from django.urls import path
from chat.consumers import ChatConsumer

websocket_urlpatterns = [
    path('ws/chat/', ChatConsumer),
]
Copy the code

The routing.py routing file is similar to Django’s url.py functionality and has the same syntax, meaning that access to WS /chat/ is handled by ChatConsumer

  1. Next, write a Consumer, which is similar to a View in Django
from channels.generic.websocket import WebsocketConsumer
import json

class ChatConsumer(WebsocketConsumer):
    def connect(self):
        self.accept()

    def disconnect(self, close_code):
        pass

    def receive(self, text_data):
        text_data_json = json.loads(text_data)
        message = Operation coffee Bar: + text_data_json['message']

        self.send(text_data=json.dumps({
            'message': message
        }))
Copy the code

The connect method fires when a connection is established, Disconnect fires when the connection is closed, and receive fires when a message is received. The entire ChatConsumer class prefixes all incoming messages to the client with “Operation cafe:”

  1. Finally, we added websocket support to the HTML template page
{% extends "base.html" %}

{% block content %}
  <textarea class="form-control" id="chat-log" disabled rows="20"></textarea><br/>
  <input class="form-control" id="chat-message-input" type="text"/><br/>
  <input class="btn btn-success btn-block" id="chat-message-submit" type="button" value="Send"/>
{% endblock %}

{% block js %}
<script>
  var chatSocket = new WebSocket(
    'ws://' + window.location.host + '/ws/chat/');

  chatSocket.onmessage = function(e) {
    var data = JSON.parse(e.data);
    var message = data['message'];
    document.querySelector('#chat-log').value += (message + '\n');
  };

  chatSocket.onclose = function(e) {
    console.error('Chat socket closed unexpectedly');
  };

  document.querySelector('#chat-message-input').focus();
  document.querySelector('#chat-message-input').onkeyup = function(e) {
    if (e.keyCode === 13) {  // enter, return
        document.querySelector('#chat-message-submit').click(); }}; document.querySelector('#chat-message-submit').onclick = function(e) {
    var messageInputDom = document.querySelector('#chat-message-input');
    var message = messageInputDom.value;
    chatSocket.send(JSON.stringify({
        'message': message
    }));

    messageInputDom.value = ' ';
  };
</script>
{% endblock %}
Copy the code

WebSocket objects support four messages: onOpen, onMessage, onCluse and onError. We use two onMessages and onClose

Onopen: The onOpen message is triggered when the connection between the browser and the WebSocket server is successful

Onerror: An onError message is triggered if the connection fails, or data fails to be sent or received, or data processing fails

Onmessage: The onMessage is triggered when the browser receives data from the WebSocket server. The parameter E contains the data sent by the server

Onclose: An onClose message is triggered when the browser receives a request from the WebSocket server to close the connection

  1. Complete the preceding code, a chat webSocket page is complete, run the project, enter a message in the browser through webSocket ->rouging.py- >consumer.py and return to the front end

To enable the Channel Layer

Example above we have achieved the sending and receiving messages, but now that is a chat room, bound to support many people chat at the same time, when we open multiple browser found only oneself messages are received after the input message, not receive other browsers end, how to solve this problem, let all the client can chat together?

Channels introduces the concept of a Layer, a communication system that allows multiple consumer instances to communicate with each other and with external Djanbo programs.

Channel Layer implements two main conceptual abstractions:

Channel name: A channel is a channel that sends messages. Each channel has a name. Anyone with this name can send messages to the channel

Group: Multiple channels can form a Group, and each Group has a name. Each person with this name can add/delete channels to the Group, or send messages to the Group. All channels in the Group can receive them. However, it cannot be sent to a specific Channel within the Group

With this concept in mind, let’s use channel Layer to implement a real chat room, allowing messages sent by multiple clients to be seen by each other

  1. The official recommendation is to use Redis as channel layer, so install channels_redis first
PIP install channels_redis = = 2.3.3Copy the code
  1. Then modify settings.py to add layer support
CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'channels_redis.core.RedisChannelLayer'.'CONFIG': {
            "hosts": [('ops-coffee.cn', 6379)],},},}Copy the code

After adding channels, we can check that the channel layer is working properly by using the following command

>python manage.py shell
Python 3.6.3 (v3.6.3:2c5fed8, Oct  3 2017, 18:11:49) [MSC v.1900 64 bit (AMD64)] on win32
Type "help"."copyright"."credits" or "license" for more information.
(InteractiveConsole)
>>> import channels.layers
>>> channel_layer = channels.layers.get_channel_layer()
>>>
>>> from asgiref.sync import async_to_sync
>>> async_to_sync(channel_layer.send)('test_channel', {'site':'https://ops-coffee.cn'})
>>> async_to_sync(channel_layer.receive)('test_channel')
{'site': 'https://ops-coffee.cn'} > > >Copy the code
  1. The consumer makes the following changes to introduce the Channel Layer
from asgiref.sync import async_to_sync
from channels.generic.websocket import WebsocketConsumer
import json

class ChatConsumer(WebsocketConsumer):
    def connect(self):
        self.room_group_name = 'ops_coffee'

        # Join room group
        async_to_sync(self.channel_layer.group_add)(
            self.room_group_name,
            self.channel_name
        )

        self.accept()

    def disconnect(self, close_code):
        # Leave room group
        async_to_sync(self.channel_layer.group_discard)(
            self.room_group_name,
            self.channel_name
        )

    # Receive message from WebSocket
    def receive(self, text_data):
        text_data_json = json.loads(text_data)
        message = text_data_json['message']

        # Send message to room group
        async_to_sync(self.channel_layer.group_send)(
            self.room_group_name,
            {
                'type': 'chat_message'.'message': message
            }
        )

    # Receive message from room group
    def chat_message(self, event):
        message = Operation coffee Bar: + event['message']

        # Send message to WebSocket
        self.send(text_data=json.dumps({
            'message': message
        }))
Copy the code

Here we set a fixed room name as the Group name, and all messages will be sent to this Group. Of course, you can also pass the room name as the Group name through parameters, so as to establish multiple groups, so that you can only communicate with messages in the room

When we enable the Channel Layer, all communication with the consumer will be asynchronous, so asynC_to_sync must be used

When a link is created, add a channel to the Group by group_add. When a link is closed, remove a channel from the Group by group_discard. When a message is received, call the group_send method to send the message to the Group. All channels in this Group can be received

The type in group_send specifies the message processing function, which forwards the message to the Chat_message function for processing

  1. After the above modification, we open the chat page again in multiple browsers to enter messages, found that each other can see, so far a complete chat room is basically completed

Change to asynchronous

Our previous implementation of consumer is synchronous. For better performance, we officially support asynchronous writing. We just need to modify consumer.py

from channels.generic.websocket import AsyncWebsocketConsumer
import json

class ChatConsumer(AsyncWebsocketConsumer):
    async def connect(self):
        self.room_group_name = 'ops_coffee'

        # Join room group
        await self.channel_layer.group_add(
            self.room_group_name,
            self.channel_name
        )

        await self.accept()

    async def disconnect(self, close_code):
        # Leave room group
        await self.channel_layer.group_discard(
            self.room_group_name,
            self.channel_name
        )

    # Receive message from WebSocket
    async def receive(self, text_data):
        text_data_json = json.loads(text_data)
        message = text_data_json['message']

        # Send message to room group
        await self.channel_layer.group_send(
            self.room_group_name,
            {
                'type': 'chat_message'.'message': message
            }
        )

    # Receive message from room group
    async def chat_message(self, event):
        message = Operation coffee Bar: + event['message']

        # Send message to WebSocket
        await self.send(text_data=json.dumps({
            'message': message
        }))
Copy the code

In fact, the asynchronous code is not much different from the previous code, only a few minor differences:

ChatConsumer changed from WebsocketConsumer to AsyncWebsocketConsumer

All methods are modified to asynchronous defAsync def

Async I/O calls with await

The Channel layer also no longer uses async_to_sync

Now a fully asynchronous and fully functional chat room has been built

The code address

I have uploaded the above demo code to Github for your reference during the implementation process. The specific address is:

Github.com/ops-coffee/…


Related articles recommended reading:

  • Django Model Select
  • Django configures tasks with asynchronous tasks and timed tasks
  • Django implements WebSocket using Channels — part 2