保姆级别 附带源码 Django集成channels(一)实现简单聊天功能

     阅读:97

目录

前言

不想看我瞎BB可以直接跳到这里

1.WebSocket

1.1 ajax轮询

1.2 long poll

1.3 Websocket

2.Channels

2.1 WSGI

2.2 ASGI

3.准备Django环境

3.1 安装channels

3.2 创建一个Django项目

 3.3 创建一个聊天应用程序

3.4 添加索引视图

3.5 在chat/views.py中创建视图函数:

3.6 在chat中创建一个名为urls.py用来存放路由:

3.7 在mysite/urls.py中配置总路由

3.8 启动一下,检查

4. channels在Django中的集成

4.1 配置环境

4.3 在chat/views.py创建视图函数

4.4 在chat/urls.py 创建路由

4.5 WebSocket 连接配置

4.6 配置socket路由

4.7 启用通道层

4.7.1 安装 channels_redis

4.7.2 配置通道层的环境

4.7.3 测试一下环境是否配置成功

4.8 替换旧代码

4.9 测试

5. 源码参考


前言

网上绝大多数博客都是发送端或者接收端同时作为服务器,这不扯么…  要不就是写的乱七八糟的根本运行不了,实在受不了,经过一段时间的学习,决定自己写一份保姆级别的集成文章,文末附带源码

不想看我瞎BB可以直接跳到这里

为了实现双向奔赴通信,我选择websocket;但是Django 3.0往上走就不支持websocket了就很无语,所以这里我通过channels实现websocket。

1.WebSocket

        在讲Websocket之前,先了解下 long pollajax轮询 的原理。

1.1 ajax轮询

        ajax轮询的原理非常简单,让浏览器隔个几秒就发送一次请求,询问服务器是否有新信息。这样就很浪费服务器资源,项目小还好,项目要是大一点老板直接让你走人。

1.2 long poll

  long poll 其实原理跟 ajax轮询 差不多,都是采用轮询的方式,不过采取的是阻塞模型(一直打电话,没收到就不挂电话),也就是说,客户端发起连接后,如果没消息,就一直不返回Response给客户端。直到有消息才返回,返回完之后,客户端再次建立连接,周而复始。

  ajax轮询 需要服务器有很快的处理速度和资源(速度)。long poll 需要有很高的并发,也就是说同时接待客户的能力(场地大小)。

1.3 Websocket

  WebSocket是一种在单个TCP连接上进行全双工通讯的协议。WebSocket允许服务端主动向客户端推送数据。在WebSocket协议中,客户端浏览器和服务器只需要完成一次握手就可以创建持久性的连接,并在浏览器和服务器之间进行双向的数据传输。

2.Channels

  Django本身不支持WebSocket,但可以通过集成Channels框架来实现WebSocket

  Channels是针对Django项目的一个增强框架,可以使Django不仅支持HTTP协议,还能支持WebSocketMQTT等多种协议,同时Channels还整合了Djangoauth以及session系统方便进行用户管理及认证。

2.1 WSGI

WSGI(Python Web Server Gateway Interface):为Python语言定义的Web服务器和Web应用程序或者框架之间的一种简单而通用的接口。

2.2 ASGI

ASGI(Asynchronous Web Server Gateway Interface):异步网关协议接口,一个介于网络协议服务和Python应用之间的标准接口,能够处理多种通用的协议类型,包括HTTPHTTP2WebSocket

WSGI是基于HTTP协议模式的,不支持WebSocket,而ASGI的诞生则是为了解决Python常用的WSGI不支持当前Web开发中的一些新的协议标准。同时,ASGI对于WSGI原有的模式的支持和WebSocket的扩展,即ASGIWSGI的扩展。

3.准备Django环境

本文是为 Channels 3.0 编写的,它支持 Python 3.6+Django 2.2+

python3 -m django --version

3.1 安装channels

pip install channels

3.2 创建一个Django项目

 3.3 创建一个聊天应用程序

python manage.py startapp chat

 我们需要告诉我们的项目 chat应用程序已安装。编辑 mysite/settings.py文件并添加'chat'INSTALLED_APPS设置。它看起来像这样:

3.4 添加索引视图

我们现在将创建第一个视图,一个索引视图,允许您键入要加入的聊天室的名称。

templates目录中创建一个名为chat的文件夹。用来存放html页面。

index.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8"/>
    <title>Chat Rooms</title>
</head>
<body>
    What chat room would you like to enter?<br>
    <input id="room-name-input" type="text" size="100"><br>
    <input id="room-name-submit" type="button" value="Enter">

    <script>
        document.querySelector('#room-name-input').focus();
        document.querySelector('#room-name-input').onkeyup = function(e) {
            if (e.keyCode === 13) {  // enter, return
                document.querySelector('#room-name-submit').click();
            }
        };

        document.querySelector('#room-name-submit').onclick = function(e) {
            var roomName = document.querySelector('#room-name-input').value;
            window.location.pathname = '/chat/' + roomName + '/';
        };
    </script>
</body>
</html>

3.5 在chat/views.py中创建视图函数:

# chat/views.py
from django.shortcuts import render

def index(request):
    return render(request, 'chat/index.html')

3.6 在chat中创建一个名为urls.py用来存放路由

# chat/urls.py
from django.urls import path

from . import views

urlpatterns = [
    path('', views.index, name='index'),
]

 

3.7 在mysite/urls.py中配置总路由

# mysite/urls.py
from django.conf.urls import include
from django.urls import path
from django.contrib import admin

urlpatterns = [
    path('chat/', include('chat.urls')),
    path('admin/', admin.site.urls),
]

3.8 启动一下,检查

 

Django 准备成功!

4. channels在Django中的集成

到目前为止,我们刚刚创建了一个常规的 Django 应用程序;我们根本没有使用过 Channels 库。现在是整合的时候了。

我从为 Channels 创建一个根路由配置开始。Channels路由配置是一个类似于 Django URLconf 的 ASGI 应用程序,因为它告诉 Channels 当 Channels 服务器接收到 HTTP 请求时要运行什么代码。

4.1 配置环境

mysite/asgi.py文件下

没有这个python文件可以自己手动创建一个

# mysite/asgi.py
import os

from channels.routing import ProtocolTypeRouter
from django.core.asgi import get_asgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')

application = ProtocolTypeRouter({
    "http": get_asgi_application(),
    # Just HTTP for now. (We can add other protocols later.)
})

 注意:

     Django 2.2 没有内置的 ASGI 支持,所以我们需要使用 Channel 的后备替代方案。mysite/asgi.py像这样创建:

# mysite/asgi.py
import os

import django
from channels.http import AsgiHandler
from channels.routing import ProtocolTypeRouter

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')
django.setup()

application = ProtocolTypeRouter({
  "http": AsgiHandler(),
  # Just HTTP for now. (We can add other protocols later.)
})

现在将频道库添加到已安装的应用程序列表中。编辑mysite/settings.py文件并添加'channels'到 INSTALLED_APPS设置中,还需要将 Channels 指向根路由配置

 全新启动一下,下面这种就算配置成功。

4.2 添加视图

templates/chat里面新增一个html页面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">
    {{ room_name|json_script:"room-name" }}
    <script>
        const roomName = JSON.parse(document.getElementById('room-name').textContent);

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

        chatSocket.onmessage = function(e) {
            const data = JSON.parse(e.data);
            document.querySelector('#chat-log').value += (data.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) {
            const messageInputDom = document.querySelector('#chat-message-input');
            const message = messageInputDom.value;
            chatSocket.send(JSON.stringify({
                'message': message
            }));
            messageInputDom.value = '';
        };
    </script>
</body>
</html>

4.3 在chat/views.py创建视图函数

# chat/views.py
from django.shortcuts import render

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

def room(request, room_name):
    return render(request, 'chat/room.html', {
        'room_name': room_name
    })

4.4 在chat/urls.py 创建路由

# chat/urls.py
from django.urls import path

from . import views

urlpatterns = [
    path('', views.index, name='index'),
    path('<str:room_name>/', views.room, name='room'),
]

4.5 WebSocket 连接配置

注意:

   使用公共路径前缀(例如/ws/将 WebSocket 连接与普通 HTTP 连接区分开来)是一种很好的做法,因为它会使在某些配置中更容易地将 Channels 部署到生产环境。

   创建一个新文件chat/consumers.py

 将以下代码写入chat/consumers.py

# chat/consumers.py
import json
from channels.generic.websocket import WebsocketConsumer

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

4.6 配置socket路由

现在还需要为chat具有到消费者的路由的应用程序创建路由配置

     创建一个新文件chat/routing.py

 将以下代码写入chat/routing.py

# chat/routing.py
from django.urls import re_path

from . import consumers

websocket_urlpatterns = [
    re_path(r'ws/chat/(?P<room_name>\w+)/$', consumers.ChatConsumer.as_asgi()),
]

   调用as_asgi()类方法是为了获得一个 ASGI 应用程序,该应用程序将为每个用户连接实例化我们的消费者实例。这类似于 Django 的as_view(),它对每个请求的 Django 视图实例起到相同的作用。

下一步是将根路由配置指向 chat.routing模块

# mysite/asgi.py
import os

from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application
import chat.routing

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings")

application = ProtocolTypeRouter({
  "http": get_asgi_application(),
  "websocket": AuthMiddlewareStack(
        URLRouter(
            chat.routing.websocket_urlpatterns
        )
    ),
})

这个时候就已经配置好了运行之后发现根本聊天不起来!!

这不就完了吗。。。。。。。

解决办法:

4.7 启用通道层

我们将使用一个使用 Redis 作为其后备存储的通道层。要启动 Redis 服务器

要是启动redis出错请看我另外一篇文章:windows下Redis server启动一闪而过的解决方案

4.7.1 安装 channels_redis

以便 Channels 知道如何与 Redis 交互

pip install channels_redis

4.7.2 配置通道层的环境

mysite/settings.py文件里面并CHANNEL_LAYERS在底部添加一个设置

CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'channels_redis.core.RedisChannelLayer',
        'CONFIG': {
            "hosts": [('127.0.0.1', 6379)],
        },
    },
}

4.7.3 测试一下环境是否配置成功

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'}

4.8 替换旧代码

 现在我们有了一个通道层,在/chat/consumer. 将以下代码写入chat/consumers.py,替换旧代码

# chat/consumers.py
import json
from asgiref.sync import async_to_sync
from channels.generic.websocket import WebsocketConsumer

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 将消息传输到 ChatConsumer。ChatConsumer将收到该消息并将其转发到与房间名称对应的组。同一组中(因此在同一房间中)的每个 ChatConsumer 都会收到来自该组的消息,并通过 WebSocket 将其转发回 JavaScript,然后将其附加到聊天日志中,从而实现聊天功能。

ChatConsumer代码的几个部分进一步的解释:

  • self.scope['url_route']['kwargs']['room_name']

    • 'room_name'chat/routing.py 打开到消费者的 WebSocket 连接的 URL 路由中获取参数。
    • 每个使用者都有一个范围,其中包含有关其连接的信息,特别是包括来自 URL 路由的任何位置或关键字参数以及当前经过身份验证的用户(如果有)。
  • self.room_group_name = 'chat_%s' % self.room_name

    • 直接从用户指定的房间名称构造一个 Channels 组名称,没有任何引用或转义。
    • 组名只能包含字母、数字、连字符和句点。因此,此示例代码将在包含其他字符的房间名称上失败。
  • async_to_sync(self.channel_layer.group_add)(...)

    • 加入一个小组。
    • 需要 async_to_sync(...) 包装器,因为 ChatConsumer 是一个同步 WebsocketConsumer,但它正在调用异步通道层方法。(所有通道层方法都是异步的。)
    • 组名称仅限于 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'键,对应于应该在接收事件的消费者上调用的方法的名称。

4.9 测试

 到这里已经完成了简单的channels在Django中的集成。 

下一篇文章将会说到Django集成channels实现socket 摄像头实时视频传输

5. 源码参考

码云:Django集成channels: Django集成channels系列