Django channels实现websocket用户间私聊

先了解下这个 WebSocket ,在 WebSocket 中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。浏览器和服务器之间可以建立无限制的全双工通信,任何一方都可以主动发消息给对方。如图:

为什么传统的HTTP协议不能做到WebSocket实现的功能?

这是因为HTTP协议是一个请求-响应协议,请求必须先由浏览器发给服务器,服务器才能响应这个请求,再把数据发送给浏览器。换句话说,浏览器不主动请求,服务器是没法主动发数据给浏览器的。

 

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

 

一些websocket的应用:即时聊天通信、网站消息推送、在线协同编辑,如腾讯文档、多玩家在线游戏、视频弹幕等等...

 

我用django的channels做了个websocket私聊界面,来看看websocket在Django中的实现吧~

注:阅读下文需要一点Django基础呀

 先上结果:

你可能需要安装:

django

channels

channels-redis

这些我都是用pip安装的最新版

redis数据库

 

 Channels:

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

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

  1.   channel name: channel实际上是一个发送消息的通道,每个channel都有一个名称,每一个拥有这个名称的人都可以往channel里面发送消息
  2.   group: 多个channel可以组成一个group,每个group都有一个名称。 每个拥有这个名称的人都可以往这个group里添加/删除channel,也可以往group里发送消息。group内的所有channel都可以收到,但是不能给group内的具体某个channel发送消息。 一个浏览器端发送消息,多个浏览器端都能接受到消息。

 

 

从代码出发吧:

项目名:message_example,app名:message

setting.py的修改

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'channels',
    'message'
]
#在根路由配置中指向 Channels。
ASGI_APPLICATION = 'message_example.routing.application'

# 配置channel layer
CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'channels_redis.core.RedisChannelLayer',
        'CONFIG': {
            "hosts": [('127.0.0.1', 6379)],  #这个是redis的地址
        },
    },
}

在wsgi.py同级目录下创建routing.py

from django.urls import path
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
from message.consumers import MessagesConsumer

application = ProtocolTypeRouter({
    'websocket':AuthMiddlewareStack(  #认证
        URLRouter([
                path('ws/<str:username>/',MessagesConsumer)  #相当于urls.py的作用,给这个websocket请求相应的Consumer处理
            ])
        )

    })

 

在app文件夹里创建consumers.py(与models.py同级)

import json
from channels.generic.websocket import AsyncWebsocketConsumer


class MessagesConsumer(AsyncWebsocketConsumer):
    '''处理私信websocket请求'''
    async def connect(self):
        if self.scope['user'].is_anonymous:
            # 拒绝匿名用户连接
            await self.close()
        else:
            # 加入聊天组,以当前登录用户的用户名为组名,即self.scope['user'].username
            await self.channel_layer.group_add(self.scope['user'].username,self.channel_name)
            await self.accept()

    async def receive(self,text_data=None,bytes_data=None):
        '''接收到后端发来的私信'''
        print(text_data)
        await self.send(text_data=json.dumps(text_data))  #发送到接收用户

    async def disconnect(self,code):
        '''离开聊天组'''
        await self.channel_layer.group_discard(self.scope['user'].username,self.channel_name)

 

models.py的设计

from django.db import models
from django.contrib.auth.models import User


class Message(models.Model):
    sender = models.ForeignKey(User,related_name='sent_messages',on_delete=models.SET_NULL,blank=True,null=True,verbose_name='发送者')
    recipient = models.ForeignKey(User,related_name='receive_messages',on_delete=models.SET_NULL,blank=True,null=True,verbose_name='接收者')
    message = models.TextField(blank=True,null=True,verbose_name='内容')
    unread = models.BooleanField(default=True,db_index=True,verbose_name='是否未读')
    created_at = models.DateTimeField(auto_now_add=True,verbose_name='创建时间')
    def mark_as_read(self):
        if self.unread:
            self.unread = False
            self.save()

 

views.py对聊天室的处理

def chat(request,username):
    if request.method == 'GET':
        '''处理get请求,返回两用户的聊天记录'''
        recipient = User.objects.get(username=username)
        sender = request.user
        qs_one = Message.objects.filter(sender=sender,recipient=recipient)  # A发送给B的信息
        qs_two = Message.objects.filter(sender=recipient,recipient=sender)  # B发送给A的信息
        all = qs_one.union(qs_two).order_by('created_at')  # 取查到的信息内容的并集,相当于他两的聊天记录
        return render(request,'chat.html',locals())
    else:
        recipient = User.objects.get(username=username)  # 消息接收者
        content = request.POST.get('content','')  # 消息内容
        sender = request.user  # 发送者
        msg = Message.objects.create(sender=sender,recipient=recipient,message=content)  # 把消息存储到数据库
        qs_one = Message.objects.filter(sender=sender,recipient=recipient)  # A发送给B的信息
        qs_two = Message.objects.filter(sender=recipient,recipient=sender)  # B发送给A的信息
        all = qs_one.union(qs_two).order_by('created_at')  # 取查到的信息内容的并集,相当于他两的聊天记录
        channel_layer = get_channel_layer()
        # get_channel_layer()函数获得的是当前的websocket连接所对应的consumer类对象的channel_layer
        payload = {
            'type':'receive',  # 这个type是有限制的,比如现在用到的就是cusumer的receive函数
            'message':content,  # 消息内容
            'sender':request.user.username,  # 发送者
            'created_at':str(msg.created_at)  # 创建时间
        }
        group_name = username  #这里用的是接收者的用户名为组名,每个用户在进入聊天框后就会自动进入以自己用户名为组名的group
        async_to_sync(channel_layer.group_send)(group_name, payload)
        #上一句是将channel_layer.group_send()从异步改为同步,正常的写法是channel_layer.group_send(group_name, payload)
        return render(request,'chat.html',locals())

 前端js主动建立websocket

$(function(){
    
    const ws_scheme = window.location.protocol === "https:" ? "wss":"ws";
    const ws_path = ws_scheme + "://" + window.location.host + "/ws/"+ currentUser +"/"
    const ws = new ReconnectingWebSocket(ws_path)
    //监听后端发送过来的消息
    ws.onmessage = function(event){
        const data = JSON.parse(event.data);
        var html = '<div style="border:solid 1px; margin:10px;"><p>'+data['sender']+' |' +data['message']+'</p><p>'+data['created_at']+'</p></div>'
        //$(".send_message").before(html);
        if (data['sender'] === activeUser){
            $(".send_message").before(html);

        }
    }

});

 

 最后,我还是将代码放github好了。右上角直达

测试的用法是,启动redis数据库,migrate迁移数据库。createsuperuser创建两个超级管理员,然后直接python manager.py runserver运行 然后去到主页登录,记得使用两个不同的浏览器登录用户实现聊天。

注:只是想学习websocket,所以前端页面和后端逻辑做的有点糟糕,有怪勿怪哈~

The End~

posted @ 2019-10-11 02:25  byadmin  阅读(2159)  评论(0编辑  收藏  举报