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~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM