Django使用Channels实现WebSocket--上篇

WebSocket - 开启通往新世界的大门html

WebSocket是什么?前端

WebSocket是一种在单个TCP链接上进行全双工通信的协议。WebSocket容许服务端主动向客户端推送数据。在WebSocket协议中,客户端浏览器和服务器只须要完成一次握手就能够建立持久性的链接,并在浏览器和服务器之间进行双向的数据传输。python

WebSocket有什么用?git

WebSocket区别于HTTP协议的一个最为显著的特色是,WebSocket协议能够由服务端主动发起消息,对于浏览器须要及时接收数据变化的场景很是适合,例如在Django中遇到一些耗时较长的任务咱们一般会使用Celery来异步执行,那么浏览器若是想要获取这个任务的执行状态,在HTTP协议中只能经过轮训的方式由浏览器不断的发送请求给服务器来获取最新状态,这样发送不少无用的请求不只浪费资源,还不够优雅,若是使用WebSokcet来实现就很完美了github

WebSocket的另一个应用场景就是下文要说的聊天室,一个用户(浏览器)发送的消息须要实时的让其余用户(浏览器)接收,这在HTTP协议下是很难实现的,但WebSocket基于长链接加上能够主动给浏览器发消息的特性处理起来就游刃有余了web

初步了解WebSocket以后,咱们看看如何在Django中实现WebSocketredis

Channels

Django自己不支持WebSocket,但能够经过集成Channels框架来实现WebSocketshell

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

我下文全部的代码实现使用如下python和Django版本json

  • python==3.6.3
  • django==2.2

集成Channels

我假设你已经新建了一个django项目,项目名字就叫webapp,目录结构以下

project
    - webapp
        - __init__.py
        - settings.py
        - urls.py
        - wsgi.py
    - manage.py
复制代码
  1. 安装channels
pip install channels==2.1.7
复制代码
  1. 修改settings.py文件,
# APPS中添加channels
INSTALLED_APPS = [
    'django.contrib.staticfiles',
    'channels',
]

# 指定ASGI的路由地址
ASGI_APPLICATION = 'webapp.routing.application'
复制代码

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

ASGI_APPLICATION 指定主路由的位置为webapp下的routing.py文件中的application

  1. setting.py的同级目录下建立routing.py路由文件,routing.py相似于Django中的url.py指明websocket协议的路由
from channels.routing import ProtocolTypeRouter

application = ProtocolTypeRouter({
    # 暂时为空,下文填充
})
复制代码
  1. 运行Django项目
C:\python36\python.exe D:/demo/tailf/manage.py runserver 0.0.0.0:80
Performing system checks...
Watching for file changes with StatReloader

System check identified no issues (0 silenced).
April 12, 2019 - 17:44:52
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.
复制代码

仔细观察上边的输出会发现Django启动中的Starting development server已经变成了Starting ASGI/Channels version 2.1.7 development server,这代表项目已经由django使用的WSGI协议转换为了Channels使用的ASGI协议

至此Django已经基本集成了Channels框架

构建聊天室

上边虽然在项目中集成了Channels,但并无任何的应用使用它,接下来咱们以聊天室的例子来说解Channels的使用

假设你已经建立好了一个叫chat的app,并添加到了settings.py的INSTALLED_APPS中,app的目录结构大概以下

chat
    - migrations
        - __init__.py
    - __init__.py
    - admin.py
    - apps.py
    - models.py
    - tests.py
    - views.py
复制代码

咱们构建一个标准的Django聊天页面,相关代码以下

url:

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

urlpatterns = [
    path('chat', chat, name='chat-url')
]
复制代码

view:

from django.shortcuts import render

def chat(request):
    return render(request, 'chat/index.html')
复制代码

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 %}
复制代码

经过上边的代码一个简单的web聊天页面构建完成了,访问页面大概样子以下:

接下来咱们利用Channels的WebSocket协议实现消息的发送接收功能

  1. 先从路由入手,上边咱们已经建立了routing.py路由文件,如今来填充里边的内容
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
import chat.routing

application = ProtocolTypeRouter({
    'websocket': AuthMiddlewareStack(
        URLRouter(
            chat.routing.websocket_urlpatterns
        )
    ),
})
复制代码

ProtocolTypeRouter: ASIG支持多种不一样的协议,在这里能够指定特定协议的路由信息,咱们只使用了websocket协议,这里只配置websocket便可

AuthMiddlewareStack: django的channels封装了django的auth模块,使用这个配置咱们就能够在consumer中经过下边的代码获取到用户的信息

def connect(self):
    self.user = self.scope["user"]
复制代码

self.scope相似于django中的request,包含了请求的type、path、header、cookie、session、user等等有用的信息

URLRouter: 指定路由文件的路径,也能够直接将路由信息写在这里,代码中配置了路由文件的路径,会去chat下的routeing.py文件中查找websocket_urlpatterns,chat/routing.py内容以下

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

websocket_urlpatterns = [
    path('ws/chat/', ChatConsumer),
]
复制代码

routing.py路由文件跟django的url.py功能相似,语法也同样,意思就是访问ws/chat/都交给ChatConsumer处理

  1. 接着编写consumer,consumer相似django中的view,内容以下
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 = '运维咖啡吧:' + text_data_json['message']

        self.send(text_data=json.dumps({
            'message': message
        }))
复制代码

这里是个最简单的同步websocket consumer类,connect方法在链接创建时触发,disconnect在链接关闭时触发,receive方法会在收到消息后触发。整个ChatConsumer类会将全部收到的消息加上“运维咖啡吧:”的前缀发送给客户端

  1. 最后咱们在html模板页面添加websocket支持
{% 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 %}
复制代码

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

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

onerror: 若是链接失败,或者发送、接收数据失败,或者数据处理出错都会触发onerror消息

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

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

  1. 完成前边的代码,一个能够聊天的websocket页面就完成了,运行项目,在浏览器中输入消息就会经过websocket-->rouging.py-->consumer.py处理后返回给前端

启用Channel Layer

上边的例子咱们已经实现了消息的发送和接收,但既然是聊天室,确定要支持多人同时聊天的,当咱们打开多个浏览器分别输入消息后发现只有本身收到消息,其余浏览器端收不到,如何解决这个问题,让全部客户端都能一块儿聊天呢?

Channels引入了一个layer的概念,channel layer是一种通讯系统,容许多个consumer实例之间互相通讯,以及与外部Djanbo程序实现互通。

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

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

group: 多个channel能够组成一个Group,每一个Group都有一个名称,每个拥有这个名称的人均可以往Group里添加/删除Channel,也能够往Group里发送消息,Group内的全部channel均可以收到,可是没法发送给Group内的具体某个Channel

了解了上边的概念,接下来咱们利用channel layer实现真正的聊天室,可以让多个客户端发送的消息被彼此看到

  1. 官方推荐使用redis做为channel layer,因此先安装channels_redis
pip install channels_redis==2.3.3
复制代码
  1. 而后修改settings.py添加对layer的支持
CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'channels_redis.core.RedisChannelLayer',
        'CONFIG': {
            "hosts": [('ops-coffee.cn', 6379)],
        },
    },
}
复制代码

添加channel以后咱们能够经过如下命令检查通道层是否可以正常工做

>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'}
>>>
复制代码
  1. consumer作以下修改引入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 = '运维咖啡吧:' + event['message']

        # Send message to WebSocket
        self.send(text_data=json.dumps({
            'message': message
        }))
复制代码

这里咱们设置了一个固定的房间名做为Group name,全部的消息都会发送到这个Group里边,固然你也能够经过参数的方式将房间名传进来做为Group name,从而创建多个Group,这样能够实现仅同房间内的消息互通

当咱们启用了channel layer以后,全部与consumer之间的通讯将会变成异步的,因此必须使用async_to_sync

一个连接(channel)建立时,经过group_add将channel添加到Group中,连接关闭经过group_discard将channel从Group中剔除,收到消息时能够调用group_send方法将消息发送到Group,这个Group内全部的channel均可以收的到

group_send中的type指定了消息处理的函数,这里会将消息转给chat_message函数去处理

  1. 通过以上的修改,咱们再次在多个浏览器上打开聊天页面输入消息,发现彼此已经可以看到了,至此一个完整的聊天室已经基本完成

修改成异步

咱们前边实现的consumer是同步的,为了能有更好的性能,官方支持异步的写法,只须要修改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 = '运维咖啡吧:' + event['message']

        # Send message to WebSocket
        await self.send(text_data=json.dumps({
            'message': message
        }))
复制代码

其实异步的代码跟以前的差异不大,只有几个小区别:

ChatConsumer由WebsocketConsumer修改成了AsyncWebsocketConsumer

全部的方法都修改成了异步defasync def

await来实现异步I/O的调用

channel layer也再也不须要使用async_to_sync

好了,如今一个彻底异步且功能完整的聊天室已经构建完成了

代码地址

我已经将以上的演示代码上传至Github方便你在实现的过程当中查看参考,具体地址为:

github.com/ops-coffee/…


相关文章推荐阅读:

相关文章
相关标签/搜索