Получение списка подключенных пользователей в комнате с помощью 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
, мне приходят в голову две идеи (первая, возможно, лучше второй):
Использовать уровень канала в памяти для отслеживания того, какие пользователи подключаются/отключаются. При этом я хотел бы, чтобы список пользователей возвращался обратно установленному соединению (т.е. клиенту) при подключении. Как это можно сделать?
Альтернативно, я видел примеры, где мы можем отслеживать активность пользователя (т.е. онлайн/отсутствие/оффлайн и т.д.) с помощью базы данных. Для меня это не совсем оптимальное решение, так как я не хочу запрашивать базу данных для такого рода активности.
.
Если есть лучшее решение, я весь внимание, но по существу, как это можно сделать?
Вы можете создать модель 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
}))
Для отправки сообщения при отключении вы также будете следовать той же схеме.
Конечно, вы должны быть осторожны, чтобы не перегрузить память, поэтому вы можете установить ограничения на количество пользователей, которые могут быть сохранены для одной комнаты, прежде чем передавать ее в базу данных.
Надеюсь, это поможет! Мне интересно услышать, какие преимущества/недостатки видят другие в этом подходе по сравнению с обращением к БД.