Django实现websocket

一 什么是Websocket

WebSocket是一种在单个TCP连接上进行全双工通信的协议

WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输

现在,很多网站为了实现推送技术,所用的技术都是轮询。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。

而比较新的技术去做轮询的效果是Comet。这种技术虽然可以双向通信,但依然需要反复发出请求。而且在Comet中,普遍采用的长链接,也会消耗服务器资源。

在这种情况下,HTML5定义了WebSocket协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。

很可能用不到的判断

WebSocket 协议在2008年诞生,2011年成为国际标准,所有浏览器都已经支持了。你可以这么判断浏览器是否支持:

<script>
    if ('WebSocket' in window) {
        console.log('你的浏览器支持 WebSocket')
    }
</script>

WebSocket for Django

django实现websocket大致上有两种方式,一种channels,一种是dwebsocket。channels依赖于redis,twisted等,相比之下使用dwebsocket要更为方便一些。

Install dwebsocket

pip install dwebsocket  # 最新版
# 网上貌似说最新的不好用,我们可以下载大家使用较多的老版本
pip install dwebsocket==0.4.2

我开始就下的默认版本,然后报错:

AttributeError: 'WSGIRequest' object has no attribute 'is_websocket'

后来下载老版本就好了。

服务端常用方法或者属性

名称描述备注
@accept_websocket 处理websocket和HTTP请求 该装饰器用的较多
@require_websocket 仅处理websocket请求,拒绝HTTP请求  
request.is_websocket() 如果请求类型是websocket,返回True,否则返回False 通常与@accept_websocket装饰器搭配
request.websocket 当websocket请求建立后,该请求具有一个websocket属性,可以通过该属性进行通信, 如果request.is_websocket()是False,则这个属性为None。
request.websocket.wait() 阻塞接收消息  
request.websocket.read() 非阻塞接收消息  
request.websocket.count_messages() 返回队列中的消息数量  
request.websocket.has_messages() 如果有新消息返回True,否则返回False  
request.websocket.send() 向客户端发送bytes类型的数据  
request.websocket.close() 服务器端主动关闭websocket服务  
request.websocket._iter_() websocket迭代器  

客户端的属性和方法

名称类型描述
WebSocket 对象 提供到服务端的双向通道
onopen 属性 当websocket连接时调用的事件处理程序
onmessage 属性 通知接收到消息的事件处理程序
onerror 属性 当出现错误时调用的事件处理程序
onclose 属性 当套接字关闭时调用的事件处理程序
readState 属性 报告websocket连接状态
close 方法 关闭websocket
send 方法 使用websocket向服务端发送数据
url 属性 报告套接字的当前URL
protocol 属性 报告服务器所选中的协议
binaryType 属性 由onmessage接收的二进制数据格式
bufferedAmount 属性 使用send的已排队的数据字节数
extensions 属性 包括服务器所选中的扩展名

关于readState,根据readState属性可以判断websocket的连接状态,该属性的值可以是以下几种:

属性值对应常量描述备注
0 CONNECTING 正在建立连接 但还没有建立完毕
1 OPEN 连接成功建立,可以进行通信  
2 CLOSING 连接正在关闭 即将关闭
3 CLOSED 连接已关闭 或者根本没有建立连接

根据bufferedAmount可以知道有多少字节的数据等待发送,若websocket已经调用了close方法该属性将会一直增长。

必要的settings配置

settings.py

MIDDLEWARE_CLASSES = [
    'dwebsocket.middleware.WebSocketMiddleware'
]
WEBSOCKET_ACCEPT_ALL=True  # 可以允许每一个单独的视图实用websocket

添加上这个中间件,就会拒绝单独的视图使用websocket,不过我们一般都是使用视图搭配websocket,所以,这个配置忘掉吧,顺便把第二个配置也忘掉,除非你要搞复杂的操作......

示例

环境

django1.11 + Python3.6 + PyCharm2018.1 + win10

Django中的配置

settings中保持默认即可

urls.py

from django.conf.urls import url
from django.contrib import admin
from web import views   # web是我的APP名称

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^test/', views.test, name='test'),
]

views.py

import time
from django.shortcuts import render
from dwebsocket.decorators import accept_websocket


@accept_websocket
def test(request):
    if request.is_websocket():
        print('websocket connection....')
        msg = request.websocket.wait()  # 接收前端发来的消息
        print(msg, type(msg), json.loads(msg))  # b'["1","2","3"]' <class 'bytes'> ['1', '2', '3']
        while 1:
            if msg:
                # 你要返回的结果
                for i in range(10):
                    request.websocket.send('service message: {}'.format(i).encode())  # 向客户端发送数据
                    time.sleep(0.5)  # 每0.5秒发一次
                request.websocket.close()
    else:  # 如果是普通的请求返回页面
        return render(request, 'test.html')

test.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="x-ua-compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>test</title>
</head>
<body>
<div></div>

</body>
<!-- 首先引入 jQuery -->
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.js"></script>
<script>
    // 判断浏览器是否支持WebSocket,目前应该所有的浏览器都支持了.....
    if ('WebSocket' in window) {
        console.log('你的浏览器支持 WebSocket')
    }
	// 创建一个WebSocket对象:sk,并且建立与服务端的连接(服务端程序要跑着哦)
    var sk = new WebSocket('ws://' + window.location.host + '/test/');
    // 向服务端发送消息
    sk.onopen = function () {
        console.log('websocket connection successful...');
        var l = ['1', '2', '3'];
        sk.send(JSON.stringify(l));
    };
    // 接收服务端的消息,主要的业务逻辑也在这里完成
    sk.onmessage = function (msg) {
        // 业务逻辑
        html = "<p>" + msg.data + "</p>";
        $("div").append(html);
        console.log('from service message: ', msg.data);
        // 由于服务端主动断开连接,这里也断开WebSocket连接
        if (sk.readyState == WebSocket.CLOSED) sk.close();
    };
    // 完事就关闭WebSocket连接
    sk.onclose = function (msg) {
        console.log('websocket connection close...');
        sk.close()
    };
    // 当WebSocket连接创建成功后,我们就可以向服务端发送数据了
    if (sk.readyState == WebSocket.OPEN) sk.onopen();

</script>
</html>

详解

dwebsocket有两种装饰器:require_websocket和accept_websocekt,使用require_websocket装饰器会导致视图函数无法接收导致正常的http请求,一般情况使用accept_websocket方式就可以了,

dwebsocket的一些内置方法:

request.is_websocket():判断请求是否是websocket方式,是返回true,否则返回false
request.websocket: 当请求为websocket的时候,会在request中增加一个websocket属性,
WebSocket.wait() 返回客户端发送的一条消息,没有收到消息则会导致阻塞
WebSocket.read() 和wait一样可以接受返回的消息,只是这种是非阻塞的,没有消息返回None
WebSocket.count_messages()返回消息的数量
WebSocket.has_messages()返回是否有新的消息过来
WebSocket.send(message)像客户端发送消息,message为byte类型

Django channels

  • http是一个网络协议(五状态短连接)
  • https是一个网络协议(五状态短连接)
  • websocket是一个网络协议(让浏览器和服务端创建链接支持,默认不再断开,两端就可以完成相互之间的收发数据)
  • websocket协议的诞生,可以让我们真正实现服务端向客户端推送消息

websocket实现原理:
——握手环节,验证服务端是否支持websocket协议,浏览器生成一个随机字符串,将随机字符串发送给服务端,服务端接收到随机字符串之后,让他跟magic string拼接,然后再进行sha1/base64加密,将密文返回到用户浏览器,用户浏览器会自动进行校验

——收发数据,密文 数据解密时需要读取数据第2个字节的后7位,和127,126,125比较后看情况~~

django channels 是django支持websocket的一个模块。在channels的内部已经帮我们写了握手/加密/解密等所有环节。建议在python3.6的环境中去运行。

1. 安装

pip3 install channels==2.3

2. 快速上手

2.1 在settings中添加配置

 

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'channels',
]
注册channels的app

 

 

ASGI_APPLICATION = "channel_demo.routing.application"
添加ASGI_APPLICATION支持websocket

 

2.2 创建websocket应用和路由

 

#!/usr/bin/env python
# -*- coding:utf-8 -*-
from channels.routing import ProtocolTypeRouter, URLRouter
from django.conf.urls import url
from chat import consumers


application = ProtocolTypeRouter({
    'websocket': URLRouter([
        url(r'^chat/$', consumers.ChatConsumer),
    ])
})
应用和路由

 

2.3 编写处理websocket逻辑业务

 

#!/usr/bin/env python
# -*- coding:utf-8 -*-
from channels.generic.websocket import WebsocketConsumer
from channels.exceptions import StopConsumer

class ChatConsumer(WebsocketConsumer):

    def websocket_connect(self, message):
        self.accept()

    def websocket_receive(self, message):
        print('接收到消息', message)
        self.send(text_data='收到了')

    def websocket_disconnect(self, message):
        print('客户端断开连接了')
        raise StopConsumer()
示例一

 

#!/usr/bin/env python
# -*- coding:utf-8 -*-
from channels.generic.websocket import WebsocketConsumer
from channels.exceptions import StopConsumer


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

    def receive(self, text_data=None, bytes_data=None):
        self.send(text_data)

        # 主动断开连接
        # self.close()

    def disconnect(self, code):
        print('客户端要断开了')
示例二
#!/usr/bin/env python
# -*- coding:utf-8 -*-
from channels.generic.websocket import WebsocketConsumer
from channels.exceptions import StopConsumer


CLIENTS = []

class ChatConsumer(WebsocketConsumer):

    def connect(self):
        self.accept()
        CLIENTS.append(self)

    def receive(self, text_data=None, bytes_data=None):
        for item in CLIENTS:
            item.send(text_data)

        # 主动断开连接
        # self.close()

    def disconnect(self, code):
        CLIENTS.remove(self)
示例三

 

3. channel layer

基于内存的channel layer

CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels.layers.InMemoryChannelLayer",
    }
}
配置
from channels.generic.websocket import WebsocketConsumer
from asgiref.sync import async_to_sync


class ChatConsumer(WebsocketConsumer):

    def connect(self):
        async_to_sync(self.channel_layer.group_add)('x1', self.channel_name)
        self.accept()

    def receive(self, text_data=None, bytes_data=None):
        async_to_sync(self.channel_layer.group_send)('x1', {
            'type': 'xxx.ooo',
            'message': text_data
        })

    def xxx_ooo(self, event):
        message = event['message']
        self.send(message)

    def disconnect(self, code):
        async_to_sync(self.channel_layer.group_discard)('x1', self.channel_name)
业务处理

基于 redis的channel layer

pip3 install channels-redis
CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels_redis.core.RedisChannelLayer",
        "CONFIG": {
            "hosts": [('10.211.55.25', 6379)]
        },
    },
}


CHANNEL_LAYERS = {
    'default': {
    'BACKEND': 'channels_redis.core.RedisChannelLayer',
    'CONFIG': {"hosts": ["redis://10.211.55.25:6379/1"],},
    },
}
 

CHANNEL_LAYERS = {
    'default': {
    'BACKEND': 'channels_redis.core.RedisChannelLayer',
    'CONFIG': {"hosts": [('10.211.55.25', 6379)],},},
}
 

CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels_redis.core.RedisChannelLayer",
        "CONFIG": {
            "hosts": ["redis://:password@10.211.55.25:6379/0"],
            "symmetric_encryption_keys": [SECRET_KEY],
        },
    },
}
配置
from channels.generic.websocket import WebsocketConsumer
from asgiref.sync import async_to_sync


class ChatConsumer(WebsocketConsumer):

    def connect(self):
        async_to_sync(self.channel_layer.group_add)('x1', self.channel_name)
        self.accept()

    def receive(self, text_data=None, bytes_data=None):
        async_to_sync(self.channel_layer.group_send)('x1', {
            'type': 'xxx.ooo',
            'message': text_data
        })

    def xxx_ooo(self, event):
        message = event['message']
        self.send(message)

    def disconnect(self, code):
        async_to_sync(self.channel_layer.group_discard)('x1', self.channel_name)
业务逻辑

 

posted @ 2019-09-25 22:29  silencio。  阅读(616)  评论(0)    收藏  举报