直播聊天室的解决方案
import json
from django.shortcuts import render, HttpResponse
from django.http import JsonResponse
# Create your views here.
DB = []
def home(request):
return render(request, "home.html")
def send_msg(request):
text = request.GET.get("text")
DB.append(text)
return HttpResponse("OK")
def get_msg(request):
index = int(request.GET.get("index"))
context = {
"data": DB[index:],
"max_index": len(DB),
}
return JsonResponse(context)
urlpatterns = [
path('admin/', admin.site.urls),
path('home/', views.home),
path('send/msg/', views.send_msg),
path('get/msg/', views.get_msg),
]
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
<style>
.message{
height: 300px;
border: 1px solid #dddddd;
width: 100%;
}
style>
head>
<body>
<div class="message">div>
<input type="text" placeholder="请输入" id="txt">
<input type="button" value="发送" onclick="sendMessage();">
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js">script>
<script>
function sendMessage() {
var text = $("#txt").val();
// 基于ajax将用户输入的请求发送到后台
$.ajax({
url: "/send/msg/",
type: "GET",
data: {
text: text,
},
success: function (res) {
console.log("请求发送成功", res);
}
});
}
max_index = 0
// 每2s向后台发送请求,获取数据
setInterval(function () {
$.ajax({
url: "/get/msg/",
type: "GET",
data: {
index: max_index,
},
dataType: "JSON",
success: function (res) {
console.log("获取到数据:", res)
var dataArray = res.data
max_index = res.max_index
$.each(dataArray, function (index, item) {
console.log(index, item)
var tag = $("")
tag.text(item)
$(".message").append(tag)
})
}
})
}, 2000)
script>
body>
html>
长轮询实现聊天室
- 后台代码:
import queue
from django.shortcuts import render, HttpResponse
from django.http import JsonResponse
# Create your views here.
USER_QUEUE = {}
def home(request):
uid = request.GET.get("uid")
USER_QUEUE[uid] = queue.Queue()
return render(request, "home.html", {"uid": uid})
def send_msg(request):
text = request.GET.get("text")
for uid, q in USER_QUEUE.items():
q.put(text)
return HttpResponse("OK")
def get_msg(request):
# 去自己队列获取数据
uid = request.GET.get("uid")
q = USER_QUEUE.get(uid)
result = {"status": True, "data": None}
try:
data = q.get(timeout=10)
result["data"] = data
except queue.Empty:
result["status"] = False
return JsonResponse(result)
urlpatterns = [
path('admin/', admin.site.urls),
path('home/', views.home),
path('send/msg/', views.send_msg),
path('get/msg/', views.get_msg),
]
- 前台代码
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
<style>
#message{
height: 300px;
border: 1px solid #dddddd;
width: 100%;
}
style>
head>
<body>
<div id="message">div>
<input type="text" placeholder="请输入" id="txt">
<input type="button" value="发送" onclick="sendMessage();">
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js">script>
<script>
USER_UID = "{{ uid }}"
function sendMessage() {
var text = $("#txt").val();
// 基于ajax将用户输入的请求发送到后台
$.ajax({
url: "/send/msg/",
type: "GET",
data: {
text: text,
},
success: function (res) {
console.log("请求发送成功", res);
}
});
}
function getMessage() {
$.ajax({
url: "/get/msg/",
data: {
uid: USER_UID,
},
type: "GET",
dataType: "JSON", // 后台传递过来的数据类型必须是JSON
success: function (res) {
// 超时,没有获取到新数据
// 有新数据,展示新数据
if (res.status) {
var tag = $("")
tag.text(res.data)
$("#message").append(tag)
}
// js中的这种模式底层实现不是通过递归,所以这样一直调用没有问题
getMessage()
}
})
}
// 当页面框架加载完成之后执行
$(function () {
getMessage()
})
script>
body>
html>
问题:服务端持有这个链接,压力是否会很大?
我们目前采用的长轮询案例:100线程同时只能有100个用户请求,其他用户就需要等待。
如果是基于IO多路复用+异步,那么就既能节省资源,又可以提高并发量
初始 websocket
websocket: web版的socket
原来web中:
- http协议(无状态、短链接):客户端主动链接服务端,客户端向服务端发送消息,服务端给客户端响应数据,客户端接收数据,断开链接。
- https基于http+ssl/tls(对数据进行加密)
- websocket协议:创建持久链接不断开,基于这个链接进行收发数据,当服务端向客户端推送消息的时候,就需要使用websocket协议了。
- websocket应用场景:聊天室、数据大屏
websocket握手
http协议:
- 链接
- 数据传输
- 断开链接
websocket协议:
- 链接,客户端发起
- 握手,客户端发送一个消息,后端在接收到消息后在做一些特殊处理并返回,服务端支持websocket协议
- 收发数据(加密)
- 断开链接
请求和响应的【握手】信息需要遵循规则:
- 从请求【握手】信息中提取 Sec-WebSocket-Key
- 利用magic_string 和 Sec-WebSocket-Key 进行hmac1加密,再进行base64加密
- 将加密结果响应给客户端
注:magic string为:258EAFA5-E914-47DA-95CA-C5AB0DC85B11
请求握手信息为:
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
...
...
提取Sec-WebSocket-Key值并加密:
magic_string = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
value = headers['Sec-WebSocket-Key'] + magic_string
ac = base64.b64encode(hashlib.sha1(value.encode('utf-8')).digest())
参考文档
服务端响应头:
"HTTP/1.1 101 Switching Protocols
"Upgrade:websocket
"Connection: Upgrade
"Sec-WebSocket-Accept: ac
websocket数据解密
django中配置channels
django默认不支持websocket,需要安装组件
pip install channels
配置:
注册channels应用:
INSTALLED_APPS = [
...
'channels',
]
settings.py中增加:
ASGI_APPLICATION = "ws_demo.asgi.application"
修改asgi.py文件:
import os
from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from . import routing
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ws_demo.settings')
# application = get_asgi_application()
application = ProtocolTypeRouter({
"http": get_asgi_application(),
"websocket": URLRouter(routing.websocket_urlpatterns),
})
在settings同级目录创建routing.py文件
from django.urls import re_path
from app01 import consumers
websocket_urlpatterns = [
re_path(r"ws/(?P\w+)/$" , consumers.ChatConsumer.as_asgi()),
]
在app01目录下创建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()
def websocket_receive(self, message):
# 浏览器基于websocket向后端发送数据的时候, 自动触发接收消息
print(message)
self.send("不要回复,不要回复")
# 服务端主动断开链接
# self.close()
def websocket_disconnect(self, message):
# 客户端与服务端断开链接时,自动触发
print("断开链接")
raise StopConsumer()
在django中需要了解的:
- wsgi: Web服务器网关接口,是Python为了解决Web服务器与客户端之间的通信基于CGI标准而设计的。实现WSGI协议的服务器有uWSGI、uvicorn、gunicorn。
- asgi: wsgi+异步+websocket
http:
- urls.py
- views.py
websocket:
- routing.py
- consumers.py
注意一点: 如果在channels4.0开始,注册组件使用daphne,一定放在开头
`pip install daphne’
INSTALLED_APPS = [
"daphne", #注册daphne组件,在channels4.0开始,注册组件使用daphne,一定放在开头
...
]
websocket收发数据
- 访问地址,看到聊天室页面,http请求
- 让客户端主动向服务端发起websocket链接,服务端接收到链接后通过(握手)
demo版本:
- 客户端
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
<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()">
<input type="button" value="关闭链接" onclick="closeConn()">
div>
<script>
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接收到服务端发来的消息时,自动会触发这个函数
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() {
socket.close() // 向服务端发送断开链接的请求
}
script>
body>
html>
- 服务端
from channels.generic.websocket import WebsocketConsumer
from channels.exceptions import StopConsumer
class ChatConsumer(WebsocketConsumer):
def websocket_connect(self, message):
# 有客户端向后端发送websocket链接的请求时, 自动触发
print("有人来链接了...")
# 服务端允许和客户端创建链接(握手)
self.accept()
# 给客户端发送消息
# self.send("来了呀,客官")
# 服务端不允许和客户端创建链接
# raise StopConsumer()
def websocket_receive(self, message):
# 浏览器基于websocket向后端发送数据的时候, 自动触发接收消息
print("接收到消息", message["text"]) # {'type': 'websocket.receive', 'text': '你好啊'}
if message["text"] == "关闭":
# 服务端主动断开链接, 给客户端发送一条断开链接的消息
self.close()
# 如果服务端断开链接时,执行StopConsumer()异常,那么websocket_disconnect将不会再执行
raise StopConsumer()
# return
res = "{}SB".format(message["text"])
self.send(res)
# 服务端主动断开链接
# self.close()
def websocket_disconnect(self, message):
# 客户端与服务端断开链接时,自动触发
print("断开链接")
raise StopConsumer()
小结: 上面基于django实现的websocket请求,只能对某个人进行处理。
群聊(1)
- 前端代码:跟上面一样
- 后端代码:
from channels.generic.websocket import WebsocketConsumer
from channels.exceptions import StopConsumer
CONN_OBJ_LIST = []
class ChatConsumer(WebsocketConsumer):
def websocket_connect(self, message):
# 有客户端向后端发送websocket链接的请求时, 自动触发
print("有人来链接了...")
# 服务端允许和客户端创建链接(握手)
self.accept()
CONN_OBJ_LIST.append(self)
# 给客户端发送消息
# self.send("来了呀,客官")
# 服务端不允许和客户端创建链接
# raise StopConsumer()
def websocket_receive(self, message):
# 浏览器基于websocket向后端发送数据的时候, 自动触发接收消息
print("接收到消息", message["text"]) # {'type': 'websocket.receive', 'text': '你好啊'}
res = "{}SB".format(message["text"])
for conn in CONN_OBJ_LIST:
conn.send(res)
# 服务端主动断开链接
# self.close()
def websocket_disconnect(self, message):
# 客户端与服务端断开链接时,自动触发
print("断开链接")
CONN_OBJ_LIST.remove(self)
raise StopConsumer()
群聊(2)
基于channels中提供的channel layers来实现
- settings中进行配置
# channel layers
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels.layers.InMemoryChannelLayer",
}
}
基于redis的channle layer
pip install channels-redis
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [('10.211.55.25', 6379)]
},
},
}
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {"hosts": ["redis://10.211.55.25:6379/1"],},
},
}
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {"hosts": [('10.211.55.25', 6379)],},},
}
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": ["redis://:[email protected]:6379/0"],
# "symmetric_encryption_keys": [SECRET_KEY],
},
},
}
- consumers中需要做一些修改
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()
# 获取群号, 路由匹配当中的
group = self.scope["url_route"]["kwargs"].get("group")
# 将这个客户端的链接对象加入到某个地方(内存,redis)
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"]
self.send(text)
def websocket_disconnect(self, message):
group = self.scope["url_route"]["kwargs"].get("group")
# 将当前客户端从组内移除
async_to_sync(self.channel_layer.group_discard)(group, self.channel_name)
raise StopConsumer()
同时,前端发送websocket请求的时候,需要把群号传递过来:
socket = new WebSocket("ws://127.0.0.1:8000/room/{{ qq_group_num }}/");
这个群号可以是后台给返回过来的:
def index(request):
qq_group_num = request.GET.get("num")
return render(request, "index.html", {"qq_group_num": qq_group_num})
也就是说前端第一次请求页面的时候携带上群号即可。