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', ]

ASGI_APPLICATION = "channel_demo.routing.application"
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)