Django聊天室(二)实现聊天室服务

 2018年5月8日 15:19   Nick王   开发    0 评论   312 浏览 

Django 版本:1.11.13
Django-Channels 版本:2.1.1
Python 版本:3.6.5

Django聊天室(二)实现聊天室服务

本章我们会实现聊天室视图,来实现聊天功能。

增加room视图

我们现在创建第二个视图,一个房间视图,可以让您看到发布在特定聊天室中的消息。

创建聊天室页面(chat/templates/chat/room.html):

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8"/>
    <title>Chat Room</title>
</head>
<body>
    <textarea id="chat-log" cols="100" rows="20"></textarea><br/>
    <input id="chat-message-input" type="text" size="100"/><br/>
    <input id="chat-message-submit" type="button" value="Send"/>
</body>
<script>
    var roomName = {{ room_name_json }};

    var chatSocket = new WebSocket(
        'ws://' + window.location.host +
        '/ws/chat/' + roomName + '/');

    chatSocket.onmessage = function(e) {
        var data = JSON.parse(e.data);
        var message = data['message'];
        document.querySelector('#chat-log').value += (message + '\n');
    };

    chatSocket.onclose = function(e) {
        console.error('Chat socket closed unexpectedly');
    };

    document.querySelector('#chat-message-input').focus();
    document.querySelector('#chat-message-input').onkeyup = function(e) {
        if (e.keyCode === 13) {  // enter, return
            document.querySelector('#chat-message-submit').click();
        }
    };

    document.querySelector('#chat-message-submit').onclick = function(e) {
        var messageInputDom = document.querySelector('#chat-message-input');
        var message = messageInputDom.value;
        chatSocket.send(JSON.stringify({
            'message': message
        }));

        messageInputDom.value = '';
    };
</script>
</html>

编写room视图(chat/views.py):

from django.shortcuts import render
from django.utils.safestring import mark_safe
import json


def index(request):
    return render(request, template_name='chat/index.html', context={})


def room(request, room_name):
    return render(request, template_name='chat/room.html', context={
        'room_name_json': mark_safe(json.dumps(room_name))
    })

创建room视图的路由chat/urls.py:

from django.conf.urls import url
from chat import views

urlpatterns = [
    url(r'^$', views.index, name='index'),
    url(r'^(?P<room_name>[^/]+)/$', views.room, name='room'),
]

现在启动开发服务器:

$ python manage.py runserver

现在,我们输入房间名,敲回车会出现如下界面:

AA

现在它还不能正常工作,因为这个房间页面,尝试打开连接ws://127.0.0.1:8000/ws/chat/lobby/, 但是我们的后端并没有创建一个消费者来接收WebSocket连接。

编写消费者

当Django接收一个HTTP请求,它会根据根URL(URLconf)来查找对应的视图函数,然后会调用这个视图函数来处理这个请求。

同样的,当一个Channel接收了一个WebSocket连接,它会根据根路由配置来查找对应的消费者,然后调用消费者的各个函数来处理来自这个链接的事件。

我们将会编写一个简单的消费者,它会在路径/ws/chat/ROOM_NAME来接收WebSocket连接,然后把接收到的任何消息,回显给同一个WebSocket链接。

编写消费者代码(chat/consumers.py):

from channels.generic.websocket import WebsocketConsumer
import json


class ChatConsumer(WebsocketConsumer):
    def connect(self):
        self.accept()

    def disconnect(self, close_code):
        pass

    def receive(self, text_data):
        text_data_json = json.loads(text_data)
        message = text_data_json['message']

        self.send(text_data=json.dumps({
            'message': message
        }))

这是一个同步WebSocket使用者,它接受所有连接,接收来自其客户端的消息,并将这些消息回显给同一客户端。目前,它不会广播消息给同一个房间的其他客户端。

接下来,我们需要为我们的消费者来配置一个路由(chat/routing.py):

from django.conf.urls import url
from chat import consumers

websocket_urlpatterns = [
    url(r'^ws/chat/(?P<room_name>[^/]+)/$', consumers.ChatConsumer),
]

然后我们需要在项目的根路由配置中配置chat.routing模块。在根路由(mysite/routing.py)进行如下配置:

from channels.routing import ProtocolTypeRouter
from channels.routing import URLRouter
from channels.auth import AuthMiddlewareStack
import chat.routing

application = ProtocolTypeRouter({
    # Empty for now (http->django views is added by default)
    'websocket': AuthMiddlewareStack(
        URLRouter(
            chat.routing.websocket_urlpatterns
        )
    ),
})

这个根路由配置指定了,在与Channels开发服务器建立连接的时候,ProtocolTypeRouter会检查该连接的类型。如果是一个WebSocket连接(ws://或者wss://),那么这个连接会交给AuthMiddlewareStack

这个AuthMiddlewareStack会使用当前已经通过身份认证的用户来填充连接的scope

这个类似于Django的AuthenticationMiddleware

最后,链接会转给URLRouter

这个URLRouter将会检查链接的HTTP的路径,最后路由给一个特定的消费者。

现在,我们来验证一下,是否能正常工作:

$ python manage.py runserver

此时已经能正常工作了。不过同一个房间的不同的客户端的消息还是不通。可以重新打开一个新的浏览器来测试。

为了能够让不同的客户端之间的消息能传递。我们需要让同一个ChatConsumer的多个实例能够相互交谈。

Channels提供了channel layer的抽象,可以启用不同消费者之间的通信。


启用channel layer

channel layer 是一个通信系统。它允许多个消费者实例之前或者是和Django的其他部分进行通信。

channel layer提供了以下的抽象:

  • channel——邮箱。每个channel都有名字,任何拥有该频道名称的人都可以向该频道发送消息。

  • group——一组管理的channel。每个组有名字。任何拥有该组名称的人都可以按名称,向该组添加/删除一个频道,并向该组中的所有频道发送消息。不允许枚举group中的channel。

每个消费者实例都会自动的生成一个唯一的通道名称。因此可以通过channel layer进行通信。

在我们的聊天室应用中我们希望同一个房间中的多个ChatConsumer能够互相通信。要做到这一点,我们可以将ChatConsumer添加到一个基于房间名称的group中。这将允许ChatConsumers将消息传输到同一房间内的所有其他ChatConsumer。

我们使用Redis作为我们channel layer的后端存储。

安装channels_redis

$ pip install channels_redis

在使用channel layer之前,我们必须要先配置它:

# Channels
ASGI_APPLICATION = 'mysite.routing.application'
CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'channels_redis.core.RedisChannelLayer',
        'CONFIG': {
            "hosts": [("redis://127.0.0.1:33307/8")],  # "redis://:mypassword@127.0.0.1:6379/0"
        },
    },
}

可以配置多个channel layer,但是大部分情况下,只会使用default通道层。

为了验证channel layer可以和Redis通信,我们在命令行执行如下操作:

$ python manage.py shell
Python 3.6.5 (default, Mar 30 2018, 06:42:10)
[GCC 4.2.1 Compatible Apple LLVM 9.0.0 (clang-900.0.39.2)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> import channels.layers
>>> channel_layer = channels.layers.get_channel_layer()
>>> from asgiref.sync import async_to_sync
>>> async_to_sync(channel_layer.send)('test_channel', {'type': 'hello'})
>>> async_to_sync(channel_layer.receive)('test_channel')
{'type': 'hello'}
>>> exit()

接下来在ChatConsumer使用channel layer,修改代码chat/consumers.py:

from channels.generic.websocket import WebsocketConsumer
from asgiref.sync import async_to_sync
import json


class ChatConsumer(WebsocketConsumer):
    def connect(self):
        self.room_name = self.scope['url_route']['kwargs']['room_name']
        self.room_group_name = 'chat_%s' % self.room_name

        # Join room group
        async_to_sync(self.channel_layer.group_add)(
            self.room_group_name,
            self.channel_name
        )

        self.accept()

    def disconnect(self, close_code):
        # Leave room group
        async_to_sync(self.channel_layer.group_discard)(
            self.room_group_name,
            self.channel_name
        )

    # Receive message from WebSocket
    def receive(self, text_data):
        text_data_json = json.loads(text_data)
        message = text_data_json['message']

        # Send message to room group
        async_to_sync(self.channel_layer.group_send)(
            self.room_group_name,
            {
                'type': 'chat_message',  # 对应方法名称
                'message': message
            }
        )

    # Receive message from room group
    def chat_message(self, event):
        message = event['message']

        # Send message to WebSocket
        self.send(text_data=json.dumps({
            'message': message
        }))

当用户发布消息时,JavaScript函数将通过WebSocket将消息发送给ChatConsumerChatConsumer将收到该消息并将其转发给与房间名称对应的组。同一组中的每个ChatConsumer(并且因此在同一个房间中)将接收来自组的消息,并通过WebSocket将其转发回JavaScript,并将其附加到聊天记录中。

部分解释:

  • self.scope[‘url_route’][‘kwargs’][‘room_name’]

    • 从链接中获取'room_name'

    • 每个消费者都有一个包含其连接信息的scope,特别包括来自URL路由和当前经过身份验证的用户的任何位置或关键字参数(如果有)。

  • self.room_group_name = ‘chat_%s’ % self.room_name

    • 直接从用户指定的房间名称构造通道组名称,而不进行任何引用或转义

    • Group names may only contain letters, digits, hyphens, and periods.

  • async_to_sync(self.channel_layer.group_add)(…)

    • 加入一个group

    • 这个async_to_sync()方法是必须的,因为ChatConsumer是一个同步的操作,而channel layer方法都是异步的。所以这里要将channel_layer的方法转为同步。

    • 组名仅限于ASCII字母数字,连字符和句点

  • self.accept()

    • 接收一个WebSocket链接

    • 如果你不在connect()方法中调用accept(),那么连接将被拒绝并关闭。例如,您可能想要拒绝连接,因为请求用户无权执行请求的操作。

    • 如果您选择接受连接,则建议将accept()称为connect()中的最后一个操作。

  • async_to_sync(self.channel_layer.group_discard)(…)

    • 离开一个组

  • async_to_sync(self.channel_layer.group_send)

    • 将一个事件发送给一个组

    • 事件有一个特殊的'type'键,对应于应该在接收事件的消费者上调用的方法的名称。

最后我们开启多个浏览器选显卡来验证一下吧:

$ python manage.py runserver


AA



如无特殊说明,文章均为本站原创,转载请注明出处