title: Python Web 开发及 Django 总结
date: 2023-07-24 17:26:26
tags:
- Python
- Web
categories: - Python
cover: https://cover.png
feature: false
Python 基础部分见:Python 基础总结
1. 创建项目
1.1 命令行
1、下载安装 Django
在终端输入 pip install django
,下载 Django。在 Python 的安装目录下存在如下两个目录
- Lib
- 包含一些内置模块
- 以及 site-packages 文件夹:用于存放一些第三方模块
- Scripts
- pip.exe、pip3.exe 等,可以用来安装一些第三方模块
下载完 Django 后,会在 Lib\site-packages
目录下生成 django 文件夹,包含了 Django 的源码;除此之外,还会在 scripts 目录下生成一个 django-admin.exe 文件,用于创建 Django 项目中的文件和文件夹
2、创建项目
用上面的 django-admin.exe 工具来创建 Django 项目,django-admin startproject 项目名称
1.2 Pycharm
选择创建 Django 项目
1.3 区别
- 命令行:创建的项目是标准的
- Pycharm:在标准的基础上增加了一些东西
- templates 目录,根目录下的模板目录(可删除)
- settings.py 中
'DIRS': [BASE_DIR / 'templates']
,配置的是根目录下模板目录的路径,可将 DIRS 置为空'DIRS': []
1.4 初始目录结构
- 与项目名相同的包
__init__.py
asgi.py
:接收网络请求,不需要动settings.py
:项目配置文件urls.py
:配置 URL 和 函数的对应关系wsgi.py
:接收网络请求,不需要动
- templates:已删除
- venv:虚拟环境
manage.py
:用于项目的管理、启动项目、创建 app、数据管理等,不需要动
2. APP
一个 App 即一个独立的功能包,使用 manage.py
创建 app,python .\manage.py startapp app
,初始目录结构如下
- migrations:数据库变更记录,不需要动
__init__.py
- templates:模板目录,后续可添加
- static:静态文件目录,后续可添加
__init__.py
admin.py
:Django 默认提供了 admin 后台管理,不需要动apps.py
:app 启动类,不需要动models.py
:对数据库进行操作tests.py
:单元测试,不需要动views.py
:与 URL 对应的函数
3. 基础使用
3.1 注册 App
添加 App 信息到 settings.py
里
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'app.apps.AppConfig'
]
3.2 编写 URL 和视图函数
在 urls.py
文件里添加 url 信息,先注释掉默认示例,参照默认示例自定义一个 url,然后导入 app 的 views.py
文件,因为 url 对应的函数写在 app 的 views.py
文件里
from django.contrib import admin
from django.urls import path
from app import views
urlpatterns = [
# path('admin/', admin.site.urls),
path('index/', views.index),
]
在 views.py
文件里编写对应的函数
from django.http import HttpResponse
def index(request):
return HttpResponse('欢迎使用')
3.3 启动项目
- 命令行启动,
python ./manage.py runserver
- Pycharm 启动,直接点启动按钮即可
项目启动后,访问项目的默认地址,http://127.0.0.1:8000/
,可以看到列出了项目的 url
加上 url 再访问,http://127.0.0.1:8000/index
,可以看到我们在函数里返回的信息
4. 模板和静态文件
4.1 模板渲染
在 app 中创建 templates 文件夹,注意是在 app 里创建,而不是 1.3 的顶层 templates 文件夹,然后创建 index.html
文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
欢迎
</body>
</html>
修改 views.py
文件的 index()
函数,将原本返回的 http 响应改为模板渲染,使用 render()
函数
from django.shortcuts import render
def index(request):
# return HttpResponse('欢迎使用')
return render(request, 'index.html')
Pycharm 会自动更新修改,再访问 http://127.0.0.1:8000/index
,显示的就是 index.html
文件的内容
这里的模板加载顺序为
- 优先去项目根目录的 templates 文件夹中寻找(即 1.3 的配置),不配置的话无效
- 第一点未配置的话,默认会根据 app 的注册顺序,依次在每个 app 下的 templates 目录中寻找
4.2 静态文件目录
默认的静态文件目录为 app 目录下的 static 目录,该项配置可在 settings.py
中修改,STATIC_URL = 'static/'
对应的静态文件相关目录结构为
- static
- CSS
- img
- js
- plugins
对应的 HTML 文件中加载静态目录可以使用绝对路径,即写死文件的目录名,如 /static/img/1.jpg
,缺点是假如修改了目录名或结构变化,对应的路径全都要修改;推荐使用相对路径(动态路径),如下
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
欢迎
<img src="{% static 'img/1.jpg' %}">s
</body>
</html>
动态加载静态文件目录,这时会去读取 settings.py
文件配置的静态文件目录
4.3 模板语法
1、传参
修改 views.py
文件的视图函数,传入的参数格式为字典
from django.shortcuts import render
def index(request):
name = '张三'
roles = ['管理员', '测试']
user_info = {'name': '张三', 'age': 28}
return render(request, 'index.html', {'n1': name, 'n2': roles, 'n3': user_info})
2、模板语法,由 Django 提供
render()
函数内部先会读取含有模板语法的 HTML 文件- 然后内部进行渲染,执行模板语法并替换数据,最终得到只包含 HTML 标签的字符串
- 将渲染(替换)完成的字符串返回给用户浏览器
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<p> 直接取值 </p>
<ul>
<li> {{ n1 }} </li>
<li> {{ n2 }} </li>
</ul>
<p> 取列表值 </p>
<ul>
<li> {{ n2.0 }} </li>
<li> {{ n2.1 }} </li>
</ul>
<p> 循环展示 </p>
<ul>
{% for item in n2 %}
<li> {{ item }} </li>
{% endfor %}
</ul>
<ul>
{% for k, v in n3.items %}
<li> {{ k }} = {{ v }} </li>
{% endfor %}
</ul>
<p> 条件 </p>
{% if n1 == 'xxx'%}
<h1> V1 </h1>
{% elif n1 == 'xx' %}
<h1> V2 </h1>
{% else %}
<h1> V3 </h1>
{% endif %}
</body>
</html>
页面展示为
5. 请求与响应
5.1 请求
views.py
文件中视图函数如下,request 为封装了用户发送过来的所有请求相关数据的对象
from django.http import HttpResponse
from django.shortcuts import redirect
def index(request):
# 获取请求方式
print(request.method)
# 获取请求路径
print(request.path)
# 获取 URL 上传递的值
print(request.GET)
# 获取请求体中提交的数据
print(request.POST)
return redirect('https://www.baidu.com')
如浏览器请求:http://127.0.0.1:8000/index/?id=1
GET
/index/
<QueryDict: {‘id’: [‘1’]}>
<QueryDict: {}>
5.2 响应
响应分为三种
- HttpResponse,直接返回响应数据
- render,返回 HTML 页面
- redirect,返回重定向地址
5.3 JSON 格式
前后端之间通常采用 JSON 格式来传递数据,可以利用 Django 封装的 JsonResponse 向浏览器返回 JSON 格式的数据
需要注意的是,JSON 格式也是一种 key:value 格式,所以返回为字典时,不会有问题,假如不是字典,要加上 参数 safe=False
,否则会报 TypeError 异常
1、GET 请求
发送请求,http://localhost:8000/index?current=1&size=5
from django.http import JsonResponse
def index(request):
print(request.GET)
print(request.POST)
# return JsonResponse({'message': '返回成功'})
return JsonResponse('返回成功', safe=False)
<QueryDict: {‘current’: [‘1’], ‘size’: [‘5’]}>
<QueryDict: {}>
2、POST 请求
为了防止跨站请求伪造(CSRF),Django 全局发送 POST 请求均需要字符串验证,否则会报 Forbidden (CSRF cookie not set.)
解决方案如下
- 注释
settings.py
文件中的'django.middleware.csrf.CsrfViewMiddleware',
- 在单个视图函数上加上
@csrf_exempt
,需要引入from django.views.decorators.csrf import csrf_exempt
发送请求,http://localhost:8000/index/
,需要注意的是,POST 请求结尾要加上 /
,并且 JSON 格式的数据会解析到 request.body
里,而非 request.POST
由于传递过来的数据为 bytes 类型数据,因此需要用 Python 提供的 json
库的 loads()
方法来加载数据,得到的是一个对象
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
import json
@csrf_exempt
def index(request):
print(request.GET)
print(request.POST)
json_data = json.loads(request.body)
print(json_data)
print(json_data.get('data')[0].get('name'))
# return JsonResponse('返回成功', safe=False)
return JsonResponse({'message': '返回成功'})
<QueryDict: {}>
<QueryDict: {}>
{‘id’: ‘1100’, ‘data’: [{‘name’: ‘张三’, ‘age’: 19, ‘address’: ‘上海’, ‘gender’: ‘男’}]}
张三
6. ORM
6.1 环境及配置
1、安装 mysqlclient,pip install mysqlclient
2、准备好对应的数据库环境,然后创建数据库,这里不多赘叙
3、Django 连接数据库,修改 settings.py
数据库配置,注释掉原有的数据库配置,添加新配置如下,可根据自己的数据库类型进行相应的改动
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'fan-web',
'USER': 'root',
'PASSWORD': 'password',
'HOST': '127.0.0.1',
'PORT': '3306'
}
}
6.2 数据库表操作
6.2.1 创建表
修改 modles.py
文件,添加数据库表对应的实体类
from django.db import models
class Userinfo(models.Model):
name = models.CharField(max_length=32)
password = models.CharField(max_length=64)
age = models.IntegerField()
依次执行如下命令,Django 会自动创建对应的数据库表,这里不需要加主键 ID,Django 会自动加
python .\manage.py makemigrations
python .\manage.py migrate
执行完成后,可以看到 Django 默认创建了一些表,而我们自定义的表也创建好了,并且自动加上了主键 ID
6.2.2 新增/删除表、删除表原有的字段
新增表直接在下面新加一个实体类即可,删除表则将对应的实体类删除,删除表原有的字段同样去掉实体类的字段即可
from django.db import models
# class Userinfo(models.Model):
# name = models.CharField(max_length=32)
# password = models.CharField(max_length=64)
# age = models.IntegerField()
class Role(models.Model):
name = models.CharField(max_length=32)
code = models.CharField(max_length=64)
然后执行前面的两行命令
python .\manage.py makemigrations
python .\manage.py migrate
6.2.3 给表添加字段
同样的给实体类加上字段后,执行 python .\manage.py makemigrations
命令时,会要求进行选择,在命令行给字段赋默认值;还是先退出,修改实体类加上默认值后,再来执行命令
1、这里先选择直接在命令行赋值,选择 1,接下来会要求你输入赋的默认值
输入后,再执行 python .\manage.py migrate
到表里看,新增的字段已经都赋了默认值
2、直接在实体类中赋默认值,或者设置成允许为空
from django.db import models
class Userinfo(models.Model):
name = models.CharField(max_length=32)
password = models.CharField(max_length=64)
age = models.IntegerField(default=2)
data = models.IntegerField(null=True, blank=True)
6.2.4 修改表的字段
相当于先删除原有的字段,然后再新增一个字段,即同样需要赋默认值,操作与上同
6.3 数据库数据操作
6.3.1 添加数据
通过 modles.py
的实体类执行 create(key=value,...)
方法即可,Userinfo.objects.create(name='张三', password='123', age=18)
可以将其放到视图函数里,然后通过 url 来请求创建。在 views.py
文件里引入 modles.py
,从而获取到实体类,执行其 create()
方法
from django.http import HttpResponse
from app import models
def index(request):
models.Userinfo.objects.create(name='张三', password='123', age=18)
return HttpResponse('成功')
然后访问 http://127.0.0.1:8000/index/
,则可看到数据库中添加了一条数据
6.3.2 获取数据
- 获取全部数据:通过
modles.py
的实体类执行all()
方法,models.Userinfo.objects.all()
,获取到的数据是一个 QuerySet 类型 - 条件获取:通过
modles.py
的实体类执行filter()
方法,获取到的也是一个 QuerySet 类型- 不过通过主键 ID 来获取的话,主键 ID 是唯一的,这时肯定只有一行数据,可以加上
first()
方法来获取第一行数据,返回的是一个对象
- 不过通过主键 ID 来获取的话,主键 ID 是唯一的,这时肯定只有一行数据,可以加上
其他的同上
from django.http import HttpResponse
from app import models
def index(request):
data_list = models.Userinfo.objects.all()
print(data_list, type(data_list))
for obj in data_list:
print(obj.id, obj.name, obj.password, obj.age)
filter_list = models.Userinfo.objects.filter(id=1)
print(filter_list)
obj = models.Userinfo.objects.filter(id=1).first()
print(obj.id, obj.name, obj.password, obj.age)
return HttpResponse('欢迎')
<QuerySet [<Userinfo: Userinfo object (1)>, <Userinfo: Userinfo object (2)>]> <class ‘django.db.models.query.QuerySet’>
1 张三 123 1
2 李四 123 1
<QuerySet [<Userinfo: Userinfo object (1)>]>
1 张三 123 1
6.3.3 删除数据
即在获取数据的基础上加上 delete()
方法
- 条件删除:条件获取数据后再执行
delete()
方法,models.Userinfo.objects.filter(id=4).delete()
- 全部删除:获取全部数据后再执行
delete()
方法,models.Userinfo.objects.all().delete()
其他的同上
from django.http import HttpResponse
from app import models
def index(request):
models.Userinfo.objects.filter(id=4).delete()
# models.Userinfo.objects.all().delete()
return HttpResponse('成功')
6.3.4 修改数据
在获取数据的基础上执行 update(key=value,...)
方法,与删除数据类似
from django.http import HttpResponse
from app import models
def index(request):
models.Userinfo.objects.filter(id=4).update(age=18)
# models.Userinfo.objects.all().update(age=15)
return HttpResponse('成功')
7. WebSocket
7.1 引入
现在要实现一个小型的聊天室,如网站的客服聊天窗口,或者直播平台的弹幕,只要有人发消息,会自动展示到页面上,该如何实现呢?
在原来的 Web 中一般都是使用 HTTP 协议,它是无状态的并且是短连接
- 客户端主动连接服务器
- 客户端向服务端发送请求,服务端收到请求返回数据
- 客户端接收到响应数据
- 断开连接
由上,只有客户端发起请求,才会更新数据,并且连接就断开了,一次请求一次响应;那当其他客户端发送消息时,我们怎么实时的获取到更新的数据呢?
-
HTTP 协议
- 轮询:客户端每隔一段时间(如 2s)就向服务端发送请求获取数据,但缺点是有延迟,而且对服务器压力很大,就算数据一直没变化也会不停的发送请求
- 长轮询:客户端发送请求,但服务端并不立即返回数据,假如数据有变化,则立即返回,数据一直没变化则等待一段时间(如 20s)再返回;客户端接收到返回后重新发送请求,然后重复上面的过程;即把轮询的时间拉长
-
WebSocket 协议:客户端与服务端创建连接不断开,那么就可以实现双向通道,服务端主动返回消息给客户端
7.2 原理
HTTP 协议
- 连接
- 数据传输
- 断开连接
WebSocket 协议是建立在 HTTP 协议之上的
- 客户端发起连接
- 握手(验证是否支持 WebSocket)
-
客户端向服务器发送
Sec-WebSocket-Key
,值为一个随机字符串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-Extensions: permessage-deflate; client_max_window_bits ... ... \r\n\r\n
-
服务端接收到后提取
Sec-WebSocket-Key
与 Magic String 进行拼接,默认为258EAFA5-E914-47DA-95CA-C5AB0DC85B11
,然后进行hmac1
加密和 Base64 加密v1 = 'mnwFxiOlctXFN/DeMt1Amg==' + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' v2 = hmac1(v1) v3 = base64(v2)
-
然后服务端通过
Sec-WebSocket-Accept
将加密好的数据返回HTTP/1.1 101 Switching Protocols Upgrade:websocket Connection: Upgrade Sec-WebSocket-Accept: v3
-
- 收发数据(加密)
- 先获取第 2 个字节,8 位
- 再获取第 2 个字节的后 7 位(Payload Len),7 位最大为 127
Payload Len == 127
:向后读取 8 个字节,然后是其他字节,其他字节再取 4 个字节拿到 Masking Key,再去读后面的数据进行解密Payload Len == 126
:向后读取 2 个字节,再去读取 2 个字节,然后是其他字节,其他字节再取 4 个字节拿到 Masking Key,再去读后面的数据进行解密Payload Len <= 125
:向后读取 2 个字节,然后是其他字节,其他字节再取 4 个字节拿到 Masking Key,再去读后面的数据进行解密
- 获取 Masking Key 进行解密
var DECODED = ""; for (var i = 0; i < ENCODED.length; i++) { DECODED[i] = ENCODED[i] ^ MASK[i % 4]; }
- 断开连接
7.3 WebSocket 配置及实现
Django 默认不支持 WebSocket,需要安装组件,pip install channels
注意,假如 channels 版本在 4.0 以上,默认不带 Daphne 服务器,需要手动安装 Daphne,pip install Daphne
然后进行配置
1、注册 channels
添加 channels 信息到 settings.py
里
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'app.apps.AppConfig',
'channels'
]
如果 channels 版本在 4.0 以上,下载完 Daphne 后也需要把 Daphne 注册进来,注意要放在第一行
INSTALLED_APPS = [
'daphne',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'app.apps.AppConfig',
'channels'
]
2、在 settings.py
中添加 ASGI_APPLICATION
- wsgi:默认
- asgi:wsgi + 异步 + WebSocket
ASGI_APPLICATION = 'fan_py_learning.asgi.application'
即 asgi.py
文件中的 application
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'fan_py_learning.settings')
application = get_asgi_application()
3、修改 asgi.py
文件,这里的 routing 需要创建该文件
import os
from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application
from . import routing
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'fan_py_learning.settings')
# application = get_asgi_application()
application = ProtocolTypeRouter({
'http': get_asgi_application(), # 自动找 urls、views
'websocket': URLRouter(routing.websocket_urlpatterns) # routing 即 urls, routing 里的 consumers 即 views
})
4、创建 routing.py
文件,这里的 consumers 需要创建该文件
from django.urls import re_path
from app import consumers
websocket_urlpatterns = [
# urls 路径
re_path(r'ws/(?P<group>\w+)/$', consumers.ChatConsumer.as_asgi()) # consumers 即 views
]
5、在 app 目录下创建 consumers.py
文件,继承 WebsocketConsumer
from channels.exceptions import StopConsumer
from channels.generic.websocket import WebsocketConsumer
class ChatConsumer(WebsocketConsumer):
def websocket_connect(self, message):
# 有客户端来向服务端发送 WebSocket 连接的请求时自动触发
print('客户端发起连接')
# 服务端允许和客户端创建连接(握手)
self.accept()
def websocket_receive(self, message):
# 客户端基于 WebSocket 向服务端发送数据, 自动触发接收消息
print('接收到的消息: ', message)
# 服务端主动断开连接
if message.get('text') == '关闭':
self.close()
# raise StopConsumer
return
self.send(message.get('text'))
def websocket_disconnect(self, message):
# 客户端与服务端断开连接时自动触发
print('断开连接了')
# 服务端同意断开连接
raise StopConsumer
7.4 实践
客户端发送请求,index.html
文件
new WebSocket('ws://localhost:8080/ws/connect')
创建 WebSocket 连接,这里的/ws/xxx
为上面routing.py
配置的走 WebSocket 的路径onopen()
:事件,当创建好连接时触发,即服务端执行self.accept()
send()
:发送数据onmessage()
:事件,当接收到服务端返回的数据时触发close()
:关闭连接onclose()
:事件,当连接关闭时触发
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="message" style="height: 500px; width: 500px; border: 1px solid blue;"></div>
<div>
<input type="text" placeholder="请输入" id="msg">
<input type="button" value="发送" onclick="sendMsg()">
<input type="button" value="关闭连接" onclick="closeConn()">
</div>
<script>
const socket = new WebSocket('ws://127.0.0.1:8000/ws/connect/')
let message = document.getElementById('message')
let msg = document.getElementById('msg')
{# 创建好连接后自动触发, 服务端执行 self.accept() #}
socket.onopen = function (event) {
message.innerText = '连接成功\n'
}
function sendMsg() {
socket.send(msg.value)
msg.value = ''
}
{# 当 WebSocket 接收到服务端返回的消息时自动触发 #}
socket.onmessage = function (event) {
message.innerText += event.data + '\n'
}
function closeConn() {
socket.close()
}
socket.onclose = function (event) {
message.innerText += '连接关闭'
}
</script>
</body>
</html>
7.5 群聊
7.5.1 简易实现
之前 consumers.py
文件的 WebSocket 代码如下
from channels.exceptions import StopConsumer
from channels.generic.websocket import WebsocketConsumer
class ChatConsumer(WebsocketConsumer):
def websocket_connect(self, message):
# 有客户端来向服务端发送 WebSocket 连接的请求时自动触发
print('客户端发起连接')
# 服务端允许和客户端创建连接(握手)
self.accept()
def websocket_receive(self, message):
# 客户端基于 WebSocket 向服务端发送数据, 自动触发接收消息
print('接收到的消息: ', message)
# 服务端主动断开连接
if message.get('text') == '关闭':
self.close()
# raise StopConsumer
return
self.send(message.get('text'))
def websocket_disconnect(self, message):
# 客户端与服务端断开连接时自动触发
print('断开连接了')
# 服务端同意断开连接
raise StopConsumer
这里的 self
表示当前的连接,那么很显然,这个只适用于一对一的通信,即范围只在当前的连接内,那么如何让它变成群聊形式的通信呢?
添加一个列表保存当前已经建立的连接,然后每当接收到消息时,就遍历连接列表,依次给每个连接都发送消息,同样当有某个连接断开时,则从列表中移除该连接
from channels.exceptions import StopConsumer
from channels.generic.websocket import WebsocketConsumer
conn_list = []
class ChatConsumer(WebsocketConsumer):
def websocket_connect(self, message):
# 有客户端来向服务端发送 WebSocket 连接的请求时自动触发
print('客户端发起连接')
# 服务端允许和客户端创建连接(握手)
self.accept()
# 添加进连接列表
conn_list.append(self)
def websocket_receive(self, message):
# 客户端基于 WebSocket 向服务端发送数据, 自动触发接收消息
print('接收到的消息: ', message)
# 服务端主动断开连接
if message.get('text') == '关闭':
self.close()
# raise StopConsumer
conn_list.remove(self)
return
for conn in conn_list:
conn.send(message.get('text'))
def websocket_disconnect(self, message):
# 客户端与服务端断开连接时自动触发
print('断开连接了')
conn_list.remove(self)
# 服务端同意断开连接
raise StopConsumer
7.5.2 Channel Layers
上面是用一个列表来保存连接信息从而实现群聊功能,在连接数量很多并且发送消息频率很高的情况下,每次都遍历去给每个连接发送消息就有点吃不消了,这里 Channels 提供了 Channel Layers 来实现群聊的功能
1、在 settings.py
添加配置
- 基于内存
CHANNEL_LAYERS = { 'default': { 'BACKEND': 'channels.layers.InMemoryChannelLayer' } }
- 基于 Redis
- 先下载 channels-redis,
pip install channels-redis
- 修改配置
CHANNEL_LAYERS = { 'default': { 'BACKEND': 'channels_redis.core.RedisChannelLayer', 'CONFIG': { 'hosts': [{'124.222.xxx.xxx', 6379}] } } }
- 先下载 channels-redis,
2、修改 consumers.py
文件 的WebSocket 代码
from asgiref.sync import async_to_sync
from channels.exceptions import StopConsumer
from channels.generic.websocket import WebsocketConsumer
class ChatConsumer(WebsocketConsumer):
def websocket_connect(self, message):
# 接受这个客户端的连接
self.accept()
# 将这个客户端的连接对象分组加入到内存或 Redis, 由配置决定使用内存还是 Redis
# self.channel_layer.group_add('group_code', self.channel_name) # group_add() 方法默认为异步
async_to_sync(self.channel_layer.group_add)('123123', self.channel_name) # 转换为同步
def websocket_receive(self, message):
print('接收到的消息: ', message)
async_to_sync(self.channel_layer.group_send)('123123', {'type': 'send_msg', 'message': message})
def send_msg(self, event):
print(event)
text = event['message']['text']
self.send(text)
def websocket_disconnect(self, message):
async_to_sync(self.channel_layer.group_discard)('123123', self.channel_name)
raise StopConsumer
上面的分组的组号是写死的,可以改成动态获取
1、修改 views.py
文件,让传入请求时传入群号,并获取该群号传到模板
修改后的请求如下:http://127.0.0.1:8000/index/?group_id=123
from django.shortcuts import render
def index(request):
group_id = request.GET.get('group_id')
return render(request, 'index.html', {'group_id': group_id})
2、修改 index.html
文件,创建 WebSocket 连接时传入群号
<script>
const socket = new WebSocket('ws://127.0.0.1:8000/ws/{{ group_id }}/')
</script>
3、调整群号为传入的参数
之前写的 routing.py
的 WebSocket 匹配地址为:r'ws/(?P<group>\w+)/$'
,因此传入的群号即为路由中地址的 group
from asgiref.sync import async_to_sync
from channels.exceptions import StopConsumer
from channels.generic.websocket import WebsocketConsumer
class ChatConsumer(WebsocketConsumer):
def websocket_connect(self, message):
# 接受这个客户端的连接
self.accept()
# 获取群号, 即路由匹配中的地址
url_route = self.scope['url_route']
print(url_route)
group_id = url_route['kwargs'].get('group')
# 将这个客户端的连接对象分组加入到内存或 Redis, 由配置决定使用内存还是 Redis
# self.channel_layer.group_add('group_code', self.channel_name) # group_add() 方法默认为异步
async_to_sync(self.channel_layer.group_add)(group_id, self.channel_name) # 转换为同步
def websocket_receive(self, message):
print('接收到的消息: ', message)
group_id = self.scope['url_route']['kwargs'].get('group')
async_to_sync(self.channel_layer.group_send)(group_id, {'type': 'send_msg', 'message': message})
def send_msg(self, event):
print(event)
text = event['message']['text']
self.send(text)
def websocket_disconnect(self, message):
group_id = self.scope['url_route']['kwargs'].get('group')
async_to_sync(self.channel_layer.group_discard)(group_id, self.channel_name)
raise StopConsumer
7.6 客户端
上面的客户端是在模板里实现的,那么如何用 Python 代码实现客户端呢?
首先下载客户端,pip install websocket-client
,具体的代码实现如下:
import websocket
# 1. 建立连接
ws = websocket.create_connection('ws://127.0.0.1:8000/ws/123/')
# 2. 获取连接状态
print("获取连接状态: ", ws.getstatus())
while True:
msg = input('请输入内容: ')
if msg == 'quit':
break
# 3. 发送信息
ws.send(msg)
# 4. 获取返回结果
result = ws.recv()
print("接收结果: ", result)
# 5. 关闭连接
ws.close()