问: websocket是什么?
websocket是在HTML5开始提供的一种在单个TCP上进行全双工通讯的协议
问:为什么要引入WebSocket协议呢?
因为HTTP协议是一种无状态、短连接、单向的应用层协议,采用的是请求/响应模式,通信请求只能由客户端主动发起,服务端只能被动的响应,
弊端: HTTP协议无法实现服务器主动向客户端发起消息
websocket协议可以创建持久的连接不断开,基于这个连接可以进行收发数据
【适用于服务端向客户端主动推送消息的场景】
- web聊天室
- 监控平台的实时图表更新展示
问: HTTP协议能实现服务端和客户端的双向通信吗?
可以
, 大多数Web应用程序通过将频繁的异步AJAX
请求实现长轮询
(在浏览器端定时的给服务器发请求,拿到服务器的最新的数据展示出来,因为是异步,所以用户感知不到),但轮询的效率低,浪费资源(因为每次都需要重新连接,或者保持HTTP
连接始终打开)
http协议
websocket协议(建立在http协议之上)
# 下面为HTTP请求头中的信息
GET /chatsocket HTTP/1.1
Host: localhost
Connection: Upgrade # 标识该HTTP请求时一个协议升级请求
Upgrade: websocket # 协议升级为WebSocket协议
Origin: http://localhost:63342
Sec-WebSocket-Version: 13 # 客户端支持WebSocket的版本
Sec-WebSocket-Key: mnwFxiOlctXFN/DeMt1Amg== # (重要)随机字符串
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
....
\r\n
\r\n
重要(目的是为了验证服务端是否支持WebSocket协议)
服务端接收后客户端发过来的连接请求后
从请求【握手】信息中提取 Sec-WebSocket-Key
利用
magic_string
和Sec-WebSocket-Key
进行hmac1
加密,再进行base64
加密得到密文
将加密结果响应给客户端
注:magic string为:258EAFA5-E914-47DA-95CA-C5AB0DC85B11
服务端返回响应数据
HTTP/1.1 101 Switching Protocols
Upgrade:websocket
Connection: Upgrade
Sec-WebSocket-Accept: base64(hmac1(Sec-WebSocket-Key + magic string)) // 服务端加密后返回给客户端
收发数据
断开连接
上述流程总结
在浏览器中生成一个随机字符串,进行加密得到密文
再将上一步生成的随机字符串发送给服务器,服务器拿到随机字符串后也可进行加密
服务端将生成的密文返回给客户端,客户端将服务端发过来的密文与自己生成的密文进行比较,经过比 较二者值相同就说明服务端支持websocket协议(前提:服务端和客户端加密算法一样)
创建应用
python manage.py startapp app01
django
默认不支持websocket
,需要安装组件,这里博主安装的Django
和channels
的版本都是4.x
开头的
pip install channels # 默认是当前最新的版本
# django2.x 需要匹配安装 channels 2
# django3.x 需要匹配安装 channels 3
# 版本搞错,django 就不能正常启动 ASGI_APPLICATION
注意:
WSGI: (Web Server Gateway Interface)服务器网关接口,一种描述web server如何与web application通信的规范,用于接收用户请求并将请求进行初次封装然后将请求交给web框架(wsgiref:本质就是编写一个socket服务端用于接收用户请求)
ASGI: ASGI是WSGI的扩展异步Python标准,比只能同步的WSGI性能更好,且可支持websocket,在 Django3+ 和 Flask2+ 中得到支持
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', # 注册你自己的app应用
]
settings.py
中添加asgi_application
# asgi.py文件在我的djangoProjects目录下,这里你需要根据自己的配置来
ASGI_APPLICATION = "djangoProjects.asgi.application"
settings.py
的同级目录下创建 routings.py
(给websocket协议提供路由匹配) 这就等价于urls.py
(给http协议提供路由匹配)from django.urls import re_path
from app01 import consumers # 从你的app应用中导入
websocket_urlpatterns = [
re_path(r'room/(?P\w+)/$' , consumers.ChatConsumer.as_asgi()),
]
asgi.py
文件import os
from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from djangoProject import routings
# 和你自己的项目名称保持一致
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'djangoProject.settings')
# 支持 http 和 websocket
application = ProtocolTypeRouter(
{
"http": get_asgi_application(), # 对http请求:自动找urls.py,根据路由找视图函数views.py
# 对websocket请求: 自动找routings.py(urls.py)、根据路由找视图:consumers.py(views.py)
"websocket": URLRouter(routings.websocket_urlpatterns),
}
)
app01
目录下创建cosummer.py
,编写处理websocket请求的业务逻辑from channels.generic.websocket import WebsocketConsumer
from channels.exceptions import StopConsumer
class ChatConsumer(WebsocketConsumer):
# 有客户端向后端发送websocket连接的请求时自动触发
def websocket_connect(self, message):
print("客户端发来连接请求")
# 服务端允许和客户端创建连接(即上文所介绍的握手,无需关心上文中所介绍的握手细节,这里已经做好封装)
self.accept()
# 浏览器基于websocket向后端发送数据,自动触发接收消息
def websocket_receive(self, message):
text = message['text'] # 根据text字段提取客户端发过来的消息的文本内容
print(f"客户端发来了{text} 消息")
if not text:
self.send("你发个空消息干嘛")
else:
self.send(f"{text} SB")
# 客户端主动与服务端断开连接时自动触发
def websocket_disconnect(self, message):
print("客户端发起断开连接请求")
raise StopConsumer()
接下来开始实现客户端的配置
下面的API用户创建一个websocket对象
var ws_socket = new WebSocket(url)
事件 | 事件处理程序 | 描述 |
---|---|---|
open | websocket对象.onopen | 连接建立时触发 |
message | websocket对象.onmessage | 客户端接收服务端数据时触发 |
error | websocket对象.onerror | 通信发生错误时触发 |
close | websocket对象.onclose | 连接关闭时触发 |
方法 | 描述 |
---|---|
send() | 使用连接发送数据 |
用户发起
http
请求,根据urls.py
进行路由匹配,找到对应的views.py
下面的视图函数,返回对应的index.html
页面
让客户端主动向服务端发起
websocket
连接,服务端接收到连接后进行握手验证
- 客户端发起
websocket
请求- 收发消息-客户端向服务端发消息
app01/templates/index.html
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WebSocket客户端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()">
<input type="button" value="关闭连接" onclick="closeConn()">
div>
<script>
// 客户端向服务端发起连接请求
socket = new WebSocket("ws://127.0.0.1:8001/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>
ASGI部署
配置完毕后,你需要运行你写的django程序,如果你的使用的django和channels版本和博主使用的一样都是最新的则根据下面来,如果django
和channels
的版本使用的是2.x
,3.x
请自行在网上找部署说明,这个不难:
pip install daphne
# 安装完毕后使用daphne启动服务进程
# 项目名称和端口请与你自己的保持一致
daphne djangoProject.asgi:application -b 127.0.0.1 -p 8001
部署完毕后打开你的浏览器访问http://127.0.0.1:8001/index/
可以看到如下页面,客户端可以持续给服务端发送消息,服务端也能将浏览器发过来的消息加以SB
后缀返回
就目前而言,上文基于django
实现的websocket
请求只能对某个浏览器创建连接,进行收发消息,还未实现群聊的功能
基于channels
中提供的channnels layer
来实现
基于上面的工程修改相关文件
settings.py
中配置
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels.layers.InMemoryChannelLayer",
}
}
app01/templates/index.html
// 客户端向服务端发起连接请求
socket = new WebSocket("ws://127.0.0.1:8001/room/{{ group_num }}/");
app01/views.py
from django.shortcuts import render
def index(request):
group_num = request.GET.get("num")
return render(request, 'index.html', {"group_num": group_num})
consummers.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()
# 获取群号,获取路由匹配中的
group = self.scope['url_route']['kwargs'].get("group")
# 将这个客户端的连接对象加入到某个地方内存, group:群号
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()
重新运行django服务
# 项目名称和端口请与你自己的保持一致
daphne djangoProject.asgi:application -b 127.0.0.1 -p 8001
同时开启三个客户端,保持群号差异,分别发送消息,可以看到如下效果,即我们实现了一个非常简易的群聊功能
非常感谢你能看到这里,到此为止,我们对于websocke
t的介绍以及基于Django Channels
实现一个简单的群聊系统,赶紧动起手来,Ctrl+C, Ctrl+V感受一些WebSocket的魅力把~,博主水平有限,不足之处还请大家在评论区中指出