Javascript is required
Django集成Channels

Django本身不支持WebSocket,但可以通过集成Channels框架来实现WebSocket

Channels是针对Django项目的一个增强框架,可以使Django不仅支持HTTP协议,还能支持WebSocket,MQTT等多种协议,同时Channels还整合了Django的auth以及session系统方便进行用户管理及认证。

环境准备

Django           3.1.3
channels         2.2.0

channels

https://channels.readthedocs.io/en/stable/

入门

安装

pip install channels
pip install channels_redis

配置

# 安装APP
INSTALLED_APPS = [
    'django.contrib.admin',
    ...
    'channels',
  
# 全局配置 指定ASGI的路由地址
ASGI_APPLICATION = 'DEMO.routing.application'# APP_NAME.routing.application

image-20201119195254411

channels运行于ASGI协议上,ASGI的全名是Asynchronous Server Gateway Interface。它是区别于Django使用的WSGI协议 的一种异步服务网关接口协议,正是因为它才实现了websocket

asgi 与 wsgi

image-20201119174520916

紧跟着需要在根目录创建routing.py

from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter

import apps.webapp.routing #.自己的routing


application = ProtocolTypeRouter({
    # Empty for now (http->django views is added by default)
    'websocket': AuthMiddlewareStack(
        URLRouter(
            apps.webapp.routing.websocket_urlpatterns # APP下的urlpatterns
        )
    ),
})

创建一个APP

python3 manage.py startapp webapp

app下创建routing.py

from django.conf.urls import url
from .sockets.ChatConsumer import ChatConsumer

websocket_urlpatterns = [
    url(r'^chat/$', ChatConsumer),
]

目录结构

views目录保存当前APP下的HTTP请求

sockets目录保存SOCKET协议请求

image-20201119213022417

启动项目

image-20201119175704633

后台代码

apps/webapp/sockets/ChatConsumer.py

from channels.generic.websocket import WebsocketConsumer
import json


class ChatConsumer(WebsocketConsumer):
    def connect(self):
        print("连接...Websocket")
        self.accept()
    def disconnect(self, code):
        print('失去连接...Websocket')

    def receive(self, text_data=None, bytes_data=None):
        print(text_data)
        text_data_json = json.loads(text_data)
        message = 'Server: '+ text_data_json['message']
        self.send(text_data = json.dumps({
            'message': message
        }))

前端代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>


<div id="app">
    <textarea  disabled rows="20" v-model="dataList"></textarea><br/>
    <input  type="text" v-model="key" /><br/>
    <button @click="send">发送</button>
</div>

</body>
<script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.9/vue.min.js"></script>
<script>
    /*
    var chatSocket = new WebSocket('ws://127.0.0.1:8000/chat/');
    chatSocket.onmessage = function(e) {
        var data = JSON.parse(e.data);
        var message = data['message'];
        document.querySelector('#chat-log').value += (message + '\n');
    };*/

    var app = new Vue({
        el: '#app',
        data(){
            return {
                dataList: '',
                key: '',
            }
        },
        methods: {
            send(e){
                console.log("Send...");
                let data = JSON.stringify({
                    'message': this.key,
                })
                this.websocket.send(data)
                this.key = '';
            },
            initWebSocket(){
                this.websocket =  new WebSocket('ws://127.0.0.1:8000/chat/');
                this.websocket.onclose = this.websocketClose;
                this.websocket.onmessage = this.websocketOnMessage;
                this.websocket.onopen = this.websocketOnOpen;
                this.websocket.onerror = this.websocketOnError;
            },
            websocketOnError(e){
                console.log(e);
            },
            websocketOnOpen(e){
                console.log(e);
            },
            websocketOnMessage(e){
                console.log(e);
                var data = JSON.parse(e.data);
                 var message = data['message'];
                 this.dataList += message + '\n';
            },
            websocketClose(e){
                console.log('断开连接', e, e.code, e.wasClean);
            }
        },
        mounted(){
            this.initWebSocket();
        },
    })
</script>
</html>

image-20201119211926401


总结

在根目录下的routing.py

ProtocolTypeRouter
ASIG支持多种不同的协议,在这里可以指定特定协议的路由信息,我们只使用了websocket协议,这里只配置websocket即可

AuthMiddlewareStack
django的channels封装了django的auth模块,使用这个配置我们就可以在consumer中通过下边的代码获取到用户的信息

URLRouter
指定路由文件的路径,也可以直接将路由信息写在这里,代码中配置了路由文件的路径,会去apps.webapp下的routeing.py文件中查找websocket_urlpatterns

前端连接WebSocket

WebSocket对象一个支持四个消息:onopen,onmessage,oncluse和onerror,我们这里用了两个onmessage和onclose


onopen: 当浏览器和websocket服务端连接成功后会触发onopen消息

onerror: 如果连接失败,或者发送、接收数据失败,或者数据处理出错都会触发onerror消息

onmessage: 当浏览器接收到websocket服务器发送过来的数据时,就会触发onmessage消息,参数e包含了服务端发送过来的数据

onclose: 当浏览器接收到websocket服务器发送过来的关闭连接请求时,会触发onclose消息

流程

image-20201210233417393

启用Channel Layer

image-20210122065758221

我们已经实现了消息的发送和接收,但既然是聊天室,肯定要支持多人同时聊天的,当我们打开多个浏览器分别输入消息后发现只有自己收到消息,其他浏览器端收不到,如何解决这个问题,让所有客户端都能一起聊天呢?

Channels引入了一个layer的概念,channel layer是一种通信系统,允许多个consumer实例之间互相通信。

channel layer主要实现了两种概念抽象

channel name: channel实际上就是一个发送消息的通道,每个Channel都有一个名称,每一个拥有这个名称的人都可以往Channel里边发送消息



group: 多个channel可以组成一个Group,每个Group都有一个名称,每一个拥有这个名称的人都可以往Group里添加/删除Channel,也可以往Group里发送消息,Group内的所有channel都可以收到,但是无法发送给Group内的具体某个Channel
pip install channels_redis

电脑安装并且启动Redis

image-20210122071133174

修改配置

# 配置redis作为通道层
# 如果要使用channel layer 则必须配置通道层,否则无法获取channel_name
# settings.py
CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'channels_redis.core.RedisChannelLayer',
        'CONFIG': {
            "hosts": [('127.0.0.1', 6379)],
        },
    },
}

测试是否可以

(venv) ➜  DEMO git:(master) ✗ python3 manage.py shell
# 打开终端
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':'http://taoya.art'})
async_to_sync(channel_layer.receive)('test_channel')

image-20210122071505339

如果报错了,说明Redis服务没有启动成功。

全局通告

image-20210122071942769

总结

代码规范

1. 项目文件下创建routing.py
2. APP下创建的是routing.py