Получение списка подключенных пользователей в комнате с помощью django channels

Как получить список подключенных пользователей к комнате внутри канала с помощью Django Channels? Я знаю, что этот вопрос задавался много раз, но, честно говоря, я перерыл весь интернет и очень удивлен, что нет обычного способа сделать это. Согласно этому ответу, Django Channels избегает этого сценария, однако, есть другой пакет, Django Presence, который может это сделать.

На данный момент у меня есть очень простое приложение, где веб-сокет передает данные, которые отправляются от каждого клиента, так что каждый подключенный клиент в комнате получает эти данные. У меня также есть простая аутентификация, так что только аутентифицированные пользователи могут подключаться. Вот что у меня есть:

routing.py

application = ProtocolTypeRouter(
    {
        "websocket": TokenAuthMiddleware(
            URLRouter([
                re_path(r'ws/(?P<room_name>\w+)/$', consumers.MyConsumer.as_asgi()),
            ])
        )
    }
)

middleware.py

class TokenAuthMiddleware(BaseMiddleware):
    def __init__(self, inner):
        super().__init__(inner)

    async def __call__(self, scope, receive, send):
        # Get the token
        token = parse_qs(scope["query_string"].decode("utf8"))["token"][0]

        #custom code to validate token
        if token_is_valid:
            # assign scope['user'] = user
        else:
            scope['user'] = AnonymousUser()

        return await super().__call__(scope, receive, send)

consumer.py

    def connect(self):
        user = self.scope["user"]

        if user.is_anonymous:
            self.close()
        else:
            self.room_name = self.scope['url_route']['kwargs']['room_name']
            async_to_sync(self.channel_layer.group_add) (
                self.room_name,
                self.channel_name
            )

            self.accept() # WHAT CAN I DO HERE TO RETURN THE LIST OF CONNECTED USERS IN THIS SAME ROOM NAME?

    def disconnect(self, close_code):
        async_to_sync(self.channel_layer.group_discard) (
            self.room_name,
            self.channel_name
        )

что касается моего комментария в consumers, мне приходят в голову две идеи (первая, возможно, лучше второй):

  1. Использовать уровень канала в памяти для отслеживания того, какие пользователи подключаются/отключаются. При этом я хотел бы, чтобы список пользователей возвращался обратно установленному соединению (т.е. клиенту) при подключении. Как это можно сделать?

  2. Альтернативно, я видел примеры, где мы можем отслеживать активность пользователя (т.е. онлайн/отсутствие/оффлайн и т.д.) с помощью базы данных. Для меня это не совсем оптимальное решение, так как я не хочу запрашивать базу данных для такого рода активности.

    .

Если есть лучшее решение, я весь внимание, но по существу, как это можно сделать?

Вы можете создать модель chat, которая будет представлять это room

class Chat(models.Model):

    #collect in charfield `pk` of all users online 
    users_online = models.CharField(max_length=1500)

тогда, когда любой пользователь подключается к websocket, добавьте его в поле Chat's users_online:

def connect(self):
    user = self.scope["user"]

    if user.is_anonymous:
        self.close()
    else:
        self.room_name = self.scope['url_route']['kwargs']['room_name']
        async_to_sync(self.channel_layer.group_add) (
            self.room_name,
            self.channel_name
        )

        self.accept() 
        
        #after user successfully conected
        chat_object = Chat.objects.get(get_chat_object = here)
        # first option append it with semicolon, so later you could split list
        chat_object.users_online.append(str(user.pk)+";")
        self.custom_user_list = []

Когда пользователь отключается от вашего вебсокета, remove пользователь pk из поля users_online

Если вы используете RedisChannelLayer, расширьте его следующим методом:

class ExtendedRedisChannelLayer(RedisChannelLayer):

    async def get_group_channels(self, group):
        assert self.valid_group_name(group), "Group name not valid"
        key = self._group_key(group)
        connection = self.connection(self.consistent_hash(group))
        # Discard old channels based on group_expiry
        await connection.zremrangebyscore(
            key, min=0, max=int(time.time()) - self.group_expiry
        )

        return [x.decode("utf8") for x in await connection.zrange(key, 0, -1)]

определите в настройках:

CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "your_path.ExtendedRedisChannelLayer",
        ...
    }
}

использовать внутри потребителя:

group_channels = await self.channel_layer.get_group_channels('group_name')

Хотя я не тестировал это в производстве, я смог справиться с этим, добавив defaultdict в качестве атрибута класса к моему потребителю, и отслеживая соответствующую информацию о соединении в памяти. Для моего случая использования меня интересовало только количество пользователей:

from collections import defaultdict
from channels.generic.websocket import AsyncWebsocketConsumer

class MyConsumer(AsyncWebsocketConsumer):
        room_connection_counts = defaultdict(lambda: 0)

        async def connect(self):
            self.user = self.scope['user']
            self.room_name = self.scope['url_route']['kwargs']['room_name']
            self.room_group_name = f'group_{self.room_name}'

            await self.channel_layer.group_add(
                self.room_group_name,
                self.channel_name,
            )

            await self.accept()

            self.room_connection_counts[self.room_name] += 1

            print(
                "Connection Count:",
                self.room_connection_counts[self.room_name]
            )

        async def disconnect(self):
            await self.channel_layer.group_discard(
                self.room_group_name,
                self.channel_name,
            )
            self.room_connection_counts[self.room_name] -= 1
            print(
                "Connection Count:",
                self.room_connection_counts[self.room_name]
            )

В вашем случае вы хотите отслеживать список подключенных пользователей. Для этого нужно изменить room_connection_counts defaultdict так, чтобы по умолчанию возвращался list или set пользователей. Например, в теле класса можно использовать:

room_connected_users = defaultdict(set)

А в методе connect вы бы использовали:

self.room_connected_users[self.room_name].add(self.user)

А в разъединении:

self.room_connected_users[self.room_name].remove(self.user)

Что касается второй части вашего вопроса, вы хотите отправить список подключенных пользователей обратно в группу каналов. Вы можете сделать это, добавив следующее в метод connect:

async def connect(self):
    ...
    await self.channel_layer.group_send(
        self.room_group_name,
        {
            'type': 'user_connect',
            'message': list(self.room_connected_users[self.room_name])
        }
    )

И определите user_connect метод в вашем потребителе, который будет вызываться методом group_send:

async def user_connect(self, event):
    message = event['message']

    await self.send(text_data=json.dumps({
        'type': 'player_connect',
        'message': message
    }))

Для отправки сообщения при отключении вы также будете следовать той же схеме.

Конечно, вы должны быть осторожны, чтобы не перегрузить память, поэтому вы можете установить ограничения на количество пользователей, которые могут быть сохранены для одной комнаты, прежде чем передавать ее в базу данных.

Надеюсь, это поможет! Мне интересно услышать, какие преимущества/недостатки видят другие в этом подходе по сравнению с обращением к БД.

Вернуться на верх