1、websocket 相关
实现一个系统,20 个用户同时打开网站,呈现出来一个群聊界面
解决方案
-
轮询:让
浏览器每隔2s向后台
发送一次请求,缺点:延迟,请求太多网站压力大 -
长轮询:
客户端向服务端
发送请求,服务器最多宕20s,一旦有数据接入,就立即返回。数据的响应没有延迟时间。 -
websocket:客户端和服务端创建连接后,不断开,实现
双向通信
轮询
-
访问 /home/ 显示的聊天室界面
-
点击发送内容,数据可以发送到后台
-
定时获取消息,发送到前端
长轮询
-
访问/home/ 显示聊天界面, → 每个用户创建一个队列
-
点击发送内容,数据也可以发送到后台 → 扔到每个用户的队列中
-
递归获取消息, 去自己的队列中获取数据,然后展示在界面中。
问题:
-
服务端持有连接,压力是否会很大?
如果基于IO多复用 + 异步,还会有这种情况吗? 可以
-
如果后台有100线程,同时对应100个用户的请求,则在等待期间(假设10s),这100个用户则一直占用县城,如果有更多的用户请求,则需等待释放。
webSocket
原来的web中:
-
http协议: 无状态 & 短连接
-
客户端主动连接服务端。
-
客户端向服务端发送消息,服务端接收后,返回数据
-
客户端接收到数据
-
断开连接
-
-
https协议 = http协议 + 对数据进行加密
我们在开发过程中,想要保留一些状态信息,基于Cookie来做
现在支持:
-
http协议:一次请求一次响应
-
websocket协议: 创建持有的连接不断开,基于这个连接可以进行收发数据。【服务端向客户端主动推送消息】
-
web聊天室
-
实时图标,柱状图、饼图(echarts)
-
WebSocket原理
-
http协议
-
连接
-
数据传输
-
断开连接
-
-
websocket 协议 → 建立在 http 协议之上
-
连接, 客户端发出请求
-
握手(验证), 客户端发送一段消息,后端接收到消息后,做一些特殊处理并返回。服务端支持 websocket 协议
https://www.***blogs.***/wupeiqi/p/6558766.html
-
-
客户端向服务端发送握手信息
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 进行拼接
Sec-WebSocket-Key: mnwFxiOlctXFN/DeMt1Amg==
magic_string = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
v1 = "mnwFxiOlctXFN/DeMt1Amg==" + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
v2 = hmac1(v1) // 通过 hmac1 进行加密
v3 = base64(v2) // 通过 base64 加密
- 返回数据
python">HTTP/1.1 101 Switching Protocols
Upgrade:websocket
Connection: Upgrade
Sec-WebSocket-A***ept: 密文
-
收发数据(加密)
-
先获取第 2 个字节,对应 8 位
-
在获取第二个字节的后 7 位
-
-
断开连接
Django 框架实现 WebSocket
Django 默认不支持WebSocket,安装第三方组件
pip install channels
版本不能超过4.0,最好是3.0.5,不然不能成功启动asgi
配置:
django channels - 武沛齐 - 博客园
- 注册 channels
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'channels',
'app01.apps.App01Config'
]
- 在 settings.py 中添加 asgi_application
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 ws_demo import routings
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ws_demo.settings')
# application = get_asgi_application()
# 支持 http 和 WebSocket 请求
application = ProtocolTypeRouter({
"http": get_asgi_application(), # 自动找 urls.py , 找视图函数 --》 http
"websocket": URLRouter(routings.websocket_urlpatterns), # routing(urls)、 consumers(views)
})
- 在 settings.py同级目录下,创建routings.py
from django.urls import re_path
from app01 import consumers
websocket_urlpatterns = [
# 示例 url : xxxxx/room/x1/
re_path(r"room/(?P<group>\w+)/$", consumers.ChatConsumer.as_asgi())
]
- 在 app01 目录下,创建consumers.py ,用于设置 WebSocket 请求
from channels.generic.websocket import WebsocketConsumer
from channels.exceptions import StopConsumer
class ChatConsumer(WebsocketConsumer): # 继承WebsocketConsumer
def websocket_connect(self, message):
print("有人进行连接了。。。。")
# 有客户端向后端发送 WebSocket 连接的请求时,自动触发(握手)
self.a***ept()
def websocket_receive(self, message):
# 浏览器基于 WebSocket 向后端发送数据,自动触发接收消息
print(message)
self.send("不要回复不要回复!!!")
def websocket_disconnect(self, message):
# 客户端向服务端断开连接时,自动触发
print("连接断开!!")
raise StopConsumer()
django 中,需要了解:
- wsg:
- asgi: wsgi + 异步 + WebSocket
聊天室
-
访问地址看到聊天室界面,使用 http 请求
-
让客户端主动向服务端发起 websocket连接,服务端接收到连接后,进行握手
-
客户端向后台发布WebSocket请求
var socket = new WebSocket("ws://localhost:8000/room/123/")
-
服务端接收消息
from channels.generic.websocket import WebsocketConsumer from channels.exceptions import StopConsumer class ChatConsumer(WebsocketConsumer): # 继承WebsocketConsumer def websocket_connect(self, message): print("有人进行连接了。。。。") # 有客户端向后端发送 WebSocket 连接的请求时,自动触发(握手) self.a***ept()
-
-
收发消息(客户端向服务端发消息)
- 客户端发送消息
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
.message {
height: 300px;
border: 1px solid #ddd;
width: 100%;
}
</style>
</head>
<body>
<div class="message" id="message"></div>
<div>
<label>
<input type="text" placeholder="请输入" id="txt">
</label>
<input type="button" value="发送" οnclick="sendMessage()">
</div>
<script>
// 创建websocket对象,向后台发送请求
let socket = new WebSocket("ws://localhost:8000/room/123/");
function sendMessage(){
let tag = document.getElementById("txt");
socket.send(tag.value);
}
</script>
</body>
</html>
- 服务端接收消息
def websocket_receive(self, message):
# 浏览器基于 WebSocket 向后端发送数据,自动触发接收消息
text = message["text"]
print("接收到的消息为:", text)
- 收发消息(服务端主动发给客户端)
# 连接之后,服务端给客户端发送消息
self.send("来了啊 !!!")
// 回调函数,客户端接收服务端消息
socket.onmessage = function (event){
console.log(event.data)
}
其他方法
// 创建websocket对象,向后台发送请求
let socket = new WebSocket("ws://localhost:8000/room/123/");
// 当客户端和服务端刚创建好连接(self.a***ept)之后,自动触发.
socket.onopen = function (event){
let tag = document.createElement("div");
tag.innerText = "[连接成功]";
document.getElementById("message").appendChild(tag);
}
// 回调函数,客户端接收服务端消息
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 closeMessage(){
socket.close();
}
function handleKeyPress(event){
if (event.keyCode === 13){
document.getElementById("send").click();
document.getElementById("txt").value = "";
}
}
document.onkeydown = handleKeyPress;
def websocket_connect(self, message):
print("有人进行连接了。。。。")
# 有客户端向后端发送 WebSocket 连接的请求时,自动触发(握手)
self.a***ept()
# 连接之后,服务端给客户端发送消息
self.send("来了啊 !!!")
def websocket_receive(self, message):
# 浏览器基于 WebSocket 向后端发送数据,自动触发接收消息
text = message["text"]
print("接收到的消息为:", text)
# 当接收的值为【关闭】,则服务端关闭连接
if text == "close":
self.close()
return
else:
self.send(text + "NB")
def websocket_disconnect(self, message):
# 客户端向服务端断开连接时,自动触发
print("断开连接!!!")
# 当客户端断开连接时,服务端也需关闭与客户端的连接,连接是双向的
raise StopConsumer()
小结
现在的交互还是停留在对某个人的操作
群聊
基于 channels 中提供的channel layers 来实现
- settings 中配置
# 声明基于内存的 channel layers
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels.layers.InMemoryChannelLayer"
}
}
也可声明基于 redis 的 channel layer → pip install channels-redis
# 基于redis 内存的 channel layers
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": ["redis://10.211.55.25:6379/1"]
}
}
}
- consumers 中特殊的代码
from channels.generic.websocket import WebsocketConsumer
from channels.exceptions import StopConsumer
from asgiref.sync import async_to_sync
class ChatConsumer(WebsocketConsumer): # 继承WebsocketConsumer
def websocket_connect(self, message):
# 接收客户端的连接
self.a***ept()
print("连接成功!!!")
# 获取群号
group_num = self.scope["url_route"]["kwargs"].get("group")
# 将这个客户端的链接对象添加到某个地方(内存或者 redis)
# self.channel_layer.group_add(group_num, self.channel_name)
async_to_sync(self.channel_layer.group_add)(group_num, self.channel_name)
def websocket_receive(self, message):
# 浏览器基于 WebSocket 向后端发送数据,自动触发接收消息
text = message["text"]
print("接收到的消息为:", text)
group_num = self.scope["url_route"]["kwargs"].get("group")
print("group_num", group_num)
# 通知组内的所有的客户端,执行 xx_oo方法,在方法中可以自定义任意的功能
# self.channel_layer.group_send(group_num, {"type": "xx.oo", "message": message})
async_to_sync(self.channel_layer.group_send)(group_num, {"type": "xx.oo", "message": message})
def xx_oo(self, event):
text = event["message"]["text"]
print("发送的 text:", text)
self.send(text) # 给组中的每一个人去发送消息
def websocket_disconnect(self, message):
# 客户端向服务端断开连接时,自动触发
print("断开连接!!!")
group_num = self.scope["url_route"]["kwargs"].get("group_num")
# self.channel_layer.group_discard(group_num, self.channel_name)
async_to_sync(self.channel_layer.group_discard)(group_num, self.channel_name)
raise StopConsumer()
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
.message {
height: 300px;
border: 1px solid #ddd;
width: 100%;
}
</style>
</head>
<body>
<div class="message" id="message"></div>
<div>
<label>
<input type="text" placeholder="请输入" id="txt">
</label>
<input type="button" value="发送" οnclick="sendMessage()" id="send">
<input type="button" value="关闭链接" οnclick="closeMessage()">
</div>
<script>
// 创建websocket对象,向后台发送请求
let socket = new WebSocket("ws://localhost:8000/room/{{ group_num }}/");
// 当客户端和服务端刚创建好连接(self.a***ept)之后,自动触发.
socket.onopen = function (event){
let tag = document.createElement("div");
tag.innerText = "[连接成功]";
document.getElementById("message").appendChild(tag);
}
// 回调函数,客户端接收服务端消息
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 closeMessage(){
socket.close();
}
function handleKeyPress(event){
if (event.keyCode === 13){
document.getElementById("send").click();
document.getElementById("txt").value = "";
}
}
document.onkeydown = handleKeyPress;
</script>
</body>
</html>
总结
-
WebSocket 是什么? 协议
-
django 中实现 WebSocket, channels 组件
-
单独连接和收发数据
-
手动创建列表 & channel layers
-
运维&运维开发的同学,使用 WebSocket 实现代码发布系统项目