Django与Channels实现WebSocket

本文介绍了如何使用Django结合Channels实现WebSocket,详细阐述了WebSocket、ajax轮询、long poll的区别,以及Channels在Django中的配置和使用。通过前端实现WebSocket连接,并展示了在前后端分离项目中WebSocket的实现代码。

WebSocket

在讲Websocket之前,先了解下 long pollajax轮询 的原理。

ajax轮询

ajax轮询的原理非常简单,让浏览器隔个几秒就发送一次请求,询问服务器是否有新信息。

long poll

long poll 其实原理跟 ajax轮询 差不多,都是采用轮询的方式,不过采取的是阻塞模型(一直打电话,没收到就不挂电话),也就是说,客户端发起连接后,如果没消息,就一直不返回Response给客户端。直到有消息才返回,返回完之后,客户端再次建立连接,周而复始。

ajax轮询 需要服务器有很快的处理速度和资源(速度)。long poll 需要有很高的并发,也就是说同时接待客户的能力(场地大小)。

Websocket

WebSocket是一种在单个TCP连接上进行全双工通讯的协议。WebSocket允许服务端主动向客户端推送数据。在WebSocket协议中,客户端浏览器和服务器只需要完成一次握手就可以创建持久性的连接,并在浏览器和服务器之间进行双向的数据传输。[外链图片转存失败(img-hQOZev9H-1565446193334)()]

WebSocket的请求头中重要的字段:

  • ConnectionUpgrade:表示客户端发起的WebSocket请求
  • Sec-WebSocket-Version:客户端所使用的WebSocket协议版本号,服务端会确认是否支持该版本号
  • Sec-WebSocket-Key:一个Base64编码值,由浏览器随机生成,用于升级request

WebSocket的响应头中重要的字段:

  • HTTP/1.1 101 Swi tching Protocols:切换协议,WebSocket协议通过HTTP协议来建立运输层的TCP连接
  • ConnectionUpgrade:表示服务端发起的WebSocket响应
  • Sec-WebSocket-Accept:表示服务器接受了客户端的请求,由Sec-WebSocket-Key计算得来

WebSocket协议的优点:

  • 支持双向通信,实时性更强
  • 数据格式比较轻量,性能开销小,通信高效
  • 支持扩展,用户可以扩展协议或者实现自定义的子协议(比如支持自定义压缩算法等)

WebSocket协议的优点:

  • 少部分浏览器不支持,浏览器支持的程度与方式有区别
  • 长连接对后端处理业务的代码稳定性要求更高,后端推送功能相对复杂
  • 成熟的HTTP生态下有大量的组件可以复用,WebSocket较少

WebSocket的应用场景:

  • 即时聊天通信,网站消息通知
  • 在线协同编辑,如腾讯文档
  • 多玩家在线游戏,视频弹幕,股票基金实施报价

Channels

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

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

[外链图片转存失败(img-rV2ydJ2h-1565446193339)()]

channels中文件和配置的含义

  • asgi.py:介于网络协议服务和Python应用之间的接口,能够处理多种通用协议类型,包括HTTPHTTP2WebSocket
  • channel_layers:在settings.py中配置。类似于一个通道,发送者(producer)在一段发送消息,消费者(consumer)在另一端进行监听
  • routings.py:相当于Django中的urls.py
  • consumers.py:相当于Django中的views.py

WSGI

WSGI(Python Web Server Gateway Interface):为Python语言定义的Web服务器和Web应用程序或者框架之间的一种简单而通用的接口。

ASGI

ASGI(Asynchronous Web Server Gateway Interface):异步网关协议接口,一个介于网络协议服务和Python应用之间的标准接口,能够处理多种通用的协议类型,包括HTTPHTTP2WebSocket

WSGI是基于HTTP协议模式的,不支持WebSocket,而ASGI的诞生则是为了解决Python常用的WSGI不支持当前Web开发中的一些新的协议标准。同时,ASGI对于WSGI原有的模式的支持和WebSocket的扩展,即ASGIWSGI的扩展。

Django中使用

1、安装channels,要注意版本的对应,在channels官网中可以得到对应的django版本

pip install channels==2.1.7

2、修改settings.py文件,

# APPS中添加channels
INSTALLED_APPS = [
    'django.contrib.staticfiles',
    ... ...
    'channels',
]
# 指定ASGI的路由地址
ASGI_APPLICATION = 'webapp.routing.application' #ASGI_APPLICATION 指定主路由的位置为webapp下的routing.py文件中的application

3、setting.py的同级目录下创建routing.py路由文件,routing.py类似于Django中的url.py指明websocket协议的路由

from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
import chat.routing
# 第一种设置的方法
from channels.security.websocket import AllowedHostsOriginValidator
application = ProtocolTypeRouter({
    # 普通的HTTP协议在这里不需要写,框架会自己指明
    'websocket': AllowedHostsOriginValidator(
	AuthMiddlewareStack(
    	URLRouter(
        	# 指定去对应应用的routing中去找路由
        	chat.routing.websocket_urlpatterns
    		)
		),
	)
})

# 第二种设置的方法,需要手动指定可以访问的IP
from channels.security.websocket import OriginValidator
application = ProtocolTypeRouter({
    # 普通的HTTP协议在这里不需要写,框架会自己指明
    'websocket': OriginValidator(
	AuthMiddlewareStack(
    	URLRouter(
        	# 指定去对应应用的routing中去找路由
        	chat.routing.websocket_urlpatterns
    		)
		),
        # 设置可以访问的IP列表
        ['*']
	)
})

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

AllowedHostsOriginValidator:指定允许访问的IP,设置后会去Django中的settings.py中去查找ALLOWED_HOSTS设置的IP

AuthMiddlewareStack:用于WebSocket认证,继承了Cookie MiddlewareSessionMiddleware,SessionMiddleware。djangochannels封装了djangoauth模块,使用这个配置我们就可以在consumer中通过下边的代码获取到用户的信息

def connect(self):
    self.user = self.scope["user"]

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

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

chat/routing.py内容如下

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

websocket_urlpatterns = [
    path('ws/chat/', EchoConsumer), # 这里可以定义自己的路由
    path('ws/<str:username>/',MessagesConsumer) # 如果是传参的路由在连接中获取关键字参数方法:self.scope['url_route']['kwargs']['username']
]

routing.py路由文件跟djangourl.py功能类似,语法也一样,意思就是访问ws/chat/都交给ChatConsumer处理。

4、在要使用WebSocket的应用中创建consumers.pyconsumers.py是用来开发ASGI接口规范的python应用,而Django中的view.py是用来开发符合WSGI接口规范的python应用。

首先了解下面的意思:
event loop事件循环、event handler事件处理器、sync同步、async异步

下面是一个同步的consumers.py

from channels.consumer import SyncConsumer

class EchoConsumer(SyncConsumer):
    def websocket_connect(self, event):
        self.send({
            'type': "websocket.accept"  # 这里是固定的写法,type不可以改变,是ASGI的接口规范,
        })

    def websocket_receive(self, event):
        user = self.scope['user'] # 获取当前用户,没有登录显示匿名用户
        path = self.scope['path'] # Request请求的路径,HTTP,WebSocket
        
        # ORM 同步代码 假如要查询数据库
        user = User.objects.filter(username=username)
        self.send({
            "type": "websocket.send",  # 这里是固定的写法,type不可以改变
            "text": event['text']  # 把前端返回过来的text返回回去
        })

异步的consumers.py

from channels.consumer import AsyncConsumer

class EchoConsumer(AsyncConsumer):
    async def websocket_connect(self, event):
        await self.send({
            'type': "websocket.accept"
        })
    async def websocket_receive(self, event):
        # 在异步中所有的操作都需要异步执行,比如发送请求,操作ORM
        # 对于异步的请求可以使用模块aiohttp实现异步的request请求
        # ORM 异步代码 假如要查询数据库
        # 第一种方式 使用channels通过的模块
        from channels.db import database_sync_to_async
        user = await database_sync_to_async(User.objects.filter(username=username))
        # 第二种方式 使用装饰器
        @database_sync_to_async
        def get_username():
			return User.objects.filter(username=username)
        await self.send({
            "type": "websocket.send", 
            "text": event['text']  
        })

需要注意的是在异步中所有的逻辑都应该是异步的,不可以那同步的和异步的代码混合使用。

继承WebSocketConsumer的连接

from channels.generic.websocket import AsyncWebsocketConsumer

class MessageConsumer(AsyncWebsocketConsumer):
    def connect(self):
        if self.scope['user'].is_anonymous:
            # 没有登陆的用户直接断开连接
            self.close()
        else:
            # 加入聊天组,并监听对应的频道
            # self.channel_layer进行监听频道
            # self.scope['user'].username以用户名作为组名,
            # self.channel_name 要进行监听的频道,会自己生成唯一的频道
            self.channel_layer.group_add(self.scope['user'].username,self.channel_name)
            self.accept()

    def receive(self, text_data=None, bytes_data=None):
        '''接受私信'''
        self.send(text_data=json.dumps(text_data)) # 将接收到的信息返回出去
    def disconnect(self, code):
        '''离开聊天组'''
        # self.scope['user'].username要结束的组名
     	self.channel_layer.group_discard(self.scope['user'].username,self.channel_name)

要改为异步和前面的方法一致

信息交互的周期

在这里插入图片描述

项目中可以在视图中直接推送信息给用户

view.py
from channels.layers import get_channel_layer
def send_message(request):
	... ...
	channel_layer = get_channel_layer()
	payload = {
        'type':'receive', # 这里的写法是固定的,receive代表的是consumers中的receive函数
        'message':'要发送的信息',
        'sender':sender.username, # 发送者的昵称
	}
	channel_layer.group_send(receiver_username,payload)

前端实现WebSocket

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

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

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

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

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

% 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 %}

参考博客:https://juejin.im/post/5cb67fc3e51d456e6a1d0237

前后端分离项目实现Websocket

环境版本:
	django==2.0
	channels==2.2.0
	channels-redis==2.3.2

vue实现代码:

全局配置 websocket.js

const path = window.location.host
const WSS_URL = 'wss://' + path + '/ws/chat/'
let Socket = ''
let setIntervalWebsocketPush = null

/** 建立连接 */
export function createSocket(projectId) {
  if (!Socket) {
    console.log('建立websocket连接')
    Socket = new WebSocket(WSS_URL + projectId)
    Socket.onopen = onopenWS
    Socket.onmessage = onmessageWS
    Socket.onerror = onerrorWS
    Socket.onclose = oncloseWS
  } else {
    console.log('websocket已连接')
  }
}
/** 打开WS之后发送心跳 */
export function onopenWS() {
  sendPing() // 发送心跳
}
/** 连接失败重连 */
export function onerrorWS() {
  clearInterval(setIntervalWebsocketPush)
  Socket.close()
  createSocket() // 重连
}
/** WS数据接收统一处理 */
export function onmessageWS(e) {
  window.dispatchEvent(new CustomEvent('onmessageWS', {
    detail: e.data
  }))
}
/** 发送数据 */
export function sendWSPush(eventTypeArr) {
  const obj = {
    appId: 'airShip',
    cover: 0,
    event: eventTypeArr
  }
  if (Socket !== null && Socket.readyState === 3) {
    Socket.close()
    createSocket() // 重连
  } else if (Socket.readyState === 1) {
    Socket.send(JSON.stringify(obj))
  } else if (Socket.readyState === 0) {
    setTimeout(() => {
      Socket.send(JSON.stringify(obj))
    }, 3000)
  }
}
/** 关闭WS */
export function oncloseWS() {
  clearInterval(setIntervalWebsocketPush) // 取消由setInterval()设置的timeout。
  Socket = ''
  console.log('websocket已断开')
}
/** 发送心跳 */
export function sendPing() {
  Socket.send('ping')
  setIntervalWebsocketPush = setInterval(() => {
    Socket.send('ping')
  }, 5000)
}

组件内使用 Index.vue

import { createSocket } from '@/api/websocket'
destroyed() {
	// 根据需要,销毁事件监听
	window.removeEventListener('onmessageWS', this.getDataFunc)
},
created() {
	createSocket(projectId)
	// 添加事件监听
	window.addEventListener('onmessageWS', this.getDataFunc)
},
methods:{
	// 监听ws数据响应
    getDataFunc(e) {
      const tempData = JSON.parse(e.detail)
    }
}

django实现代码:

settings.py

# 在应用中注册 channels
# Channels
ASGI_APPLICATION = 'cmdb.routing.application'
CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels_redis.core.RedisChannelLayer",
        "CONFIG": {
            "hosts": [('127.0.0.1', 6379)],
        },
    },
}

consumers.py

import json
from channels.generic.websocket import WebsocketConsumer
from asgiref.sync import async_to_sync

class MsgConsumer(WebsocketConsumer):
    def __init__(self, *args, **kwargs):
        self.room_group_name = ""
        super(MsgConsumer, self).__init__(*args, **kwargs)

    def connect(self):
        # 链接后将应用id作为组名,
        project_id = self.scope["path_remaining"]
        self.room_group_name = project_id
        async_to_sync(self.channel_layer.group_add)(
            self.room_group_name,
            self.channel_name
        )
        self.accept()

    def disconnect(self, close_code):
        # 断开连接时从组里面删除
        async_to_sync(self.channel_layer.group_discard)(
            self.room_group_name,
            self.channel_name
        )

    def receive(self, text_data=None, bytes_data=None):
        # 接受到信息时执行
        text_data_json = json.loads(text_data)
        message = text_data_json['message']
        async_to_sync(self.channel_layer.group_send)(
            self.room_group_name, {
                'type': 'chat.message',  # 必须在MsgConsumer类中定义chat_message
                'message': message
            })

    def send_message(self, event): 
        # 发送信息是执行
        message = event['message']
        self.send(text_data=json.dumps({
            'message': message
        }))

    def chat_message(self, event):
        message = event['message']
        self.send(text_data=json.dumps({
            'message': message
        }))

在其他视图内使用

view.py

# 测试函数
def send_fun():
    from asgiref.sync import async_to_sync
    from channels.layers import get_channel_layer
    channels_layer = get_channel_layer()
    data = '我是追加的内容\n'
    for i in [0, 1, 2, 3, 4, 5, 6]:
        send_dic = {
            "type": "send.message",
            "message": {
                'step': i,
                'content': data
            }
        }
        if i < 5:
            import time
            for j in range(19):
                time.sleep(0.5)
                send_dic = {
                    "type": "send.message", # 必须在MsgConsumer类中定义send_message
                    "message": {
                        'step': i,
                        'content': data
                    }
                }
                time.sleep(0.5)
                async_to_sync(channels_layer.group_send)(room_group_name , send_dic)
        else:
            async_to_sync(channels_layer.group_send)(room_group_name , send_dic)
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值