官方文档如下:
https://channels.readthedocs.io/en/stable/introduction.html
Channels改变Django在下面和通过Django的同步核心编织异步代码,允许Django项目不仅处理HTTP,还需要处理需要长时间连接的协议 - WebSockets,MQTT,chatbots,业余无线电等等。
它在保留Django同步和易用性的同时实现了这一点,允许您选择编写代码的方式 - 以Django视图,完全异步或两者混合的方式同步。除此之外,它还提供了与Django的auth系统,会话系统等的集成,使您可以比以往更轻松地将仅HTTP项目扩展到其他协议。
需求:消息实时推送消息以及通知功能,采用django-channels来实现websocket进行实时通讯。
话不多说,直接上代码:
配置settings.py
if envs.ASGI:
ASGI_APPLICATION = 'cashier.routing.application'
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
"hosts": [f'redis://:{envs.Redis.PASSWORD}@{envs.Redis.SERVER}:{envs.Redis.PORT}/4'],
},
},
}
routing.py
from channels.routing import (
ProtocolTypeRouter,
URLRouter,
)
from cashier.websocket.urls import websocket_urlpatterns
application = ProtocolTypeRouter({
'websocket': URLRouter(websocket_urlpatterns)
})
services.py
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
def build_ws_message(event: str, message: str = None, data: dict = None) -> dict:
"""
构造一条 WebSocket 消息
:param event: 消息主题,参照 WebsocketEvents
:param message: 消息文本
:param data: 消息附带数据
"""
message = message or ''
data = data or {}
return {
'event': event,
'message': message,
'data': data,
}
def publish_ws_message(to_shop_id: int, event: str, message: str = None, data: dict = None):
"""
向客户端发送 WebSocket 消息
:param to_shop_id: 发送目标店铺的 ID
:param event: 消息主题,参照 WebsocketEvents
:param message: 消息文本
:param data: 消息附带数据
"""
channel_layer = get_channel_layer()
async_to_sync(channel_layer.group_send)(
f'wsgroup-{to_shop_id}', {
'type': 'on_ws_message_published',
'event': event,
'message': message,
'data': data,
}
)
业务代码 consumers.py
import json
from urllib.parse import parse_qs
from asgiref.sync import async_to_sync
from channels.generic.websocket import (
WebsocketConsumer,
)
from cashier.staff.failures import CashierSeatInsufficient
from cashier.staff.services import (
cashier_offline,
delete_offline_key,
get_cashier_by_staff_id,
assert_cashier_seat_sufficient,
renew_cashier_online_status,
is_staff_taken_offline,
get_cashier_offline_operator_name,
)
from cashier.websocket.constants import WebsocketEvents
from cashier.websocket.services import build_ws_message
class WebsocketConnection(WebsocketConsumer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.group_name = 'wsgroup-0'
self.staff_id = None
self.staff = None
self.cashier_sn = None
self.shop_id = None
def connect(self):
args = parse_qs(self.scope['query_string'].decode())
staff_id = args.get('staff_id')
cashier_sn = args.get('cashier_sn')
shop_id = args.get('shop_id')
self.staff_id = int(staff_id[0]) if staff_id else None
self.cashier_sn = cashier_sn[0] if cashier_sn else None
self.shop_id = int(shop_id[0]) if shop_id else None
# 开始接收消息
self.accept()
if not self.cashier_sn:
self.send_ws_message_to_client(build_ws_message(
event=WebsocketEvents.ERROR_OCCURRED,
message=f'Cashier SN invalid: {self.cashier_sn}',
))
self.close()
return
self.staff = get_cashier_by_staff_id(staff_id=self.staff_id, shop_id=self.shop_id)
if not self.staff:
self.send_ws_message_to_client(build_ws_message(
event=WebsocketEvents.ERROR_OCCURRED,
message=f'Permission denied for staff {self.staff_id}',
))
self.close()
return
# 加入群组
self.group_name = f'wsgroup-{self.shop_id}'
async_to_sync(self.channel_layer.group_add)(
self.group_name,
self.channel_name,
)
# 发送回执
self.send_ws_message_to_client(build_ws_message(
event=WebsocketEvents.OK,
))
def disconnect(self, close_code):
if self.staff:
# 撤回收银员下线通知
delete_offline_key(self.staff, self.cashier_sn)
# 标记收银员下线
cashier_offline(
cashier_staff=self.staff,
cashier_sn=self.cashier_sn,
)
# 退出群组
async_to_sync(self.channel_layer.group_discard)(
self.group_name,
self.channel_name,
)
def receive(self, text_data=None, bytes_data=None):
"""
接到客户端发来的消息
"""
if self.cashier_sn != '123141': # 零售通 SN 号不考虑台位和踢下线
# 尝试续期收银员在线状态
try:
assert_cashier_seat_sufficient(
cashier_staff=self.staff,
cashier_sn=self.cashier_sn,
)
except CashierSeatInsufficient as e:
# 台位不足,关闭连接
self.send_ws_message_to_client(build_ws_message(
event=WebsocketEvents.CASHIER_SEAT_INSUFFICIENT,
))
self.close()
return
renew_cashier_online_status(
cashier_staff=self.staff,
cashier_sn=self.cashier_sn,
)
# 主动查询员工是否被要求下线
if is_staff_taken_offline(
cashier_staff=self.staff,
cashier_sn=self.cashier_sn,
):
operator_name = get_cashier_offline_operator_name(self.staff, self.cashier_sn) or '未知管理员'
self.send_ws_message_to_client(build_ws_message(
event=WebsocketEvents.TAKEN_OFFLINE,
data={
'admin_name': operator_name,
}
))
self.close()
return
# 发送回执
self.send_ws_message_to_client(build_ws_message(
event=WebsocketEvents.OK,
))
def on_ws_message_published(self, channel_event: dict):
"""
group_send 事件回调,向客户端发送消息
:param channel_event: channels 消息,发送自 publish_ws_message
"""
event = channel_event['event']
message = channel_event['message']
data = channel_event['data']
self.send_ws_message_to_client(build_ws_message(
event=event,
message=message,
data=data,
))
def send_ws_message_to_client(self, ws_message: dict):
"""
向客户端发送消息
:param ws_message: 消息体,将转为文本发送到客户端
"""
self.send(text_data=json.dumps(ws_message))