最近做的Django项目中有一条需求是当用户写完邮件并选择抄送用户,用户发送邮件后,服务器主动会给在所有抄送用户发送一条提示。这里可以使用支持WebSoket协议的Channels。
思路简述:
- 用户登录时,浏览器会主动发送器WebScoket连接,服务器接到连接后,将用户加入以用户ID标识的组群中,并返回接受连接的信息;
- 发送邮件时,会遍历抄送用户的ID,根据他们的ID,一次向相应的组群发送消息
为了验证浏览器缺失收到了信息,先写一个简单的html页面用于显示收到的信息:
<!-- static/templates/notificationMsg.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Chat Room</title>
</head>
<body>
<textarea id="notificationMsg-log" cols="100" rows="20"></textarea><br/>
</body>
<script>
var userID = {
{
userID }};
var notificationMsgSocket = new WebSocket(
'ws://' + window.location.host +
'/ws/notifications/' + userID + '/'); // 客户端发起WebSocket,并返回WebSocket对象命名为notificationMsgSocket
notificationMsgSocket.onmessage = function(e) {
// 监听服务器发回的消息
var data = JSON.parse(e.data); // 将消息中data的值转为JS对象
var message = data['message']; // 获取其中message的值
alert(message)
document.querySelector('#notificationMsg-log').value += (message + '\n'); // 将message的值添加到notificationMsg-log中并换行
};
notificationMsgSocket.onclose = function(e) {
// WebSocket的readyState变为CLOSED时调用
console.error('Notification socket closed unexpectedly'); // 在控制台输出错误信息
};
</script>
</html>
这个界面是根据channels文档给出的示例简化的,页面只有一个文本框,在收到服务器发送的消息时会显示出来。
消费者,事件
channels中用消费者(consumer)处理WebSocket中的事件(event),消费者可以理解为Django中的视图,事件可以看作HTTP中的request。
在ASGI中,事件以字典的形式传递给服务器,使用’type’键标记具体事件类型,不同的事件由同名的消费者处理,比如在这个项目中,每次发送’type’为’get_notification’的事件,则需要定义一个名为’get_notification’的消费者用于接受事件作出响应。
在视图中发送邮件通知:
13 from asgiref.sync import async_to_sync
14 from channels.layers import get_channel_layer
...
31 class IssuedReportsView(View):
...
110 def post(self, request):
...
160 ¦ send_message_users = set(copy_to_user+supervise_user) # 将抄送人与审阅人的id放到同一个集合中
161 ¦ channel_layer = get_channel_layer() # 实例化get_channel_layer()对象,用于向组群发送消息
162 ¦ for user in send_message_users: # 遍历需要发送消息的用户id
163 ¦ ¦ async_to_sync(channel_layer.group_send)( # ASGI是异步的,这里转为同步操作;通过通信层向组群发送消息
164 ¦ ¦ ¦ "notification_"+str(user), # 用户所在的组群
165 ¦ ¦ ¦ {
166 ¦ ¦ ¦ 'type':'get_notification', # 标记发送事件的type
167 ¦ ¦ ¦ 'message': 'you have got a new report', # 提示信息
168 ¦ ¦ ¦ }
169 ¦ ¦ )
定义channels的路由
channels需要一个根路由,官方建议放在配置目录下和settings.py同路径,用于分配不同的协议连接到不同的路由文件,这里仅处理WebSocket文件:
1 # myproject/routing.py
2 from channels.routing import ProtocolTypeRouter, URLRouter
3 from channels.auth import AuthMiddlewareStack
4 import consumer.routing
5
6
7 application = ProtocolTypeRouter({
# 协议路由
8 # (http->django views is added by default)
9 'websocket': AuthMiddlewareStack( # 分发WebSocket连接,使用channels中的认证中间件,根据Django赋予request的session判断用户身份
10 ¦ URLRouter(
11 ¦ ¦ consumer.routing.websocket_urlpatterns # 根据consumer目录下的routing.py中的websocket_urlpatterns进一步分发websocket请求
12 ¦ )
13 ),
14 })
根路由中指向的路由文件如下:
1 # myproject/consumer/routing.py
2
3 from django.urls import re_path
4 from channels.auth import AuthMiddlewareStack
5 from consumer.Consumers import Consumers
6
7
8 websocket_urlpatterns = [
9 re_path(r'ws/notifications/(?P<userID>\w+)/$', Consumers), # 使用django的re_path方法,将匹配到的path交给不同的消费者处理
10 ]
注册
在settings.py中注册:
# 首先在INSTALLED_APPS中注册
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'report',
'channels', # 注册channels
]
...
# 指定根路由文件,和下面的通信层一起放在文件尾部即可
ASGI_APPLICATION = 'starxteam.routing.application'
# 消费者中用到了通信层\channels.layer用于跨应用传递数据
# 但是需要使用第三方应用支持这个功能,官方推荐channels_redis
# 安装channels时默认安装了channels_redis
CHANNEL_LAYERS = {
'default':{
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
'hosts': [('127.0.0.1', 6379)],
},
},
}
使用POSTMAN测试一下:
前端收到了消息: