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
- Install the channels
PIP install channels = = 2.1.7Copy the code
- 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
- 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
- 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
- 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
- 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:”
- 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
- 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
- The official recommendation is to use Redis as channel layer, so install channels_redis first
PIP install channels_redis = = 2.3.3Copy the code
- 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
- 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
- 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