DC's blog DC's blog
首页
  • 计算机基础
  • linux基础
  • mysql
  • git
  • 数据结构与算法
  • axure
  • english
  • docker
  • opp
  • oop
  • 网络并发编程
  • 不基础的py基础
  • 设计模式
  • html
  • css
  • javascript
  • jquery
  • UI
  • 第一次学vue
  • 第二次学vue
  • Django
  • drf
  • drf_re
  • 温故知新
  • flask
  • 前后端不分离

    • BBS
    • 订单系统
    • CRM
  • 前后端部分分离

    • pear-admin-flask
    • pear-admin-django
  • 前后端分离

    • 供应链系统
  • 理论基础
  • py数据分析包
  • 机器学习
  • 深度学习
  • 华中科大的网课
  • cursor
  • deepseek
  • 杂文
  • 罗老师语录
  • 关于我

    • me
  • 分类
  • 归档
GitHub (opens new window)

DC

愿我一生欢喜,不为世俗所及.
首页
  • 计算机基础
  • linux基础
  • mysql
  • git
  • 数据结构与算法
  • axure
  • english
  • docker
  • opp
  • oop
  • 网络并发编程
  • 不基础的py基础
  • 设计模式
  • html
  • css
  • javascript
  • jquery
  • UI
  • 第一次学vue
  • 第二次学vue
  • Django
  • drf
  • drf_re
  • 温故知新
  • flask
  • 前后端不分离

    • BBS
    • 订单系统
    • CRM
  • 前后端部分分离

    • pear-admin-flask
    • pear-admin-django
  • 前后端分离

    • 供应链系统
  • 理论基础
  • py数据分析包
  • 机器学习
  • 深度学习
  • 华中科大的网课
  • cursor
  • deepseek
  • 杂文
  • 罗老师语录
  • 关于我

    • me
  • 分类
  • 归档
GitHub (opens new window)
  • python面向过程

  • python面向对象

  • 网络并发编程

    • 计算机网络储备
    • TCP简单实现
    • TCP粘包问题
    • 文件传输、UDP
    • socketserver模块
    • 进程理论储备
    • 进程开发必用
    • 进程开发必知
    • 生产者消费者模型
    • 线程开发
    • GIL详解
    • 进程池与线程池
    • 协程
    • 网络IO模型
    • 轮询长轮询
    • channels
      • websocket
        • 初识
        • 核心原理
      • channels配置
      • 收发数据
        • 大致思路
        • step1: 聊天室界面
        • step2: 建立ws连接
        • 客户端
        • 服务端
        • step3: 收发消息
        • C To S
        • S To C
        • ★ 整合
        • 服务端
        • 客户端
        • 运行结果
      • 群聊(简单版)
      • channel layers
        • 配置
        • "组"的概念
        • 多个组
    • 小项目之手撸ORM
    • 小项目之仿优酷
    • 脚本编写
  • 不基础的py基础

  • 设计模式

  • python_Need
  • 网络并发编程
DC
2023-10-09
目录

channels

# websocket

客户端和服务端创建连接不断开,那么就可以实现双向通信.
参考链接: https://www.cnblogs.com/wupeiqi/p/6558766.html

# 初识

○ 传统的Web开发过程中:
- http协议,无状态&短连接! 
  无状态,你不知道我是谁; 短连接,一次请求一次响应断开链接.
  ★ 客户端永远是主动发起者,而服务端永远是回复者.
  简单来说:
  1> 客户端主动连接服务端
  2> 客户端向服务端发送消息,服务端接收到并返回数据
  3> 客户端接收到数据
  4> 断开连接
  Ps: 登陆成功,返回cookie,下次请求带着cookie来,你就知道我是谁啦!
      想要在开发过程中保留一些状态信息,就可以基于cookie来实现.
- https = http + 对数据进行加密.


○ 现在支持:
- websocket协议,创建持久的连接不断开,基于这个连接可以进行收发数据!
  什么时候用? ★ 记住一点,服务端想要主动向客户端推送消息,就可以选择websocket协议!!
            Ps:像轮询、长轮询的机制,一定是客户端发起,服务端再响应.使用的是http协议.
  应用场景:
    - web聊天室
    - 实时图表 (柱状图、饼图等可以使用hightcharts来实现)
    - 工单审批系统
    - 代码发布系统
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 核心原理

http协议 粗略可分为三个步骤, 1.创建连接、2.传输数据、3.断开连接
websocket协议,是建立在http协议之上的! So, websocket也会有http协议的三个步骤, 但不仅是这三个步骤.
另外, http是基于TCP协议的, 所以websocket也是!

step1: 由客户端主动发起, 与服务端创建好连接后. (TCP三次握手)

step2: 并不是直接进行数据传输,而是进入websocket的 握手/验证 环节! 验证服务端需要支持websocket协议才ok.

“严格意义上来说,握手环节也是在传输数据.”
websocket的握手环节: 客户端发送一个消息,后端接收到消息后,对其做一些特殊处理,然后返回.

发什么消息?怎样的特殊处理?都必须按照规定的来.
1> 在浏览器上生成一个随机字符串,加密得到秘文,将明文发送给服务端
2> 服务端拿到这串随机字符串明文后,同样会进行加密的到秘文,加密算法得和浏览器客户端的加密算法一致,将秘文发送回浏览器
3> 浏览器拿到服务端发送过来的秘文,与自己加密得到的秘文进行比对
   若一致,则证明服务端支持websocket协议!! 握手/验证 环节通过!

具体来说:
- 客户端向服务端发送的数据
  GET /chatsocket HTTP/1.1  # 请求首行
  # 请求头
  Host: 127.0.0.1:8002
  Connection: Upgrade
  Pragma: no-cache
  Cache-Control: no-cache
  Upgrade: websocket
  Origin: http://localhost:63342
  Sec-WebSocket-Version: 13
  Sec-WebSocket-Key: mnwFxiOlctXFN/DeMt1Amg==  # Sec-WebSocket-Key就是那个随机字符串!!
  Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
  ... ... 
  \r\n\r\n  # \r\n\r\n后面是请求体内容,因为是GET请求,所以请求体肯定是空的
- 服务端接收数据后,会拿到随机字符串 
  拼接magic string、hmac1加密、bases64加密、放到响应头中返回
  1> 会将随机字符串与magic string进行拼接!
     (magic string固定等于“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”)
     v1 = "mnwFxiOlctXFN/DeMt1Amg==" + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
  2> 将拼接结果进行hmac1加密
     v2 = hmac1(v1)
  3> 再进行bases64加密
     v3 = base64(v2)
  4> 进行一系列处理后 将秘文v3放到响应头中返回给浏览器.
     HTTP/1.1 101 Switching Protocols
     Upgrade:websocket
     Connection: Upgrade
     Sec-WebSocket-Accept: 密文v3  # 放在这里!
- 客户端拿到v3秘文后,与自己的秘文进行比较. 若相等,握手/验证环节就通过啦!!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

step3: 若 握手/验证 环节通过啦, 就可以开始收发数据 (加密).

http协议收发数据直接传输就行.
websocket协议收发数据的过程会对数据进行加密!!

- 客户端发送数据,在网络里是通过字节传输的!!
- 服务端接收到这堆字节后,进行解密!! 解密过程如下:  ★ payload len不同,报头和数据的长度也就不同.
  - 先取第2个字节 一个字节是8bit位,eg: 10010110
  - 取该字节的后7个bit位 “专业术语 payload len”  即0010110
    根据后7位的值,分为3种情况.(7位的话,最大值是127) 根据该值的大小,动态的控制了报头占多少位.
      值     报头长度        masking key  数据
    - =127   2个字节,8个字节  4个字节     剩下字节
    - =126   2个字节,2个字节  4个字节     剩下字节
    - <=125  2个字节         4个字节     剩下字节
  - 获取masking key,对数据进行解密  解密算法如下:
    var DECODED = "";  # DECODED就是数据
    for (var i = 0; i < ENCODED.length; i++) {
        DECODED[i] = ENCODED[i] ^ MASK[i % 4];  # 拿到数据后的每个字节跟masking key进行位运算
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

step4: 断开连接.


# channels配置

进行一下一系列配置就可让Django框架支持websocket协议!!
需要自己手动创建 routings.py 和 consumers.py 并修改asgi.py文件

Django==3.2 ; channels==3.0.3

悄悄问个问题: 何为wsgi、何为asgi呢?
1> wsgi Django用的是第三方的,默认是wsgiref,部署时用的uwsgi,都仅支持同步
2> asgi = wsgi + 异步 + websocket
进行ASGI_APPLICATION的配置后,运行项目可以看到这样的字眼!! Starting ASGI/Channels..

1. 创建Django项目ws_demo.

pycharm创建名为ws_demo的py项目,使用的python虚拟环境
pip install Django==3.2 -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install channels==3.0.3 -i https://pypi.tuna.tsinghua.edu.cn/simple
django-admin startproject ws_demo .
python manage.py startapp api
一键运行/调试的配置
1
2
3
4
5
6

2. 在settings中添加配置

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'api',
    'channels',
]

WSGI_APPLICATION = 'ws_demo.wsgi.application'
ASGI_APPLICATION = 'ws_demo.asgi.application'
1
2
3
4
5
6
7
8
9
10
11
12
13

3. 在api这个app下创建consumers.py文件, 写入一下内容!!
编写处理websocket逻辑业务.. 以下是最简单的逻辑!!

from channels.generic.websocket import WebsocketConsumer
from channels.exceptions import StopConsumer


class ChatConsumer(WebsocketConsumer):
    def websocket_connect(self, message):
        """
        当有客户端向服务端发送websocket连接时,自动触发该方法的执行
        self.accept()        表明,服务端允许与客户端建立连接
        raise StopConsumer() 表明,服务端不想与客户端建立连接
        """
        print("有人来连接啦!!")
        self.accept()

    def websocket_receive(self, message):
        """
        当客户端基于websocket向服务端发送消息时,服务端会自动触发该方法接收消息!
        self.send()   服务端可向客户端主动发消息!
        self.close()  服务端主动断开连接! `四次挥手,两端都可主动发起断开连接的请求`
        """
        print('接收到消息:', message)
        self.send(text_data='收到了!')

    def websocket_disconnect(self, message):
        """
        当客户端主动与服务端断开连接时,自动触发该方法的执行. `四次挥手,两端都可主动发起断开连接的请求`
        """
        print('客户端断开连接了!')
        raise StopConsumer()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

4. 在settings.py的同级目录下, 创建routings.py文件, 写入以下内容!!

from django.urls import re_path

from app01 import consumers

websocket_urlpatterns = [
    # xxxxxxx/room/x1
    # ws://127.0.0.1:8000/room/群号/
    re_path(r'room/(?P<group>\w+)/$', consumers.ChatConsumer.as_asgi()),
]
1
2
3
4
5
6
7
8
9

5. 修改项目根目录下 ws_demo/asgi.py 文件

""" 原文件是这样编写的. 只支持处理http请求!
import os
from django.core.asgi import get_asgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ws_demo.settings')
application = get_asgi_application()
"""
import os

from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from ws_demo import routings

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ws_demo.settings')

# 进行配置,让其即支持http请求和又支持websocket请求!!
application = ProtocolTypeRouter({
    # 若是http请求,走这! 它会去找urls.py,在views.py中找到该http请求对应的视图函数并执行!
    "http": get_asgi_application(),       
    # 若是websocket请求,走这! 路由和视图文件Django没帮我们写,那我们就自己写!
    # - 自己写的路由: 它会去找我们自己创建的routings.py文件(它就类似于urls.py)
    # - 自己写的视图: routings.py里的路由匹配成功后,就执行consumers.py里的websocket业务逻辑.
    "websocket": URLRouter(routings.websocket_urlpatterns), 
})

"""
http      -  urls.py      views.py
websocket -  routings.py  consumers.py
"""
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# 收发数据

# 大致思路

使用channel实现聊天室的思路步骤如下:

1.访问地址看到聊天室的页面, http请求.
2.让客户端主动向服务端发起websocket连接请求,服务端接收到该请求后想建立连接的话会通过. 
  """
  换个通俗易懂的说法,想要使用websocket让大家相互聊天
  那么当用户打开聊天室的界面后,就应该让当前页面与服务端创建一个websocket的请求,连接上后,默认不断开.
  """
3.收发消息
  - 客户端向服务端发消息
  - 服务端主动发给客户端
4.断开连接
  - 客户端主动断开
  - 服务端主动断开
1
2
3
4
5
6
7
8
9
10
11
12

# step1: 聊天室界面

访问网址 http:127.0.0.1:8000/index/ 就可打开聊天室界面..

image-20230904184752750

路由配置

from django.urls import path
from api import views

urlpatterns = [
    path('index/', views.index),
]
1
2
3
4
5
6

视图函数

from django.shortcuts import render


def index(request):
    return render(request, "index.html")
1
2
3
4
5

前端代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>聊天室</title>
    <style>
        .message {
            height: 300px;
            border: 1px solid #dddddd;
            width: 100%;
        }
    </style>
</head>
<body>
<div class="message" id="message"></div>
<div>
    <input type="text" placeholder="请输入" id="txt">
    <input type="button" value="发送">
</div>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# step2: 建立ws连接

若发http请求,是这样的 http://127.0.0.1:8000/ ;
若发ws请求, 是这样的 ws://127.0.0.1:8000/ .

# 客户端

在前端index.html中编写关键代码

<script>
    /* 
       Q: 那我直接在浏览器上输入 ws://127.0.0.1:8000/ 可以吗?
       A: 不行,ws请求必须通过websocket对象来发送!!
    */
    // new WebSocket() 创建了一个websocket对象赋值给全局变量socket!
    socket = new WebSocket("ws://127.0.0.1:8000/room/123/");
</script>
1
2
3
4
5
6
7
8
# 服务端

S端建立ws连接的关键代码如下:

from channels.generic.websocket import WebsocketConsumer
from channels.exceptions import StopConsumer


class ChatConsumer(WebsocketConsumer):
    def websocket_connect(self, message):
        print("有人来连接啦!!")
        self.accept()
1
2
3
4
5
6
7
8

当我们在浏览器上 访问http:127.0.0.1:8000/index/ , 打开了聊天室页面, 可以仔细观察, 后端控制台输出的信息:

1> 浏览器先http的GET请求访问了 /index/ , 200访问成功;  
2> 浏览器的聊天室页面, 会向 /room/123/ 发送两次websocket请求
   - 第一次 会先进入了 HANDSHAKINGS 环节, 握手/验证 客户端验证服务端是否支持websocket协议
   - 第二次 然后进入了 CONNECT 环节, 请求服务端与其建立连接  
           - 打印“有人来连接啦”,表明有连接请求来啦,会自动执行函数websocket_connect.
            (值得注意的是,执行函数websocket_connect之前,握手/验证环节已经通过啦!!)
        
        
★ 从代码层面来分析,它是这么一个过程:
  - 运行Django项目,启动的是ASGI 
    Starting ASGI/Channels version 3.0.3 development server at http://127.0.0.1:8000/
  - 当有http请求来,走的是asgi.py里http的配置 也就是我们熟知的 找urls.py和views.py
  - 当有ws请求来,走的是asgi.py里websocket的配置
    - 先去找routing.py文件,进行路由匹配
    - 路由匹配成功后,会运行consumers.py里的代码!! 此步骤是建立ws连接,那么就会自动执行里面的websocket_connect函数.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

image-20230904185329899

# step3: 收发消息

收 发
客户端 socket.onmessage = function (event) {} socket.send()
服务端 def websocket_receive(self, message) self.send()
# C To S

收发消息, 客户端主动向服务端发送消息!!

客户端

<div>
    <input type="text" placeholder="请输入" id="txt">
    <input type="button" value="发送" onclick="sendMessage()"></div>
<script>
    socket = new WebSocket("ws://127.0.0.1:8000/room/123/");

    function sendMessage() {
        let tag = document.getElementById("txt");
        socket.send(tag.value);  // C To S主动发送消息!
    }
</script>
1
2
3
4
5
6
7
8
9
10
11

image-20230904193848523

服务端

from channels.generic.websocket import WebsocketConsumer
from channels.exceptions import StopConsumer


class ChatConsumer(WebsocketConsumer):
    def websocket_connect(self, message):
        print("有人来连接啦!!")
        self.accept()

    def websocket_receive(self, message):
        """
        message --> {'type': 'websocket.receive', 'text': 'hello'}
        message的值是一个字典,是channels在底层封装的
        该字典中里面有一个键type,若值是"websocket.receive",那么就表明是C TO S发送过来,服务端接受到的消息!!
        """
        print('接收到消息:', message['text'])  # S端接收C端发送的消息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

当访问http:127.0.0.1:8000/index/, 在聊天室界面文本框里数据“Hello”,点击发送按钮. 服务端的控制台会输出以下信息:

image-20230904193913523

# S To C

收发消息, 服务端主动向客户端发送消息!!

服务端

from channels.generic.websocket import WebsocketConsumer
from channels.exceptions import StopConsumer


class ChatConsumer(WebsocketConsumer):
    def websocket_connect(self, message):
        print("有人来连接啦!!")
        self.accept()                  # 服务端允许与客户端建立连接

        self.send("hi,我来啦!")         # C端向S端主动发送消息

    def websocket_receive(self, message):
        print('接收到消息:', message['text'])
        self.send(text_data='收到了!')  # S端接收C端发送的消息
1
2
3
4
5
6
7
8
9
10
11
12
13
14

客户端

<div>
    <input type="text" placeholder="请输入" id="txt">
    <input type="button" value="发送" onclick="sendMessage()"></div>
<script>
    socket = new WebSocket("ws://127.0.0.1:8000/room/123/");

    // 当websocket接收到服务端发来的消息时,自动会触发这个回调函数.
    // 服务端向客户端发送的消息被封装到了event里面!
    socket.onmessage = function (event) {
        console.log(event)
        console.log(event.data)
    }

    function sendMessage() {
        let tag = document.getElementById("txt");
        socket.send(tag.value);
    }
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

当访问http:127.0.0.1:8000/index/, 在浏览器里打开开发者模式, 在Console中可看到以下输出信息:

image-20230904195014662

# ★ 整合

c to s + s to c, 再补充几个客户端的回调函数.

# 服务端
from channels.generic.websocket import WebsocketConsumer
from channels.exceptions import StopConsumer


class ChatConsumer(WebsocketConsumer):
    def websocket_connect(self, message):
        print("有人来连接啦!!")
        self.accept()

        self.send("S端说: C端,hi,我来啦!")

    def websocket_receive(self, message):
        print('接收到来自C端的消息:', message['text'])

        if message['text'] == "关闭":  # 当收到前端发的消息是"关闭"时,服务端主动断开连接!!
            print('服务端主动断开连接了!')
            # 服务端主动断开连接,会给客户端发送一条断开连接的消息
            self.close()
            # 默认情况下,服务端主动断开连接后,也会执行websocket_disconnect方法.
            # (即默认情况下,websocket_disconnect无论哪方主动断开连接都会触发!)
            # 但如果服务端断开连接时,抛出StopConsumer异常,那么websocket_disconnect方法就不再执行!
            # ★ (这样就可以将websocket_disconnect方法变成客户端主动断开连接时才触发!!)
            # return
            raise StopConsumer()

        self.send(f'S端说: C端,你发送的消息是{message["text"]},俺收到了!')

    # 补充一点,点击断开连接按钮/关闭聊天页面/关闭浏览器 都会触发websocket_disconnect方法的执行!
    # 这三种操作都可以简单的看作是 客户端主动断开连接!!
    def websocket_disconnect(self, message):
        print('客户端主动断开连接了!')
        # C与S的连接断开了没问题,S与C的连接也得断开!
        # raise StopConsumer() 表明服务端同意与客户端断开连接.
        raise StopConsumer()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 客户端
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>聊天室</title>
    <style>
        .message {
            height: 300px;
            border: 1px solid #dddddd;
            width: 100%;
        }
    </style>
</head>
<body>
<div class="message" id="message"></div>
<div>
    <input type="text" placeholder="请输入" id="txt">
    <input type="button" value="发送" onclick="sendMessage()"></div>
    <input type="button" value="关闭连接" onclick="closeConn()">
<script>
    // 建立ws连接
    socket = new WebSocket("ws://127.0.0.1:8000/room/123/");

    // 创建好连接之后自动触发( 在服务端执行self.accept()后,客户端会自动执行该回调函数 )
    socket.onopen = function (event) {
        // 只有连接成功,就在聊天框里生成一个连接成功的标识 [连接成功]
        let tag = document.createElement("div");
        tag.innerText = "[连接成功]";
        document.getElementById("message").appendChild(tag);
    }

    // 当websocket接收到服务端发来的消息时,自动会触发这个回调函数.
    // 服务端向客户端发送的消息被封装到了event里面!
    socket.onmessage = function (event) {
        let tag = document.createElement("div");
        tag.innerText = event.data;
        document.getElementById("message").appendChild(tag);
    }

    // 当服务端主动断开连接时,这个方法会被自动触发.
    socket.onclose = function (event) {
        let tag = document.createElement("div");
        tag.innerText = "[断开连接]";
        document.getElementById("message").appendChild(tag);
    }

    // 点击发送按钮,触发的函数
    function sendMessage() {
        let tag = document.getElementById("txt");
        socket.send(tag.value);
    }

    // 点击断开连接按钮,触发的函数
    function closeConn() {
        // 服务端主动断开连接,向服务端发送断开连接的请求
        // 服务端接受到后,对自动执行websocket_disconnect函数.
        socket.close();
    }
</script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# 运行结果

image-20230904205231079

image-20230904205301085

[建立ws连接]
前: socket = new WebSocket("ws://127.0.0.1:8000/room/123/")
后: websocket_connect   self.accept()
前: socket.onopen
[S发C收] ‘S是在websocket_connect里发的’
后: websocket_connect   self.send("S端说: C端,hi,我来啦!")
前: socket.onmessage
[C发S收]
前: socket.send(tag.value);
后: websocket_receive(self, message)   print('接收到来自C端的消息:', message['text'])
[S发C收] ‘S是在websocket_receive里发的’
后: websocket_receive(self, message)   self.send(f'S端说: C端,你发送的消息是{message["text"]},俺收到了!')
前: socket.onmessage
[主动断开连接]
- 前主断: socket.close();
  后: websocket_disconnect   raise StopConsumer()
- 后主断: self.close() raise StopConsumer()
  前: socket.onclose
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 群聊(简单版)

ws收发数据章节整合小节的客户端代码不变, 服务端仅需维护一个全局变量即可简单的实现群聊功能!!

image-20230905105614158

from channels.generic.websocket import WebsocketConsumer
from channels.exceptions import StopConsumer

CONN_LIST = []  # 连接列表


class ChatConsumer(WebsocketConsumer):
    def websocket_connect(self, message):
        print("有人来连接啦!!")
        self.accept()
        self.send("欢迎来到One_Piece聊天室!")

        """
        注:当c与s创建ws连接后,self就指代创建好的那个连接!!
        其实因为是双向通道,我更倾向于理解成ws连接连接成功后,self指代S->C的那个通道.
        """
        # 当ws连接成功创建后,将连接对象添加到列表中去!
        # 这样的话,当有两个用户打开了聊天室页面,那么CONN_LIST列表中就有两个连接对象!
        CONN_LIST.append(self)

    def websocket_receive(self, message):
        print('接收到来自C端的消息:', message['text'])

        if message['text'] == "关闭":
            print('服务端收到指令,主动断开连接了!')
            self.close()
            # 某个ws连接断开后,需要从CONN_LIST列表中移除该连接.
            CONN_LIST.remove(self)
            raise StopConsumer()

        # - 原来: 发送给C端,在聊天框中显示该用户发送的消息,其他用户收不到
        # self.send(f'{message["text"]}')
        # - 现在: 循环CONN_LIST,调用各自对象的send方法,给里面的每个连接对象都回复
        # 这样,无论哪个用户发送消息,所有用户都能接受到啦!
        for conn in CONN_LIST:
            conn.send(message['text'])

    def websocket_disconnect(self, message):
        print('客户端主动断开连接了!')
        # 某个ws连接断开后,需要从CONN_LIST列表中移除该连接.
        CONN_LIST.remove(self)
        raise StopConsumer()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

# channel layers

简单版的群聊效率很低, channels提供的channel layers能更方便的帮我们实现群聊, 功能也更全面!!

# 配置

在settings中配置, 有两种方式. 该示例中, 我们使用方式一!

方式一: 将创建好的ws连接放到内存中

CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels.layers.InMemoryChannelLayer",
    }
}
1
2
3
4
5

方式二: 将创建好的ws连接放到redis中

pip3 install channels-redis -i https://pypi.tuna.tsinghua.edu.cn/simple
1
CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels_redis.core.RedisChannelLayer",
        "CONFIG": {"hosts": [('10.211.55.25', 6379)]},
    },
}


"""
'CONFIG': {"hosts": ["redis://10.211.55.25:6379/1"],},
'CONFIG': {"hosts": [('10.211.55.25', 6379)],},},
"CONFIG": {
    "hosts": ["redis://:[email protected]:6379/0"],
    "symmetric_encryption_keys": [SECRET_KEY],
},
"""
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# "组"的概念

在该示例中, 只有那一个组

image-20230905122439612

from channels.generic.websocket import WebsocketConsumer
from channels.exceptions import StopConsumer
from asgiref.sync import async_to_sync


class ChatConsumer(WebsocketConsumer):
    def websocket_connect(self, message):
        self.accept()  # 接收这个客户端的连接

        """
        将这个客户端的连接对象加入到某个地方(内存 or redis中)
        group_add() 
            第一个参数是 群号/聊天室号,自己随便定义
            第二个参数是 可认为是为当前客户端创建的一个随机别名
        So, group_add() 相当于将某个人放到了某个组里!!
        注: self.channel_layer.group_add() 方法是异步的!
        解决方案: 使用async_to_sync将异步功能转换成同步的功能.
        """
        async_to_sync(self.channel_layer.group_add)("1234", self.channel_name)

    def websocket_receive(self, message):
        # 会执行xx_oo方法,在此方法中自己可以去定义任意的功能.
        async_to_sync(self.channel_layer.group_send)("1234", {
            "type": "xx.oo",
            "message": message
        })

    def xx_oo(self, event):
        text = event["message"]["text"]
        # ★ 注意,此处不是给当前客户端回复,而是给组内的所有客户端进行回复!!
        # 若是只给自个儿回复,应在websocket_receive方法里写self.send(message["text"])
        self.send(text)

    def websocket_disconnect(self, message):
        # 相当于,断开连接时,会将self.channel_name这个人从"1234"这个群里移除掉!
        async_to_sync(self.channel_layer.group_discard)("1234", self.channel_name)
        raise StopConsumer()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

# 多个组

根据群号将这些ws连接区分开!!

image-20230905125441792

settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'api',
    'channels',
]

WSGI_APPLICATION = 'ws_demo.wsgi.application'
ASGI_APPLICATION = 'ws_demo.asgi.application'

CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels.layers.InMemoryChannelLayer",
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

asgi.py

import os

from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from ws_demo import routings

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ws_demo.settings')

# 进行配置,让其即支持http请求和又支持websocket请求!!
application = ProtocolTypeRouter({
    # 若是http请求,走这!
    "http": get_asgi_application(),  # 自动找urls.py,找视图函数  --> http
    # 若是websocket请求,走着!
    "websocket": URLRouter(routings.websocket_urlpatterns),  # routings(urls)、consumers(views)
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

urls.py

from django.urls import path
from api import views

urlpatterns = [
    # http://127.0.0.1:8000/index/?num=12345  num是群号
    path('index/', views.index),
]
1
2
3
4
5
6
7

views.py

from django.shortcuts import render


def index(request):
    qq_group_num = request.GET.get("num")
    return render(request, "index.html", {"qq_group_num": qq_group_num})
1
2
3
4
5
6

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>聊天室</title>
    <style>
        .message {
            height: 300px;
            border: 1px solid #dddddd;
            width: 100%;
        }
    </style>
</head>
<body>
<div class="message" id="message"></div>
<div>
    <input type="text" placeholder="请输入" id="txt">
    <input type="button" value="发送" onclick="sendMessage()"></div>
    <input type="button" value="关闭连接" onclick="closeConn()">
<script>
    // 建立ws连接
    socket = new WebSocket("ws://127.0.0.1:8000/room/{{ qq_group_num }}/");

    // 创建好连接之后自动触发( 在服务端执行self.accept()后,客户端会自动执行该回调函数 )
    socket.onopen = function (event) {
        // 只有连接成功,就在聊天框里生成一个连接成功的标识 [连接成功]
        let tag = document.createElement("div");
        tag.innerText = "[连接成功]";
        document.getElementById("message").appendChild(tag);
    }

    // 当websocket接收到服务端发来的消息时,自动会触发这个回调函数.
    // 服务端向客户端发送的消息被封装到了event里面!
    socket.onmessage = function (event) {
        let tag = document.createElement("div");
        tag.innerText = event.data;
        document.getElementById("message").appendChild(tag);
    }

    // 当服务端主动断开连接时,这个方法会被自动触发.
    socket.onclose = function (event) {
        let tag = document.createElement("div");
        tag.innerText = "[断开连接]";
        document.getElementById("message").appendChild(tag);
    }

    // 点击发送按钮,触发的函数
    function sendMessage() {
        let tag = document.getElementById("txt");
        socket.send(tag.value);
    }

    // 点击断开连接按钮,触发的函数
    function closeConn() {
        // 服务端主动断开连接,向服务端发送断开连接的请求
        // 服务端接受到后,对自动执行websocket_disconnect函数.
        socket.close();
    }
</script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61

routings.py

from django.urls import re_path

from api import consumers

websocket_urlpatterns = [
    # xxxxxxx/room/x1
    # ws://127.0.0.1:8000/room/群号/
    re_path(r'room/(?P<group>\w+)/$', consumers.ChatConsumer.as_asgi()),
]
1
2
3
4
5
6
7
8
9

consumers.py

from channels.generic.websocket import WebsocketConsumer
from channels.exceptions import StopConsumer
from asgiref.sync import async_to_sync


class ChatConsumer(WebsocketConsumer):
    def websocket_connect(self, message):
        self.accept()  # 接收这个客户端的连接

        """
        self.scope['url_route']['kwargs'] 固定写法
        """
        group = self.scope['url_route']['kwargs'].get("group")  # 获取ws的匹配的路由中的群号"routing.py"
        # 不再固定一个群号,而是可以通过ws连接获取 群号/组名
        # async_to_sync(self.channel_layer.group_add)("1234", self.channel_name)
        async_to_sync(self.channel_layer.group_add)(group, self.channel_name)

    def websocket_receive(self, message):
        group = self.scope['url_route']['kwargs'].get("group")  # 获取群号

        # 会执行xx_oo方法,在此方法中自己可以去定义任意的功能.
        async_to_sync(self.channel_layer.group_send)(group, {
            "type": "xx.oo",
            "message": message
        })

    def xx_oo(self, event):
        text = event["message"]["text"]
        # 注意,此处不是给当前客户端回复,而是给组内的所有客户端进行回复!!
        # 若是只给自个儿回复,是在websocket_receive方法里写self.send(message["text"])
        self.send(text)

    def websocket_disconnect(self, message):
        group = self.scope['url_route']['kwargs'].get("group")  # 获取群号

        # 相当于,断开连接时,会将self.channel_name这个人从"1234"这个群里移除掉!
        async_to_sync(self.channel_layer.group_discard)(group, self.channel_name)
        raise StopConsumer()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

轮询长轮询
小项目之手撸ORM

← 轮询长轮询 小项目之手撸ORM→

最近更新
01
deepseek本地部署+知识库
02-17
02
实操-微信小程序
02-14
03
教学-cursor深度探讨
02-13
更多文章>
Theme by Vdoing | Copyright © 2023-2025 DC | One Piece
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式