websocket
websocket,web版的socket
原web中:
- http协议,无状态&短链接
- 客户端主动连接服务器
- 客户端向服务器发送消息,服务器收到返回数据
- 客户端收到数据
- 断开连接
- https一些 + 对数据进行加密。
我们在开发过程中想要保留一些状态信息,基于cookie来做
现在支持:
-
http协议。一次请求一次响应。
-
websocket协议,创建持久的连接不断开,基于这个连接可以进行收发数据。【服务端向客户端主动推送消息】
- web聊天室
- 实时图表格,柱状图,饼图(highcharts)
websocket原理
- http协议
- 连接
- 数据传输
- 端口连接
- webscoket协议,是建立在http协议之上的
- 连接,客户端发起
- 握手(验证),客户端发送一个消息,后端接收到消息再做一些特殊处理并返回。服务端支持websocket。
- 收发数据(加密)
- 断开连接。
django框架
django默认不支持websocket,需要安装组件:
pip install channels
pip install daphne
配置
-
注册channels, daphne 必须在 INSTALLED_APPS 中的 django.contrib.staticfiles 之前,所以建议这两个app注册到其他之前,daphne又需要在channels之前。
INSTALLED_APPS = [ "daphne", # 注册 "channels", # 注册 "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", ]
-
在settings.py中添加asgi_application
ASGI_APPLICATION = "项目名.asgi.application" # django3会自动生成application在asgi.py模块中
-
修改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(), # 自动走urls.py 找视图 就是之前正常的流程 "websocket": URLRouter(routing.websocket_urlpatterns), # 创建的routing就相当于之前的urls.py 创建的consumer.py 就相当于之前的views.py })
-
在settings.py的同级目录创建routing.py
from django.urls import re_path, path from app01 import consumers websocket_urlpatterns = [ # re_path(r"ws/(?P<group>\w+)/$", consumers.ChatConsumer.as_asgi()), path("ws/", consumers.ChatConsumer.as_asgi()) ]
-
在app01(就是你的app)目录下创建consumers.py,编写处理websocket的业务逻辑。
from channels.generic.websocket import WebsocketConsumer from channels.exceptions import StopConsumer class ChatConsumer(WebsocketConsumer): # 有客户端向后端发送websocket连接的请求时,自动触发。 def websocket_connect(self, message): # 服务端允许客户端创立连接 self.accept() # 浏览器基于websocket向后端发送数据,自动触发接收消息。 def websocket_receive(self, message): print(message) self.send("不要回复消息 不要") # self.close() 服务端也可以主动断开连接, self.close() # 客户端主动端口连接时,会自动触发 def websocket_disconnect(self, message): print("连接已断开") raise StopConsumer()
在django中需要了解的
wsgi,之前默认使用的都是wsgi
asgi,等于wsgi + 异步 + websocket
http
urls.py
views.py
websocket
routings.py
consumers.py
聊天室(单个人)
-
访问地址看到聊天室的页面,HTTP请求。
-
让客户端主动向服务端发起websocket连接,服务端接受到连接后通过(握手)。
-
客户端:
const socket = new WebSocket("ws://127.0.0.1:8888/"); // 必须以ws:开头
-
服务端
from channels.generic.websocket import WebsocketConsumer from channels.exceptions import StopConsumer class ChatConsumer(WebsocketConsumer): # 有客户端向后端发送websocket连接的请求时,自动触发。 def websocket_connect(self, message): print('收到请求了') # 服务端允许客户端创立连接 self.accept()
-
-
收发消息
- 客户端
socket.onopen = (event)=>{ socket.send("hello server!") };
- 服务端
class ChatConsumer(WebsocketConsumer):
# 上面的代码和之前一样
def websocket_receive(self, message):
# message是一个字典 {'type': 'websocket.receive', 'text': 'hello server!'}
print("收到客户端的消息", message.get('text')) # hello server!
# 服务端给客户端发消息
self.send("不要回复消息 不要")
# self.close() 服务端也可以主动断开连接, self.close()
```
-
收发消息(服务端主动发给客户端)
from channels.generic.websocket import WebsocketConsumer from channels.exceptions import StopConsumer class ChatConsumer(WebsocketConsumer): # 有客户端向后端发送websocket连接的请求时,自动触发。 def websocket_connect(self, message): # 服务端允许客户端创立连接 self.accept() # 连接建立后 服务端主动给客户端发送消息 self.send('客官您来啦~') # self 代表连接对象
-
断开连接(客户端主动断开)
-
客户端
socket.close()
-
服务端
from channels.generic.websocket import WebsocketConsumer from channels.exceptions import StopConsumer class ChatConsumer(WebsocketConsumer): # 其他代码一样 # 客户端主动端口连接时,会自动触发 def websocket_disconnect(self, message): print("连接已断开") # 这个异常表示同意断开 raise StopConsumer()
-
断开连接(客户端发送关键字,服务端收到后先断开服务端,然后触发客户端的回调函数onclose)
-
客户端
socket.send('关闭')
-
服务端
from channels.generic.websocket import WebsocketConsumer from channels.exceptions import StopConsumer class ChatConsumer(WebsocketConsumer): # 其他代码一样 def websocket_receive(self, message): text = message['text'] if text == '关闭': self.close() # 执行这个会触发客户端的 socket.onclose()回调函数,也会执行服务端自己的断开websocket_disconnect,总结就是只要客户端和服务端谁执行了close,整个连接就都断开了。
-
客户端
// 服务端主动断开连接的时候,该方法会被触发 socket.onclose = (event)=>{ console.log("链接已关闭"); }
-
补充
# 如果想要服务端主动断开连接的时候,不触发websocket_disconnect,那么在服务端断开之后,raise StopConsumer()即可 def websocket_receive(self, message): text = message['text'] if text == '关闭': self.close() # 执行这个会触发客户端的 socket.onclose()回调函数 raise StopConsumer() self.send(text + "谢谢!") # 客户端主动端口连接时,会自动触发,也就是这里只触发客户端断开的逻辑,而不是发送关键字让服务端断开的逻辑。 def websocket_disconnect(self, message): print("连接已断开") raise StopConsumer()
-
-
代码整合
# 后端
from channels.generic.websocket import WebsocketConsumer
from channels.exceptions import StopConsumer
class ChatConsumer(WebsocketConsumer):
def websocket_connect(self, message):
self.accept()
def websocket_receive(self, message):
text = message['text']
if text == '关闭':
self.close()
raise StopConsumer()
self.send(text + "谢谢!")
def websocket_disconnect(self, message):
print("连接已断开")
raise StopConsumer()
// 前端
const socket = new WebSocket("ws://127.0.0.1:8888/ws/");
// 创建好连接自动触发(服务端self.accept())后自动触发。
socket.onopen = (event)=>{
this.isShow = true
// 往服务端发送消息
socket.send(this.msg)
};
// 当websocket收到服务端的消息时,会自动触发这个函数
// 数据放在event里面,通过event.data能获取到数据的内容
socket.onmessage = (event)=>{
this.messageList.push({
id: this.messageList.length + 1,
data: event.data
})
};
// 如果有错误
socket.onerror = (error)=>{
console.log("websocket error:", error);
};
// 服务端主动断开连时,这个方法会触发
socket.onclose = (event)=>{
swal('服务端已断开链接')
console.log("websocket connection closed:", event);
}
},
小结:
基于django实现websocket请求,但只能对某个人进行处理。主要就是后端的self
聊天室(群聊一)
修改后端即可
from channels.generic.websocket import WebsocketConsumer
from channels.exceptions import StopConsumer
# 1. 创建一个列表,存储全部连接对象
# 因为每一个self都是一个连接对象,所以把他接入到一个列表中
CONN_LIST= []
class ChatConsumer(WebsocketConsumer):
def websocket_connect(self, message):
self.accept()
CONN_LIST.append(self) # 把连接对象加入到列表
def websocket_receive(self, message):
text = message['text']
for conn in CONN_LIST: # 循环遍历列表,拿到每一个连接对象,给每一个连接对象发消息
conn.send(text + "消息已收到,这是自动回复。")
def websocket_disconnect(self, message):
CONN_LIST.remove(self) # 当连接断开后,把这个连接对象送对象列表中移除
raise StopConsumer()
前端代码
<template>
<div class="home">
<div class="content">
<p v-show="isShow">[链接建立成功]</p>
<hr v-show="isShow">
<ul v-for="(item, index) in messageList" :key="item.id">
<li>收到来自服务端的消息:{{ index }}-{{ item.data }}</li>
</ul>
</div>
<div class="inner">
<label for="message">请输入:</label>
<el-input id="message" v-model="msg"></el-input>
<p>
<el-row>
<el-button type="success" @click="sendWS">发送</el-button>
<el-button type="danger" @click="closeConn">关闭连接</el-button>
<el-button type="warning" @click="messageList=[]">清空消息</el-button>
</el-row>
</p>
</div>
</div>
</template>
<script>
export default {
data() {
return {
msg: '',
isShow: false,
messageList: []
}
},
methods: {
sendWS(){
const socket = new WebSocket("ws://127.0.0.1:8888/");
// 创建好连接自动触发(服务端self.accept())后自动触发。
socket.onopen = (event)=>{
this.isShow = true
// 往服务端发送消息
socket.send(this.msg)
};
// 当websocket收到服务端的消息时,会自动触发这个函数
// 数据放在event里面,通过event.data能获取到数据的内容
socket.onmessage = (event)=>{
this.messageList.push({
id: this.messageList.length + 1,
data: event.data
})
};
// 如果有错误
socket.onerror = (error)=>{
console.log("websocket error:", error);
};
// 服务端主动断开连时,这个方法会触发
socket.onclose = (event)=>{
swal('服务端已断开链接')
console.log("websocket connection closed:", event);
}
},
// 关闭连接函数
closeConn(){
swal("关闭连接函数已触发")
socket.close()
}
},
created(){
let token = sessionStorage.getItem("token")
if (!token){
swal('请先登录').then(()=>{
this.$router.push("/login")
})
}
}
}
</script>
<style scoped>
.home .content {
border: 1px solid #dddddd;
width: 800px;
height: 500px;
margin: 50px auto;
overflow: auto;
}
.home .inner {
width: 800px;
margin: 50px auto;
}
</style>
聊天室(群聊二)
基于channels中提供的channel layers来实现
-
settings中配置
CHANNEL_LAYERS = { "default": { "BACKEND": "channels.layers.InMemoryChannelLayer" } }
如果想要换成redis的
pip3 install channels-redis
# 写法1 CHANNEL_LAYERS = { "default": { "BACKEND": "channels_redis.core.RedisChannelLayer", "CONFIG": { "hosts": [('IP', 6379)] }, }, } # 写法2 CHANNEL_LAYERS = { 'default': { 'BACKEND': 'channels_redis.core.RedisChannelLayer', 'CONFIG': {"hosts": ["redis://IP:6379/1"],}, }, } # 写法3 CHANNEL_LAYERS = { "default": { "BACKEND": "channels_redis.core.RedisChannelLayer", "CONFIG": { "hosts": ["redis://:password@IP: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):
# 有客户端向后端发送websocket连接的请求时,自动触发。
def websocket_connect(self, message):
# 服务端允许客户端创立连接
self.accept()
# 将这个客户端的连接对象加入到某个地方(内容 or redis)
# xxx 表示一个群名,随便写 slef.channel_name 表示一个随机的别名
# 需要将异步转成同步,用同步的方式做
self.group = self.scope['url_route']['kwargs']['room_id']
async_to_sync(self.channel_layer.group_add)(self.group, self.channel_name)
# 浏览器基于websocket向后端发送数据,自动触发接收消息。
def websocket_receive(self, message):
text = message['text']
if text == '关闭':
self.close() # 执行这个会触发客户端的 socket.onclose()回调函数
raise StopConsumer()
return
# 通知组内的所有客户端,执行xx_oo 方法,在此方法中可以自己取定义任意的功能,只是在字段的值中需要写成 xx.oo
async_to_sync(self.channel_layer.group_send)(self.group, {"type":"xx.oo", "message": message})
def xx_oo(self, event):
text = event["message"]['text']
# 这里的send表示给组里的每一个人send不是单独发哪一个,就是群聊里面发
self.send(text + "哒咩")
# 客户端主动端口连接时,会自动触发
def websocket_disconnect(self, message):
print("连接已断开")
# 连接断开,将这个连接从组里移除掉
async_to_sync(self.channel_layer.group_discard)(self.group, self.channel_name)
raise StopConsumer()
```
-
routing的路径为
from django.urls import path from app01 import consumers websocket_urlpatterns = [ path("chat/<str:room_id>/", consumers.ChatConsumer.as_asgi())
-
前端访问的地址为
const socket = new WebSocket("ws://127.0.0.1:8888/chat/123/");
简单总结
- websocket是什么?协议。
- django中实现websocket,channels组件。
- 单独连接和收发数据
- 手动创建连接表 & channel layers