欢迎来到破竹的博客

人生三从境界:昨夜西风凋碧树,独上高楼,望尽天涯路。 衣带渐宽终不悔,为伊消得人憔悴。 众里寻他千百度,蓦然回首,那人却在灯火阑珊处。

Django用websocket实现聊天室之筑基篇

最近闲来无事,无意发现一个聊天室的前端UI,看着挺好看的但是没有聊天室的通信代码,于是想给它安装电池(通信部分),先看UI:

 

 开始通信部分的工作:

 使用的组件:

  Django1.11.13

  channels 2.3.1

  redis

  jQuery

Django实现聊天室一般有实现轮训(比较老,效率低)、websocket等;这里用websocket,实现websocket有多种途径,一般有:channels和dwebsocket等,dwebsocket使用简单但是看了下官网好像只提供了差不多Django1.8版本以前的用法(添加MIDDLEWARE_CLASSES = ['dwebsocket.middleware.WebSocketMiddleware']),而Django1.11.13废弃了MIDDLEWARE_CLASSES,使用MIDDLEWARE,具体迁移方法未做深究,这里就直接使用channels

channels官方文档:https://channels.readthedocs.io/en/latest/

准备阶段

1.安装channels
sudo pip install -U channels

检测下 channels是否安装成功

$  python3 -c 'import channels; print(channels.__version__)'
2.3.1


2.如果没安装redis,先安装redis

(1)Ubuntu安装redis 使用命令sudo apt-get install redis-server
  whereis redis 查看redis的安装位置
  ps -aux | grep redis 查看redis服务的进程运行
  netstat -nlt | grep 6379根据redis运行的端口号查看redis服务器状态,端口号前是redis服务监听的IP(默认只有本机IP 127.0.0.1)

(2)问题解决,我的远程腾讯云Ubuntu服务器安装完redis无法启动,提示:

分析是主机上禁用了IPv6,而Ubuntu的redis-server软件包(版本5:4.0.9-1)附带了:绑定127.0.0.1 :: 1
解决办法:
修改redis配置文件中的 bind 地址;注释 bind 地址或将 bind 地址修改为 0.0.0.0
vim /etc/redis/redis.conf
// 注释bind地址
#bind 127.0.0.1 ::1
//或修改bind地址-并允许其开放访问
bind 0.0.0.0

启动redis 服务
service redis-server start

检查服务及端口
systemctl status redis-server
//提示信息
#redis-server.service - Advanced key-value store
#Active: active (running) since Fri 2019-01-25 15:24:47 CST; 41min ago

netstat -ntpl | grep 6379
//提示信息
#tcp        0      0 0.0.0.0:6379            0.0.0.0:*               LISTEN      28507/redis-server


3.安装channels_redis
sudo pip install channels_redis

4.确保channels可以与Redis通信。打开Django shell并运行以下命令:

 

$ python3 manage.py shell
>>> 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'}

 

 

 

接下来创建一个Django项目和一个app,我创建的项目名chatroom,app名chatPage

 目录结构:

chatroom
├── chatPage
│   ├── admin.py
│   ├── apps.py
│   ├── __init__.py
│   ├── models.py
│   ├── urls.py
│   ├── views.py
├── chatroom
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── db.sqlite3
└── manage.py

然后直接按channels官网流程走一遍,先把通信调通:

setting.py中注册chatPage,顺便把channels 也注册了

 

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'channels',
    'chatPage',
]

 

项目根目录添加chatPage  应用的路由

from django.contrib import admin
from django.conf.urls import url ,include 

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^chatPage/' ,include("chatPage.urls",namespace="chatPage")),
    
]

chatPage目录下新建目录templates,并在templates目录中创建一个名为chat.html的文件,作为登陆首页,将以下代码加入chat.html

<!-- chat/templates/chat/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>

chatPage目录下view.py写视图函数

def index(request):
    #return HttpResponse("helloworld")
    response= render_to_response('chat.html') 
    return render(request, 'chat.html', {})
    return response  

 chatPage目录下urls.py添加视图路由

from django.conf.urls import url

from . import views

app_name='chatPage'

urlpatterns = [
    url(r'^chat$', views.index, name= 'chat'),

]

现在运行 python manage.py runserver

浏览器输入http://localhost:8000/chatPage/chat,即可显示账号名输入页面,但是输入还不能跳转,接下来添加聊天室页面,添加之前先引入channelsRedis到Django项目:

setting.py中添加下面的代码:

ASGI_APPLICATION = 'chatroom.routing.application'   #websocket扩展

#在本地6379端口启动redis :redis-server

#
配置channels_redis的四种方法,任选择一种,建议选最后一种需要加密密码的,然后需要给Redis加密 CHANNEL_LAYERS = { "default": { "BACKEND": "channels_redis.core.RedisChannelLayer", "CONFIG": { "hosts": [("localhost", 6379)], }, }, }   CHANNEL_LAYERS = { 'default': { 'BACKEND': 'channels_redis.core.RedisChannelLayer', 'CONFIG': {"hosts": ["redis://127.0.0.1:6379/8"],}, }, }   CHANNEL_LAYERS = { 'default': { 'BACKEND': 'channels_redis.core.RedisChannelLayer', 'CONFIG': {"hosts": [('127.0.0.1', 6379)],},}, }   CHANNEL_LAYERS = { "default": { "BACKEND": "channels_redis.core.RedisChannelLayer", "CONFIG": { "hosts": ["redis://:password@127.0.0.1:6379/0"], "symmetric_encryption_keys": [SECRET_KEY], }, }, }

 

redis设置密码的两种方式

1-修改配置文件(需重启)
2-命令修改密码(无需重启)
方式1:
1-打开 /etc/redis/redis.config 文件
2-找到 :# requirepass foobared # 去掉行前的注释,并修改密码为所需要的密码。保存文件
3-重启redis sudo service redis restart
4-连接redis: redis-cli -h 127.0.0.1 -p 6379 -a 密码
方式2:
1-连接redis
2-config get requirepass # 获取当前密码
3-config set requirepass 123456 # 设置当前密码为123456
4-config get requirepass # 获取当前密码
""" 

chatPage目录下templates目录中创建一个名为chatroom.html的文件,作为登陆首页,将以下代码加入chatroom.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 }};

    //console.log(window.location.host);
    // 打开一个WebSocket:
    var chatSocket = new WebSocket(
        "ws://" + window.location.host +
        "/ws/chatPage/" + roomName +"/");
    
    // 响应onmessage事件:
    chatSocket.onmessage = function(e) {
        var data = JSON.parse(e.data);
        console.log(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>

写视图函数


def chatroom(request, room_name):
    print(room_name)
    return render(request, 'chatroom.html', {
        'room_name_json': mark_safe(json.dumps(room_name))
    })

添加路由


from django.conf.urls import url

from . import views

app_name='chatPage'

urlpatterns = [
    url(r'^chat$', views.index, name= 'chat'),

    url(r'^chatroomPage/(.*)/', views.chatroom),

]
ASGI_APPLICATION = 'chatroom.routing.application' 中的chatroom为工程名
然后需要建立属于websocket的路由文件,在和工程同名的目录chatroom下新建routing.py文件,并写入下面的代码:
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
import chatroom.routing

#ProtocolTypeRouter将首先检查连接的类型。如果是websocket,连接会交给AuthMiddlewareStack,AuthMiddlewareStack会对当前身份验证的用户填充连接的scope,然后连接将被给到URLRouter.根据提供的url模式,URLRouter将检查连接的http路径,将其路由到指定的特定的consumer.
application = ProtocolTypeRouter({      
    # (http->django views is added by default)
    'websocket': AuthMiddlewareStack(
        URLRouter(
            chatroom.routing.websocket_urlpatterns
        )
    ),
})

ProtocolTypeRouter将首先检查连接的类型。如果是websocket,连接会交给AuthMiddlewareStack,
AuthMiddlewareStack会对当前身份验证的用户填充连接的scope,然后连接将被给到URLRouter.根据提供的url模式,URLRouter将检查连接的http路径,将其路由到指定的特定的consumer.URLRouter中的chatPage为自己建的APP名
然后在应用chatPage目录下新建routing.py文件,写入下面的代码,添加websocket访问的路由
from django.conf.urls import url

from . import consumers

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

然后同级目录下新建名为consumers.py的文件,官网是先介绍同步的websocket的写法,这里直接一步到位,实现异步的websocket方式,写入下面的代码:

#-*-coding:utf-8-*-
from channels.generic.websocket import AsyncWebsocketConsumer
import json

class ChatConsumer(AsyncWebsocketConsumer):
    async def connect(self):        #连接时调用
        print(self.scope)
        self.room_name = self.scope['url_route']['kwargs']['room_name']     #从路由获取"room_name" 参数
                                                                           #每个 consumer 都有一个 scope, 其中包含有关其连接的信息, 特别是来自 URL 路由和当前经过身份验证的用户 (如果有的话) 中的任何位置或关键字参数。

        self.room_group_name = 'chat_Group'  #'chat_%s' % self.room_name    #给一个固定组名

        # Join room group
        await self.channel_layer.group_add(     #发送内容给组的通道层,加入房间
            self.room_group_name,               
            self.channel_name                   
        )

        await self.accept()     #等待连接,如果你在 connect() 方法中不调用 accept(), 则连接将被拒绝并关闭

    async def disconnect(self, close_code):
        # Leave room group
        await self.channel_layer.group_discard( #离开房间删除通道层数据
            self.room_group_name,
            self.channel_name
        )

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

        # Send message to room group
        await self.channel_layer.group_send(
            self.room_group_name,   #发送消息到room_group_name通道层
            {
                'type': 'chat_message',     #消息类型,对应处理方法
                'message': message
            }
        )

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

        # Send message to WebSocket
        await self.send(text_data=json.dumps({
            'message': message,
            #'username':self.room_name
        }))
        
        
        #1.通道层具有纯异步接口(用于发送和接收);如果要从同步代码中调用它们,则需要将它们包装在转换器中。
        #2.可以从项目中获取默认通道层 channels.layers.get_channel_layer(),但是如果您使用的是consumers ,则会自动在使用者上为您提供一个副本self.channel_layer
        #3.默认情况下send(),group_send(),group_add()等功能异步功能,这意味着你要await他们。如果需要从同步代码中调用它们,则需要使用方便的 asgiref.sync.async_to_sync包装器:
#from asgiref.sync import async_to_sync
#async_to_sync(channel_layer.send)("channel_name", {...})


函数说明:
self.scope[‘url_route’][‘kwargs’][‘room_name’]
从给 consumer 打开 WebSocket 连接的 chat/routes.py 中的 URL 路由中获取 "room_name" 参数。
每个 consumer 都有一个 scope, 其中包含有关其连接的信息, 特别是来自 URL 路由和当前经过身份验证的用户 (如果有的话) 中的任何位置或关键字参数。


self.room_group_name = ‘chat_%s’ % self.room_name
直接从用户指定的房间名称构造一个 Channels group 名称, 无需任何引用或转义。
组名可能只包含字母、数字、连字符和句点。因此, 此示例代码将在具有其他字符的房间名称上发生失败。


async_to_sync(self.channel_layer.group_add)(…)
加入一个 group。
async_to_sync(…) wrapper 是必需的, 因为 ChatConsumer 是同步 WebsocketConsumer, 但它调用的是异步 channel layer 方法。(所有 channel layer 方法都是异步的)
group 名称仅限于 ASCII 字母、连字符和句点。由于此代码直接从房间名称构造 group 名称, 因此如果房间名称中包含的其他无效的字符, 代码运行则会失败。


self.accept()
接收 WebSocket 连接。
如果你在 connect() 方法中不调用 accept(), 则连接将被拒绝并关闭。例如,您可能希望拒绝连接, 因为请求的用户未被授权执行请求的操作。
如果你选择接收连接, 建议 accept() 作为在 connect() 方法中的最后一个操作。


async_to_sync(self.channel_layer.group_discard)(…)
离开一个 group。
将 event 发送到一个 group。
event 具有一个特殊的键 'type' 对应接收 event 的 consumers 调用的方法的名称。

至此一个基本的聊天室通信部分就基本完成了,运行python manage.py runserver

浏览器打开http://localhost:8000/chatPage/chat   键入名称,回车,即可到聊天界面,此时发送的消息只能自己看到,还不能达到多人聊天,原因是不在同一个group中,现在
在consumers.py修改这句,self.room_group_name = 'chat_Group'  #'chat_%s' % self.room_name,这里我们设置了一个固定的房间名作为Group name,所有的消息都会发送到这个Group里边,当然你也可以通过参数的方式将房间名传进来作为Group name,从而建立多个Group,这样可以实现仅同房间内的消息互通

 

当我们启用了channel layer之后,所有与consumer之间的通信将会变成异步的,所以必须使用async_to_sync

一个链接(channel)创建时,通过group_add将channel添加到Group中,链接关闭通过group_discard将channel从Group中剔除,收到消息时可以调用group_send方法将消息发送到Group,这个Group内所有的channel都可以收的到

group_send中的type指定了消息处理的函数,这里会将消息转给chat_message函数去处理

现在再次在多个浏览器上打开聊天页面输入消息,发现彼此已经能够看到了

将Django + channel部署到daphne +supervisor 或部署到远程apache2服务器

到此已经完成了聊天室的基本通信功能,但是一直在本地调试,接下来需要将Django + channel正式部署到远程服务器,形成上线雏形,这里使用以下两种部署方式 daphne+supervisor和 Apache2代理

(1)daphne+supervisor 的部署

在项目目录中,已经有一个名为的文件wsgi.py,它是Django作为WSGI应用程序呈现。在它旁边创建一个新文件asgi.py,将下面的代码加入其中

 

"""
ASGI entrypoint. Configures Django and then runs the application
defined in the ASGI_APPLICATION setting.
"""

import os
import django
from channels.routing import get_default_application

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "chatroom.settings")
django.setup()
application = get_default_application()

 

启动daphne 测试是否正常运行(成功以后退出):

daphne -p 8090 chatroom.asgi:application

或规定IP和端口
daphne -b 0.0.0.0 -p 8090 chatroom.asgi:application

 

安装supervisor

  supervisor是由python实现的一个进程管理工具,可以确保所管理的进程一直运行,当进程一点中断supervisord会自动进行重启。

安装步骤:

sudo pip install supervisor        安装

sudo echo_supervisord_conf > /etc/supervisord.conf        生成配置文件

使用supervisor管理daphne进程

编辑/etc/supervisord.conf加入配置

[program:daphne]
socket=tcp://localhost:8005            # daphne 运行的端口
directory=/root/mysite                # 项目的目录,改为自己的项目目录
command=daphne -b 0.0.0.0 -p 8005 chatroom.asgi:application            # 执行的命令,改为自己的项目名称
numprocs=4                            # 执行的进程数,根据官网说,和cpu数相同
process_name=asgi%(process_num)d    # 为每一个进程起不同的名字
autostart=true                        # 这两个是自动启动和恢复进程
autorestart=true

stdout_logfile=/root/mysite/logs/websocket.log    # 日志位置
redirect_stderr=true

启动supervisor,加载配置文件,启动之前先退出daphne -b 0.0.0.0 -p 8090 chatroom.asgi:application命令,不然会提示端口占用

supervisord -c /etc/supervisord.conf

启动或者停止daphne

supervisorctl start daphne
supervisorctl stop daphne
supervisorctl stop all   #停止所有进程


supervisorctl reread                
supervisorctl update            # 这两个命令是修改了配置文件后运行
supervisorctl reload            # 重新启动

打开浏览器输入远程地址发现可以连接Django,同时websocket也能正常通信

现在已经成功部署了daphne+supervisor

下面的版块尚未测试成功,本来自己的网站是在Apache2上面运行的,结果还没能部署成功,现在只能把网站分开成两部分了,一部分运行在Apache2,一部分运行在daphne,有时间再解决下面的版块的问题

(2)Apache2代理webscoket(此版块尚未成功,留待解决)

websocket代理到Apache之前需要先将Django部署到Apache可参考我的另一篇帖子Ubuntu个人使用笔记整理

这里直接到websocket代理部分

修改Apache2配置文件

cd /etc/apache2/sites-available

暂时从nginx的配置相关帖子中找到下面的配置参数,需要加入到加入/etc/apache2/sites-available目录下的自己的网站配置文件,比如000-default.conf,具体下面的参数怎么加, 暂时留待解决,到此可以先使用daphne方式启动,配置Apache2等我实验成功再续写


  ProxyPass /ws ws://localhost:8000/ws                                                        #  websocket代理
    ProxyPassReverse /ws ws://localhost:8000/ws    
    ProxyRequests Off
    ProxyMaxForwards 100
    ProxyPreserveHost On

proxy_pass http://wsbackend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Host $server_name;

重新加载apache2

sudo /etc/init.d/apache2 reload //重新加载
sudo /etc/init.d/apache2 restart //重启apache服务

参考 :

https://blog.csdn.net/weixin_42886895/article/details/89515365

https://www.cnblogs.com/wdliu/p/10032180.html

 

至此一个基本的聊天室已经基本完成,到这里通信部分就算告一段落了,下一篇帖子将会修改UI聊天面板源码并和后端通信结合,添加对话机器人,注册登录,加好友,群组,初步完成一个美观的聊天室,效果:

posted @ 2019-11-17 23:10  破竹  阅读(1468)  评论(0编辑  收藏  举报