Django Channels 实现 websocket 通讯

官方文档如下:

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))

 

你可能感兴趣的:(Python,Django,python)